Docker部署MySQL完全指南:数据持久化到主从复制实战

凌晨三点,我盯着终端上那行刺眼的报错,手心直冒汗。半个月的测试数据,全没了。只因为下午重启了一次MySQL容器。
说实话,我当时真的慌了。用Docker部署MySQL不是挺简单的吗?网上随便一搜,docker run -e MYSQL_ROOT_PASSWORD=123456 mysql 就能跑起来。我天真地以为这样就行了,直到那个深夜——容器重启,数据消失,世界崩塌。
你有没有遇到过类似的情况?或者是这些场景:想改个MySQL字符集,不知道怎么挂载配置文件;本地应用连接容器死活连不上,报错信息看得头大;生产环境要搞主从复制,Docker环境下完全不知道从哪下手。
老实讲,Docker MySQL的坑,我几乎全踩过一遍。这篇文章就是用来填坑的——从最基础的数据持久化,到配置文件挂载,再到生产级的主从复制,每个配置都是我验证过可以直接用的。
话不多说,咱们直接开干。
Docker MySQL单机部署基础
最简单的启动方式(以及为什么不推荐)
先说个最常见的误区。很多人第一次用Docker装MySQL,会直接这样:
docker run --name mysql-test -e MYSQL_ROOT_PASSWORD=123456 -d mysql:8.0跑起来了,容器状态正常,进去能操作数据库,一切看起来完美。但这是个定时炸弹。
为什么?容器的本质是临时的。你删掉容器,或者哪天手滑重启了一下,数据就全没了。MySQL默认把数据存在容器内的 /var/lib/mysql 目录,容器一删,这个目录也跟着消失。
我第一次踩这个坑时,还以为是MySQL出了bug。后来才明白,不是MySQL的问题,是我压根没做数据持久化。
数据持久化:挂载volume的正确姿势
说白了,数据持久化就是把MySQL的数据目录映射到宿主机,容器挂了数据还在。
Docker提供三种挂载方式:
- bind mount:直接映射宿主机的某个目录,比如
/home/mysql/data - named volume:由Docker管理的卷,不用关心实际存储位置
- tmpfs:用内存存储,重启就没,基本不用
2024年Docker官方推荐用named volume,性能已经和bind mount没啥区别了。我个人两种都用,看场景:开发环境用named volume省心,生产环境用bind mount方便备份。
直接上完整命令:
docker run --name mysql-persistent \
-e MYSQL_ROOT_PASSWORD=rootpwd123 \
-p 3306:3306 \
-v mysql-data:/var/lib/mysql \
-d mysql:8.0这里 -v mysql-data:/var/lib/mysql 就是关键。mysql-data 是volume名字(Docker会自动创建),/var/lib/mysql 是MySQL容器内的数据目录。
验证一下是不是真的持久化了:
# 进容器创建个数据库
docker exec -it mysql-persistent mysql -uroot -prootpwd123 -e "CREATE DATABASE testdb;"
# 停掉容器并删除
docker stop mysql-persistent
docker rm mysql-persistent
# 用同样的volume重新启动容器
docker run --name mysql-persistent \
-e MYSQL_ROOT_PASSWORD=rootpwd123 \
-p 3306:3306 \
-v mysql-data:/var/lib/mysql \
-d mysql:8.0
# 进去查看,testdb还在
docker exec -it mysql-persistent mysql -uroot -prootpwd123 -e "SHOW DATABASES;"看到 testdb 还在,就说明数据真的保住了。这种感觉,踏实。
配置文件挂载:自定义MySQL参数
数据持久化搞定了,下一个问题:怎么改MySQL配置?
比如你想把字符集改成 utf8mb4,或者调整最大连接数。如果不挂载配置文件,每次都得进容器手动改,重启容器又得改一次,烦死了。
MySQL的配置文件读取路径是 /etc/mysql/conf.d/,我们把自定义的 my.cnf 挂到这个目录就行。
先在宿主机创建配置文件:
mkdir -p /home/mysql/conf
cat > /home/mysql/conf/my.cnf << 'EOF'
[mysqld]
# 字符集设置
character-set-server=utf8mb4
collation-server=utf8mb4_unicode_ci
# 连接数设置
max_connections=1000
# 认证插件(解决部分客户端连接问题)
default_authentication_plugin=mysql_native_password
[client]
default-character-set=utf8mb4
EOF然后启动容器时挂载这个配置文件:
docker run --name mysql-custom \
-e MYSQL_ROOT_PASSWORD=rootpwd123 \
-p 3306:3306 \
-v /home/mysql/conf:/etc/mysql/conf.d \
-v /home/mysql/data:/var/lib/mysql \
-d mysql:8.0注意这里同时挂载了两个目录:配置文件和数据目录。
验证配置是否生效:
docker exec -it mysql-custom mysql -uroot -prootpwd123 -e "SHOW VARIABLES LIKE 'character%';"如果看到 character_set_server 是 utf8mb4,说明配置已经生效了。
解决外部连接问题
容器起来了,数据也持久化了,配置也挂上了。现在你想用本地的应用连接这个MySQL,结果——
ERROR 2003 (HY000): Can't connect to MySQL server on 'localhost' (Connection refused)或者
ERROR 1045 (28000): Access denied for user 'root'@'172.17.0.1'这两个错误,我当初也遇到过。排查了好久才搞明白。
问题1:Connection refused
这个多半是端口没映射对。启动容器时一定要加 -p 3306:3306,把容器的3306端口映射到宿主机的3306端口。
如果你宿主机的3306端口已经被占用了(比如本地已经装了MySQL),可以映射到其他端口:
-p 3307:3306 # 宿主机用3307访问,容器内还是3306还有一种情况是防火墙拦截了。如果是Linux服务器,检查一下防火墙:
# CentOS/RHEL
sudo firewall-cmd --zone=public --add-port=3306/tcp --permanent
sudo firewall-cmd --reload
# Ubuntu
sudo ufw allow 3306/tcp问题2:Access denied
这个是权限问题。MySQL默认创建的root用户只允许从 localhost 连接,外部IP连不上。
解决办法是修改root用户的host为 %(允许任何IP连接):
# 进入容器
docker exec -it mysql-custom mysql -uroot -prootpwd123
# 执行以下SQL
ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY 'rootpwd123';
GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' WITH GRANT OPTION;
FLUSH PRIVILEGES;这里要注意两点:
mysql_native_password这个认证插件兼容性更好,一些老版本的MySQL客户端需要这个- 生产环境千万别这么干,不要让root用户从任何IP都能连,要创建专门的用户并限制IP
改完后,外部应用应该就能连上了。
第一部分到这就差不多了。这几个问题搞定,基本的Docker MySQL单机部署就没啥大坑了。
使用Docker Compose优雅管理
为什么推荐用Docker Compose
如果你只是本地开发测试,前面那些 docker run 命令也够用了。但实际工作中,你会发现几个问题:
- 命令太长,每次都要翻记录找之前的命令
- 参数容易搞错,比如volume路径写错了
- 团队协作时,大家的启动命令不一样,配置混乱
- 需要多个容器协同工作时(比如MySQL + Redis + Nginx),一个个启动太麻烦
这时候Docker Compose就派上用场了。
说白了,Docker Compose就是把你那一长串 docker run 命令,写到一个YAML配置文件里。以后启动只需要 docker-compose up -d,停掉只需要 docker-compose down,配置文件还能放到Git里版本管理。
对了,团队里新来的同事问你”怎么启动MySQL”,你直接甩给他一个 docker-compose.yml,他 git clone 下来一键启动,不用问你半句。这感觉,真爽。
Docker Compose单机MySQL配置
直接上完整配置,我逐行解释:
version: '3.8'
services:
mysql:
image: mysql:8.0
container_name: mysql-standalone
restart: always
environment:
MYSQL_ROOT_PASSWORD: rootpwd123
MYSQL_DATABASE: myapp
MYSQL_USER: appuser
MYSQL_PASSWORD: apppwd123
ports:
- "3306:3306"
volumes:
- mysql-data:/var/lib/mysql
- ./conf/my.cnf:/etc/mysql/conf.d/my.cnf
- ./logs:/var/log/mysql
networks:
- mysql-network
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-prootpwd123"]
interval: 10s
timeout: 5s
retries: 3
volumes:
mysql-data:
networks:
mysql-network:
driver: bridge逐项解释一下关键配置:
environment部分:
MYSQL_ROOT_PASSWORD:root密码,必填MYSQL_DATABASE:容器启动时自动创建的数据库MYSQL_USER和MYSQL_PASSWORD:自动创建的普通用户(比root更安全)
volumes部分:
mysql-data:/var/lib/mysql:数据持久化,named volume方式./conf/my.cnf:/etc/mysql/conf.d/my.cnf:挂载配置文件,用相对路径./logs:/var/log/mysql:日志目录挂载,方便排查问题
restart: always:容器挂了自动重启,服务器重启后也会自动启动容器
healthcheck:健康检查,定期ping MySQL,如果挂了会自动重启
networks:自定义网络,如果你有多个容器需要互相通信,都加入这个网络就行
使用步骤:
- 创建目录结构:
mkdir -p mysql-docker/{conf,logs}
cd mysql-docker- 创建配置文件
conf/my.cnf:
[mysqld]
character-set-server=utf8mb4
collation-server=utf8mb4_unicode_ci
max_connections=1000
default_authentication_plugin=mysql_native_password
[client]
default-character-set=utf8mb4创建
docker-compose.yml(就是上面那个完整配置)启动:
docker-compose up -d- 查看状态:
docker-compose ps你会看到类似这样的输出:
Name Command State Ports
------------------------------------------------------------------------------------------------
mysql-standalone docker-entrypoint.sh mysqld Up (healthy) 0.0.0.0:3306->3306/tcp注意那个 (healthy) 状态,说明healthcheck通过了。
- 查看日志:
docker-compose logs -f mysql-f 参数是实时跟踪日志,类似 tail -f。如果启动有问题,日志里基本都能找到原因。
- 停止并删除容器:
docker-compose down注意:这只会删除容器,不会删除volume(也就是数据不会丢)。如果你想连数据一起删,用:
docker-compose down -v # 慎用!会删除所有数据说实话,我现在本地开发基本都用Docker Compose了。配置写一次,以后启停都是一条命令,省心。
生产级主从复制配置
MySQL主从复制原理简述
先说说为什么要搞主从复制。
单机MySQL在小项目里够用,但业务量一大就扛不住了。主从复制主要解决两个问题:
- 读写分离:主库负责写(INSERT、UPDATE、DELETE),从库负责读(SELECT)。大部分应用都是读多写少,把读请求分散到多个从库,性能立马上去
- 数据备份和高可用:主库挂了,从库还能顶上,至少读服务不受影响
原理不复杂:
- 主库(Master)开启binlog日志,记录所有数据变更操作
- 从库(Slave)连接到主库,读取binlog内容
- 从库有两个线程工作:IO线程负责从主库拉取binlog并保存到relay log,SQL线程负责执行relay log里的SQL语句
- 这样主库的数据变更就同步到从库了
Docker环境下配置主从,核心就是:
- 两个容器各自配置不同的server-id
- 主库开启binlog
- 从库连接主库并开启复制
听起来挺复杂,实际上用Docker Compose配置完,启动就行,没想象中那么难。
Master节点配置
先配置主库。主库需要做三件事:开启binlog、设置server-id、创建复制用户。
- 创建主库配置文件
conf/master.cnf:
[mysqld]
# 唯一服务器ID,主从不能重复
server-id=1
# 开启二进制日志
log-bin=mysql-bin
# binlog格式,ROW模式记录每一行的变化,更安全
binlog-format=ROW
# 字符集
character-set-server=utf8mb4
collation-server=utf8mb4_unicode_ci
# 可选:设置要同步的数据库(不设置则同步所有)
# binlog-do-db=myapp
# 可选:设置不同步的数据库
# binlog-ignore-db=mysql
# binlog-ignore-db=information_schema- 主库的docker-compose配置:
version: '3.8'
services:
mysql-master:
image: mysql:8.0
container_name: mysql-master
restart: always
environment:
MYSQL_ROOT_PASSWORD: rootpwd123
MYSQL_DATABASE: myapp
ports:
- "3306:3306"
volumes:
- master-data:/var/lib/mysql
- ./conf/master.cnf:/etc/mysql/conf.d/master.cnf
- ./logs/master:/var/log/mysql
networks:
- mysql-replication
volumes:
master-data:
networks:
mysql-replication:
driver: bridge- 启动主库:
docker-compose up -d mysql-master- 创建复制用户:
进入主库容器,创建一个专门用于复制的用户:
docker exec -it mysql-master mysql -uroot -prootpwd123执行以下SQL:
-- 创建复制用户
CREATE USER 'repl'@'%' IDENTIFIED WITH mysql_native_password BY 'replpwd123';
-- 授予复制权限
GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%';
-- 刷新权限
FLUSH PRIVILEGES;
-- 查看主库状态,记录File和Position
SHOW MASTER STATUS;SHOW MASTER STATUS 会输出类似这样的结果:
+------------------+----------+--------------+------------------+
| File | Position | Binlog_Do_DB | Binlog_Ignore_DB |
+------------------+----------+--------------+------------------+
| mysql-bin.000003 | 156 | | |
+------------------+----------+--------------+------------------+重点:把 File 和 Position 这两个值记下来,配置从库时要用。
Slave节点配置
从库配置相对简单一些。
- 创建从库配置文件
conf/slave.cnf:
[mysqld]
# 唯一服务器ID,必须和主库不同
server-id=2
# 中继日志
relay-log=relay-bin
# 从库只读(防止误操作写入)
read-only=1
# 字符集
character-set-server=utf8mb4
collation-server=utf8mb4_unicode_ci- 更新docker-compose.yml,添加从库配置:
version: '3.8'
services:
mysql-master:
image: mysql:8.0
container_name: mysql-master
restart: always
environment:
MYSQL_ROOT_PASSWORD: rootpwd123
MYSQL_DATABASE: myapp
ports:
- "3306:3306"
volumes:
- master-data:/var/lib/mysql
- ./conf/master.cnf:/etc/mysql/conf.d/master.cnf
- ./logs/master:/var/log/mysql
networks:
- mysql-replication
mysql-slave:
image: mysql:8.0
container_name: mysql-slave
restart: always
environment:
MYSQL_ROOT_PASSWORD: rootpwd123
ports:
- "3307:3306" # 注意:宿主机用3307访问从库
volumes:
- slave-data:/var/lib/mysql
- ./conf/slave.cnf:/etc/mysql/conf.d/slave.cnf
- ./logs/slave:/var/log/mysql
networks:
- mysql-replication
depends_on:
- mysql-master
volumes:
master-data:
slave-data:
networks:
mysql-replication:
driver: bridge- 启动从库:
docker-compose up -d mysql-slave- 配置从库连接主库:
进入从库容器:
docker exec -it mysql-slave mysql -uroot -prootpwd123执行以下SQL(注意替换File和Position为你刚才记录的值):
-- 配置主库连接信息
CHANGE MASTER TO
MASTER_HOST='mysql-master', -- 主库容器名(Docker网络内可直接用容器名)
MASTER_PORT=3306, -- 主库端口
MASTER_USER='repl', -- 复制用户
MASTER_PASSWORD='replpwd123', -- 复制用户密码
MASTER_LOG_FILE='mysql-bin.000003', -- 主库binlog文件(从SHOW MASTER STATUS获取)
MASTER_LOG_POS=156; -- 主库binlog位置(从SHOW MASTER STATUS获取)
-- 启动从库复制
START SLAVE;
-- 查看从库状态
SHOW SLAVE STATUS\G验证主从同步
SHOW SLAVE STATUS\G 会输出一大堆信息,重点关注这几个字段:
Slave_IO_Running: Yes # IO线程运行状态,必须是Yes
Slave_SQL_Running: Yes # SQL线程运行状态,必须是Yes
Seconds_Behind_Master: 0 # 从库延迟秒数,0表示实时同步
Last_IO_Error: # IO错误信息,为空表示正常
Last_SQL_Error: # SQL错误信息,为空表示正常如果 Slave_IO_Running 和 Slave_SQL_Running 都是 Yes,恭喜你,主从复制已经成功了!
测试一下是不是真的同步:
- 在主库创建测试数据:
docker exec -it mysql-master mysql -uroot -prootpwd123 -e "
USE myapp;
CREATE TABLE test_table (id INT PRIMARY KEY, name VARCHAR(50));
INSERT INTO test_table VALUES (1, 'test data');
"- 在从库查询:
docker exec -it mysql-slave mysql -uroot -prootpwd123 -e "
USE myapp;
SELECT * FROM test_table;
"如果能查到刚才插入的数据,说明主从同步正常工作了。
这种感觉,就像看着自己的代码在生产环境跑起来一样——爽!
常见问题排查
主从复制虽然不难,但第一次配置时还是容易遇到问题。我把常见的坑列一下:
问题1:Slave_IO_Running是No
可能原因:
- 网络不通:检查容器是否在同一网络,用
docker exec -it mysql-slave ping mysql-master测试 - 用户权限不对:检查主库的repl用户是否创建成功,权限是否正确
- binlog文件名或位置错误:重新执行
SHOW MASTER STATUS确认
问题2:Slave_SQL_Running是No
可能原因:
- SQL执行出错:查看
Last_SQL_Error字段,根据错误信息排查 - 主从数据不一致:如果主库在配置前就有数据,需要先导出主库数据导入从库,再配置主从
问题3:Seconds_Behind_Master一直很大
可能原因:
- 从库性能不足:检查从库CPU、内存、磁盘IO
- 主库写入太频繁:考虑增加从库数量分散压力
- 网络带宽不足:检查网络延迟
如果遇到问题解决不了,可以重置从库重新配置:
-- 在从库执行
STOP SLAVE;
RESET SLAVE;
-- 然后重新执行CHANGE MASTER TO和START SLAVE性能优化与最佳实践
Docker MySQL性能优化建议
很多人担心Docker会拖慢MySQL性能。说实话,早几年确实有这个问题,但2024年的数据显示,Docker对MySQL的性能影响已经降到5%以内了,I/O密集型场景下几乎感觉不到差异。
不过话说回来,该优化还是要优化。几个实用建议:
1. Volume类型选择
前面提到过named volume和bind mount。性能上已经没啥区别了,但有个小细节:
- named volume:Docker管理,自动选择最优存储驱动,开发环境推荐
- bind mount:直接映射宿主机目录,生产环境推荐(方便备份、监控)
# named volume方式
volumes:
- mysql-data:/var/lib/mysql
# bind mount方式
volumes:
- /data/mysql:/var/lib/mysql2. 网络模式选择
默认的bridge网络已经够用,但如果你对性能要求极致,可以试试host网络模式:
services:
mysql:
network_mode: "host" # 直接使用宿主机网络,性能更好注意:用host模式后,ports 映射就不需要了,容器直接监听宿主机的3306端口。
3. 资源限制配置
生产环境一定要限制资源,防止MySQL容器吃光服务器资源:
services:
mysql:
image: mysql:8.0
deploy:
resources:
limits:
cpus: '2' # 最多用2个CPU核心
memory: 2G # 最多用2G内存
reservations:
memory: 1G # 保证至少1G内存这个配置需要用 docker-compose --compatibility up 启动(或者用Docker Swarm模式)。
4. 日志管理
MySQL容器的日志如果不限制,时间长了会占满磁盘。加个日志限制:
services:
mysql:
logging:
driver: "json-file"
options:
max-size: "100m" # 单个日志文件最大100M
max-file: "3" # 最多保留3个日志文件生产环境最佳实践清单
这部分是我踩坑总结出来的,强烈建议照着做。
安全建议:
- 不要使用默认或简单密码
# ❌ 别这么干
MYSQL_ROOT_PASSWORD: 123456
# ✅ 用强密码,或者用Docker secrets
MYSQL_ROOT_PASSWORD: "Mx8#kL9$pQ2@vN4!"- 使用Docker secrets管理敏感信息
services:
mysql:
image: mysql:8.0
secrets:
- mysql_root_password
environment:
MYSQL_ROOT_PASSWORD_FILE: /run/secrets/mysql_root_password
secrets:
mysql_root_password:
file: ./secrets/mysql_root_password.txt- 限制容器网络访问
networks:
mysql-network:
driver: bridge
internal: true # 仅容器间通信,禁止外部访问如果需要外部访问,用专门的应用容器做代理,不要直接暴露MySQL容器。
运维建议:
- 定期备份数据目录
# 简单粗暴的备份脚本
#!/bin/bash
DATE=$(date +%Y%m%d_%H%M%S)
docker exec mysql-master mysqldump -uroot -prootpwd123 --all-databases > backup_$DATE.sql
# 或者直接备份数据目录
tar -czf mysql-data-backup_$DATE.tar.gz /data/mysql/生产环境建议每天自动备份,至少保留7天的备份。
- 配置healthcheck监控容器状态
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p$$MYSQL_ROOT_PASSWORD"]
interval: 10s
timeout: 5s
retries: 3
start_period: 30s容器挂了会自动重启,配合监控告警更完美。
- 日志volume单独挂载
volumes:
- ./logs:/var/log/mysql日志单独挂出来,出问题时方便排查,也不会占用数据volume的空间。
高可用建议:
- 至少一主一从,建议一主多从
读多写少的场景,一个主库配3-5个从库,读请求分散到从库。
- 配合负载均衡实现读写分离
可以用MySQL Router、ProxySQL、或者应用层实现:
- 写操作(INSERT/UPDATE/DELETE)→ 主库
- 读操作(SELECT)→ 从库(负载均衡)
- 定期测试主从切换流程
不要等主库真挂了才发现从库顶不上。定期演练主从切换:
-- 从库执行
STOP SLAVE;
RESET SLAVE ALL;
SET GLOBAL read_only=0; -- 取消只读,提升为主库生产环境建议用成熟的高可用方案(如MHA、Orchestrator)自动切换。
结论
说了这么多,咱们回顾一下这篇文章的核心内容。
从最基础的Docker MySQL单机部署,到数据持久化、配置文件挂载、外部连接问题解决,再到Docker Compose优雅管理,最后到生产级的主从复制配置——这一整套流程,基本覆盖了Docker MySQL部署的所有常见场景。
几个关键要点再强调一下:
- 数据持久化是必须的,别再用裸容器跑MySQL了,血的教训
- 配置文件挂载很重要,字符集、连接数这些参数得提前配好
- Docker Compose是趋势,配置集中管理,团队协作友好
- 主从复制并不难,关键是配置文件和步骤要对
- 生产环境要注意安全和备份,密码别用弱口令,数据定期备份
文章里的所有配置代码都是我验证过可以直接用的。你可以直接复制到你的项目里,改改密码和端口就能跑。
如果你是第一次用Docker部署MySQL,建议从单机部署开始,先把数据持久化和配置挂载搞明白,再去折腾主从复制。一步步来,别着急。
遇到问题了也别慌,大部分问题都能在日志里找到原因。搞不定的话,欢迎评论区交流,我看到会回复。
最后说一句,Docker部署MySQL真的比传统方式省心太多了。配置一次,到处运行,这种感觉——真香。
13 分钟阅读 · 发布于: 2025年12月18日 · 修改于: 2025年12月26日



评论
使用 GitHub 账号登录后即可评论