MySQL 容器化的坑集中在"初始化只做一次"这件事上:密码、库、字符集、大小写敏感,全是在数据目录为空的那一次初始化里定下来的,后面改环境变量、改配置文件都不会回头补。搞清这一点,剩下的就是挂卷、权限和备份。
一、镜像和目录
docker pull mysql:8.0
sudo mkdir -p /data/mysql/{data,conf,init}
| 宿主机目录 | 容器内路径 | 用途 |
|---|---|---|
/data/mysql/data |
/var/lib/mysql |
数据文件 |
/data/mysql/conf |
/etc/mysql/conf.d |
自定义配置 |
/data/mysql/init |
/docker-entrypoint-initdb.d |
首次初始化执行的 SQL |
/data/mysql/data 要给容器里的 mysql 用户(uid 999)权限:
sudo chown -R 999:999 /data/mysql/data
如果数据目录挂的是新格式化的独立分区,注意分区根目录里会有 lost+found。MySQL 初始化时要求目录为空,看到 lost+found 就认为"目录里有文件",直接放弃初始化。这种情况用分区下的子目录,比如 /data/mysql/data,而不是直接把分区挂到 /var/lib/mysql。
二、my.cnf
/data/mysql/conf/my.cnf:
[mysqld]
character-set-server = utf8mb4
collation-server = utf8mb4_0900_ai_ci
lower_case_table_names = 1
default-time-zone = '+08:00'
max_connections = 500
innodb_buffer_pool_size = 1G
slow_query_log = 1
long_query_time = 1
几个参数说明:
| 参数 | 说明 |
|---|---|
character-set-server |
默认字符集,8.0 默认就是 utf8mb4,写出来是为了明确 |
collation-server |
8.0 的默认排序规则,从 5.7 迁过来的库要留意差异 |
lower_case_table_names |
表名大小写是否敏感,只在初始化时生效 |
default-time-zone |
容器时区是 UTC,业务按东八区存时间必须设 |
innodb_buffer_pool_size |
通常给物理内存的 50% 到 70%,容器内存限制下别超过上限 |
配置文件权限有要求:MySQL 会忽略全局可写的配置文件。挂进去之后再确认一次:
sudo chmod 644 /data/mysql/conf/my.cnf
日志里出现 World-writable config file '/etc/mysql/conf.d/my.cnf' is ignored,就是权限太松,配置压根没被读。
三、compose
services:
mysql:
image: mysql:8.0
container_name: mysql
restart: always
ports:
- "3306:3306"
environment:
TZ: Asia/Shanghai
MYSQL_ROOT_PASSWORD: 换成强密码
MYSQL_DATABASE: app
MYSQL_USER: app
MYSQL_PASSWORD: 换成强密码
volumes:
- /data/mysql/data:/var/lib/mysql
- /data/mysql/conf:/etc/mysql/conf.d:ro
- /data/mysql/init:/docker-entrypoint-initdb.d:ro
healthcheck:
test: ["CMD-SHELL", "mysql -uroot -p\"$$MYSQL_ROOT_PASSWORD\" -e 'SELECT 1' >/dev/null 2>&1"]
interval: 30s
timeout: 5s
retries: 5
start_period: 60s
四个环境变量的实际行为:
| 变量 | 生效条件 |
|---|---|
MYSQL_ROOT_PASSWORD |
只能二选一:不设它就得设 MYSQL_RANDOM_ROOT_PASSWORD,否则容器拒绝启动 |
MYSQL_DATABASE |
仅在数据目录为空时建库 |
MYSQL_USER / MYSQL_PASSWORD |
仅在初始化时建用户并授权到 MYSQL_DATABASE |
| 四个变量整体 | 数据目录非空时全部忽略,改了不起任何作用 |
healthcheck 这里用的是 mysql -e 'SELECT 1',不是 mysqladmin ping。mysqladmin ping 只要服务端能响应就返回成功,密码错了也照样"通",检测不出认证问题。
start_period 给 60 秒是有必要的,首次初始化要建系统表,磁盘慢的机器上跑一两分钟很正常,这段时间里的失败不该算进 retries。
四、初始化脚本
/data/mysql/init/ 下的 .sql、.sh 文件只在数据目录为空时按字母顺序执行一次。常见的用法是建库、建只读账号:
CREATE DATABASE IF NOT EXISTS app DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
CREATE USER IF NOT EXISTS 'readonly'@'%' IDENTIFIED BY '只读密码';
GRANT SELECT ON app.* TO 'readonly'@'%';
FLUSH PRIVILEGES;
放进去之后想让它再跑一遍,只能删数据目录重建,没有别的办法。所以初始化脚本要么一次写全,要么后面手工执行 SQL,不要指望"补一个文件进去重启就好"。
五、验证
docker ps --filter name=mysql
docker exec mysql mysql -uroot -p'密码' -e "SELECT VERSION(), @@character_set_server, @@lower_case_table_names, @@time_zone;"
docker exec mysql mysql -uroot -p'密码' -e "SHOW DATABASES;"
四个值对上了,说明配置文件和初始化参数都生效:
8.0.x utf8mb4 1 +08:00
再从外部连一次,确认端口映射和账号授权:
mysql -h 127.0.0.1 -P 3306 -uapp -p'密码' app
六、备份和恢复
逻辑备份,适合中小库和跨版本迁移:
docker exec mysql sh -c 'exec mysqldump -uroot -p"$MYSQL_ROOT_PASSWORD" \
--single-transaction --routines --triggers --events --set-gtid-purged=OFF app' \
> /backup/app-$(date +%F).sql
--single-transaction 让 InnoDB 在一致性快照里导出,不锁表;--set-gtid-purged=OFF 避免恢复时报 GTID 冲突。
恢复:
docker exec -i mysql sh -c 'exec mysql -uroot -p"$MYSQL_ROOT_PASSWORD" app' < /backup/app-2026-10-29.sql
物理备份可以直接复制 /data/mysql/data,但必须在停库之后:
docker compose stop mysql
sudo tar -czf /backup/mysql-data-$(date +%F).tar.gz -C /data/mysql data
docker compose start mysql
热复制数据目录得到的文件不可用,InnoDB 的 redo 和双写缓冲在运行中不一致,恢复时会报错。
七、常见报错
| 现象 | 原因 | 处理 |
|---|---|---|
Access denied for user 'root'@'localhost' |
数据目录已存在,初始化变量被忽略,密码还是旧的 | 用旧密码进,或 --skip-grant-tables 重置 |
[ERROR] --initialize specified but the data directory has files in it |
挂载目录非空,常见是 lost+found |
换个空子目录 |
容器反复重启,日志 Different lower_case_table_names settings for server ('1') and data dictionary ('0') |
初始化后改了 lower_case_table_names |
这个值初始化后不能改,只能重建库 |
Authentication plugin 'caching_sha2_password' cannot be loaded |
老客户端不认 8.0 的默认认证插件 | 升级驱动,或 ALTER USER ... IDENTIFIED WITH mysql_native_password BY ... |
World-writable config file ... is ignored |
配置文件权限太松 | chmod 644 |
mysqld: Can't read dir of '/etc/mysql/conf.d/' (Errcode: 13) |
目录权限或 SELinux | 修权限,CentOS 上挂载加 :z |
| 容器被 OOMKilled | innodb_buffer_pool_size 超过容器内存上限 |
调小 buffer pool,或提高内存限制 |
启动日志 Permission denied 指向 /var/lib/mysql |
数据目录属主不是 999 | chown -R 999:999 |
Can't connect to local MySQL server through socket |
服务没起来,还是初始化中 | docker logs mysql 看最后 50 行 |
八、8.4 和迁移
如果准备用 8.4,先确认客户端兼容性:mysql_native_password 在 8.4 里默认不加载,老版本驱动和工具连上来会失败。新项目直接用 8.4 没问题,存量系统升级前要在测试环境把驱动、中间件、备份脚本都过一遍。
跨大版本迁移(5.7 → 8.0)不要靠复制数据目录,走 mysqldump 导出再导入,或者用官方的 mysqlsh 升级检查工具,先把不兼容项列出来。
最后一条限制:容器里的 MySQL 性能和裸机没有本质区别,瓶颈仍然是磁盘 IO 和内存。把数据目录放在网络存储(NFS、CIFS)上跑 MySQL 属于高风险操作,遇到 fsync 语义不一致会直接损坏数据,这一点和容器无关,但容器化之后更容易被顺手挂上去。