Docker 部署 MySQL 8.0 生产实践:数据持久化、主从复制与安全基线

很多站长第一次用 Docker 跑 MySQL,都是执行一条 docker run mysql:8.0 就完事了。容器能跑起来,和能稳定跑一年,是两码事。MySQL 容器最大的坑有三个:容器一删数据全没、配置文件没法改、端口直接暴露到公网。这篇文章把我自己踩过的坑和最终的生产配置完整记录下来,从数据持久化、配置管理、性能参数、主从复制到安全基线,一条条讲清楚。

一、数据持久化:容器是临时的,数据必须是永久的

Docker 容器的文件系统是分层且易失的,容器被删除后,写入容器内部的数据会随之一并消失。所以第一步就是要把 MySQL 的数据目录挂载到宿主机。MySQL 8.0 官方镜像的数据目录是 /var/lib/mysql,配置文件目录是 /etc/mysql/conf.d

挂载方式有两种:绑定挂载(bind mount)和命名卷(named volume)。绑定挂载把宿主机目录直接映射进容器,配置文件、备份文件看得见摸得着,适合我们这种单机场景;命名卷由 Docker 管理,路径更隐蔽,适合多机编排。个人站长建议用绑定挂载:

mkdir -p /data/mysql/{data,conf,logs,backup}
docker run -d --name mysql8   --restart=always   -p 127.0.0.1:3306:3306   -v /data/mysql/data:/var/lib/mysql   -v /data/mysql/conf:/etc/mysql/conf.d   -v /data/mysql/logs:/var/log/mysql   -e MYSQL_ROOT_PASSWORD='YourStrongPass2026'   -e TZ=Asia/Shanghai   mysql:8.0

注意 -p 127.0.0.1:3306:3306 这一行:只绑定回环地址,公网完全无法直连数据库端口。这是数据库容器最基本的一条安全基线,后面会展开讲。另外务必设置 --restart=always,否则服务器重启后 MySQL 不会自动拉起,网站直接挂掉。

还有一个容易忽略的点:数据目录挂载后,容器内 MySQL 进程是以 mysql 用户运行的,宿主机目录的所有者如果不匹配,MySQL 会因无法写入而启动失败。可以先用 chown -R 999:999 /data/mysql 修正(官方镜像中 mysql 用户的 UID 是 999),或者临时跑一次 docker exec mysql8 chown -R mysql:mysql /var/lib/mysql

二、配置管理:把 my.cnf 挂出来,改参数不用重建容器

把配置目录挂载出来之后,以后调参数只需要改宿主机文件再重启容器,不用重新 docker run。我生产环境用的最小可用配置如下,写入 /data/mysql/conf/my.cnf

[mysqld]
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
innodb_buffer_pool_size = 1G
innodb_log_file_size = 256M
slow_query_log = ON
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 2
max_connections = 200
binlog_format = ROW
server-id = 1

几个参数说明:utf8mb4 是中文站点的底线,emoji 和生僻字全靠它;innodb_buffer_pool_size 一般设置为物理内存的 50%~70%,这个参数直接决定 InnoDB 的读写性能;slow_query_log 必须开,这是后面排查慢查询的基础;server-idbinlog_format=ROW 是为主从复制提前埋的伏笔。

改完配置后,用 docker exec mysql8 mysqladmin -p ping 或直接看容器日志验证配置是否生效。注意 MySQL 8.0 对配置项的校验更严格,写错的参数会导致启动失败,此时 docker logs mysql8 会给出具体报错,比在裸机上排查还方便。

三、主从复制实战:基于 GTID 的一主一从

主从复制的作用不只是备份,更重要的是把读流量从主库分流出去。Docker 环境下部署主从,最干净的做法是建一个专用的 Docker 网络,让两个容器通过容器名互相访问,避免依赖宿主机 IP:

docker network create mysql-net
docker run -d --name mysql-master --network mysql-net   -v /data/mysql-master:/var/lib/mysql   -e MYSQL_ROOT_PASSWORD='MasterPass' mysql:8.0
docker run -d --name mysql-slave --network mysql-net   -v /data/mysql-slave:/var/lib/mysql   -e MYSQL_ROOT_PASSWORD='SlavePass' mysql:8.0

MySQL 8.0 默认开启 GTID,主从同步比 5.7 时代简单很多。在主库上创建复制账号并授权:

CREATE USER 'repl'@'%' IDENTIFIED BY 'ReplPass2026';
GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%';
FLUSH PRIVILEGES;

然后在从库上执行:

CHANGE REPLICATION SOURCE TO
  SOURCE_HOST='mysql-master',
  SOURCE_USER='repl',
  SOURCE_PASSWORD='ReplPass2026',
  SOURCE_AUTO_POSITION=1;
START REPLICA;

SHOW REPLICA STATUS\G 查看状态,重点看 Replica_IO_RunningReplica_SQL_Running 是否都是 Yes。注意 8.0 已经用 SOURCE/REPLICA 术语取代了旧的 MASTER/SLAVE,旧写法虽然兼容但会有弃用警告。容器重启后复制不会自动恢复,建议在从库容器的 entrypoint 里加上自动 START REPLICA 的逻辑,或者写一个 systemd 定时任务兜底检查。

四、备份与恢复:容器内的 mysqldump 与 binlog 时间点恢复

备份永远要在凌晨低峰期做,并且要定期做恢复演练——不能恢复的备份等于没有备份。容器环境下执行备份有两种方式:

# 方式一:docker exec 直接进容器执行
docker exec mysql8 mysqldump --single-transaction   --routines --triggers -uroot -p'YourStrongPass2026'   myblog > /data/mysql/backup/myblog_$(date +%F).sql

# 方式二:进入容器用 mysql 客户端恢复到新库
docker exec -i mysql8 mysql -uroot -p'YourStrongPass2026' myblog_restore < /data/mysql/backup/myblog_2026-08-01.sql

--single-transaction 参数保证 InnoDB 表在不锁表的情况下拿到一致性快照,对线上业务几乎零影响。恢复时注意先创建好目标数据库,再导入数据。如果只想恢复到某个时间点,需要配合 binlog:先用全量备份恢复,再 mysqlbinlog --start-datetime 把之后的事务补进去,这是误删数据后的最后一道救命稻草。

五、安全基线:不暴露端口、最小权限、定期更新镜像

最后把安全基线完整列一遍,都是低成本高收益的动作:

第一,端口绝不直接暴露公网。像前面那样只绑定 127.0.0.1,应用通过宿主机访问,或者干脆把应用也容器化放进同一个 Docker 网络,连端口映射都省了。第二,root 账号只允许本机登录,业务账号单独创建并按库授权,GRANT SELECT,INSERT,UPDATE,DELETE ON myblog.* TO 'app'@'%' 就够了,千万别图省事全部用 root。第三,定期 docker pull mysql:8.0 重建容器,跟进官方安全补丁,MySQL 的 CVE 几乎每个版本都有,长期不更新等于把漏洞摆在门口。第四,日志和备份文件注意权限,chmod 600,防止其他用户读到数据库密码和业务数据。

六、健康检查与资源限制:别让容器变成失控进程

容器化的另一个好处,是终于可以给数据库加上资源上限。一台服务器上同时跑着 Nginx、PHP、Redis 的时候,如果不限制 MySQL 的内存占用,它在高峰期可能把整台机器的内存吃光,触发内核 OOM Killer 把别的进程杀掉,网站表现就是「莫名其妙挂了」。用 docker run 参数直接限制资源并加上健康检查:

docker run -d --name mysql8   --memory=2g --memory-swap=2g   --cpus=2   --health-cmd="mysqladmin ping -h127.0.0.1 -uroot -p'YourStrongPass2026' --silent"   --health-interval=30s --health-timeout=5s --health-retries=3   mysql:8.0

# 查看健康状态
docker inspect --format='{{.State.Health.Status}}' mysql8

--memory-swap 设为和内存一样大,等于彻底禁用 swap,宁可让 MySQL 启动失败也不能让它把服务器拖进交换分区死循环。健康检查会定期执行 mysqladmin ping,配合 docker ps 里的 STATUS 一栏,容器是否存活一目了然,还能被监控系统直接采集。如果你用 docker-compose 管理,同样的配置写进 compose 文件的 deploy.resources.limitshealthcheck 段即可,逻辑完全一致。

资源限制的数值怎么定?先观察再限制:用 docker stats mysql8 看一周的峰值内存,在峰值基础上加 30% 余量作为上限。限制太紧会导致数据库在高负载时直接 OOM 退出,反而比不限制更糟,所以这个值要留足余量。

七、常见故障排查清单

最后把容器化 MySQL 最常见的几个故障和排查方法列出来,遇到问题照着查就行:

问题一:容器启动后立即退出。docker logs mysql8 看日志,八成是数据目录权限不对(宿主机目录所有者不是 UID 999),或者 my.cnf 写入了不支持的参数。用 docker rm 清掉容器、修正后重新创建即可,数据卷里的数据不会丢。

问题二:应用连不上数据库。先在宿主机上 docker exec mysql8 mysql -uroot -p 确认容器内能连,再确认端口映射:ss -tlnp | grep 3306 应该看到 127.0.0.1:3306。如果应用在另一个容器里,确认它和 MySQL 在同一个 Docker 网络,并且连接串用的是容器名而不是 localhost。

问题三:容器重启后数据还在,但网站报错。多半是 MySQL 没有干净关闭,binlog 与数据文件不一致。先看日志里有没有 crash recovery 的提示,等它自动恢复完成再操作;如果一直起不来,用备份文件恢复,这也是为什么前面强调备份必须定期演练。

问题四:时区不对,时间字段差 8 小时。创建容器时加 -e TZ=Asia/Shanghai 只解决了系统时区,连接串里还要加 serverTimezone=Asia/Shanghai(Java 驱动)或建表时统一用 DATETIME 存 UTC、展示层转换,避免各环节时区口径不一致。

MySQL 容器化的核心思路其实就一句话:把容器当成一个可随时替换的执行环境,把数据、配置、日志全部外置到宿主机,再配合主从复制和定时备份,就能既享受 Docker 带来的部署便利,又不丢失裸机时代的数据安全感。这套方案我跑了两年,经历过三次服务器迁移和一次磁盘故障,数据零丢失,希望对你也有用。

Last modification:August 24th, 2026 at 08:12 am

Leave a Comment