很多站长第一次用 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-id 和 binlog_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_Running 和 Replica_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.limits 和 healthcheck 段即可,逻辑完全一致。
资源限制的数值怎么定?先观察再限制:用 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 带来的部署便利,又不丢失裸机时代的数据安全感。这套方案我跑了两年,经历过三次服务器迁移和一次磁盘故障,数据零丢失,希望对你也有用。