ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Docker部署MySQL完整指南:从镜像选型到数据持久化与问题排查

Docker部署MySQL完整指南:从镜像选型到数据持久化与问题排查

很多朋友第一次接触容器化部署,都会从数据库开始,MySQL自然成了首选试验田。Docker安装MySQL最大的好处是省心:不用在系统里手动装一堆依赖,不用担心把宿主机环境搞乱,遇到问题删掉容器重建即可。但省心不等于没门槛,实际部署时版本怎么选、数据怎么持久化、字符集怎么配、密码和权限如何初始化,这些坑我都替你踩过一遍。

这篇文章就把我多次在云服务器和本地虚拟机里用Docker部署MySQL的完整经验整理出来,按照“版本选型 → 关键配置 → 实操流程 → 问题排查”的顺序展开,既有可以直接照抄的命令,也有底层原理解释。特别适合刚接触Docker、想在容器里跑数据库的新手,也适合部署过但被各种诡异问题折磨过的老手对照排雷。

1. 镜像选择与版本博弈:Docker部署MySQL前先想清楚的事

很多人觉得Docker部署就是pull一个镜像然后run,哪有什么可选的。真不是这样。镜像选错,后面全盘难受,尤其是MySQL这种对版本差异极其敏感的数据库。

1.1 官方镜像 vs 第三方镜像:选哪来的镜像更靠谱

Docker Hub上有大量MySQL相关镜像,但真正值得用的只有两类:一类是Docker官方维护的mysql镜像,另一类是MySQL厂商官方发布的镜像。这两者本质上是同一个来源,都是MySQL团队发布的,覆盖了5.6、5.7、8.0、8.4等主流大版本。

第三方镜像里确实有一些做了优化,比如体积更小、预装插件,但我不建议你用。原因很简单:你不知道它内部改了哪些配置,不知道它和官方默认行为有多少差异。数据库是最不能有“惊喜”的组件,官方镜像行为可预测、文档齐全、安全和补丁跟进最快,出了问题全世界都在用同一套方案,排查资料也最多。

我试过用某个号称“Alpine版MySQL、体积只有几十兆”的第三方镜像跑测试环境,结果它的用户权限初始化逻辑和官方镜像完全不同,配置SSL证书时发现缺少必要的组件,最后还是老老实实换回了官方镜像。结论就是:生产环境和测试环境,一律用官方镜像,省下来的那点空间远不够填坑的时间成本。

1.2 版本选型:5.7、8.0还是8.4,别让老代码卡住你的选择

先来看一下目前Docker Hub上官方mysql镜像主推的版本情况:

版本状态默认认证插件默认字符集适用场景
MySQL 5.7已停止官方支持mysql_native_passwordlatin1遗留老项目、旧代码兼容、不愿升级的存量系统
MySQL 8.0主流普及版caching_sha2_passwordutf8mb4绝大多数新项目首选,功能全、社区资料丰富
MySQL 8.4LTS长期支持版caching_sha2_passwordutf8mb4追求长期稳定支持的新部署

选哪个版本,主要看你现有代码的兼容性,而不是哪一个“最新用哪个”。MySQL 8.0刚出来时被诟病过性能回退、认证插件不兼容,但实际上到8.0.2x之后已经非常稳了。如果你是新项目、没有历史包袱,直接上8.0或8.4没问题。但如果你手上有老系统、老业务在跑,尤其是还在用5.6、5.7的库,千万别在自己电脑上装个8.0做实验没问题,结果到了生产环境代码里某个旧驱动根本连不上——认证插件都变了,报错能把你折磨疯。

这里还要注意一个细节:MySQL 8.0默认的认证插件是caching_sha2_password,而很多老版本的PHP驱动、Java驱动、Python库用的是mysql_native_password。如果你遇到“客户端连接时提示Authentication plugin 'caching_sha2_password' cannot be loaded”,要么升级客户端驱动,要么在创建用户时显式指定老插件。具体操作在后面的账号权限部分我会详细说。

1.3 拉取镜像时的实操注意事项

确定版本后,拉取镜像建议指定具体小版本标签,不要用latest。latest看着方便,但实际使用中很容易踩到“昨天还能启动的容器今天重新拉取镜像后行为变了”的坑。比如官方在某一个小版本里调整了默认策略或修复了安全漏洞,你的容器配置可能就不适用了。

我常用的拉取命令是这样的:

docker pull mysql:8.0.36

如果你只是想快速试验,可以用大版本标签,比如mysql:8.0,但生产环境我强烈建议锁定小版本号。这样你重建容器时,拉下来的一定是同一版本的镜像,行为完全一致,不会出现“测试环境好好的,生产环境一拉镜像就开始报奇怪的错误”这种问题。

2. 部署前的四个关键决策:端口、数据卷、字符集和时区

很多教程上来就是一条docker run命令直接贴给你。我建议先停下来,想清楚四个关键决策。因为这四个决策每一个都影响后续运维体验,命令敲完之后再改,麻烦得多。

2.1 端口映射:你的宿主机3306真的空着吗

MySQL默认监听3306端口,Docker容器里也是。想让宿主机的程序访问容器里的数据库,就必须把容器端口映射到宿主机端口,命令里对应的是-p参数。

这里有一个非常容易踩的坑:如果你的宿主机上已经装了MySQL或者其他占用3306端口的服务,那么容器启动时会直接报端口冲突。解决方式有两种,一种是停掉宿主机原有服务,另一种是把容器端口映射到宿主机的其他端口,比如3307:

docker run -d -p 3307:3306 -e MYSQL_ROOT_PASSWORD=your_password mysql:8.0

把宿主机的3307映射到容器的3306,外部访问时就用127.0.0.1:3307连接。我自己的习惯是:宿主机安装的程序要访问数据库,就用一个非常见端口做映射,比如53306、6306之类,这样既避开冲突,偶尔还能少一些扫描软件撞到3306端口的干扰。当然如果你只有一个数据库服务,直接用3306也完全没问题。

2.2 数据卷持久化:容器可以删,数据绝不丢

容器的文件系统是临时的,容器一旦删除,里面的数据全部消失。MySQL作为数据库,如果数据随容器一起没了,那基本是灾难。所以必须把MySQL的数据目录挂载到宿主机的某个目录上,让数据真正存在宿主机磁盘里,容器只负责运行程序。

这个点在官方文档里其实说得很清楚,mysql镜像默认的数据目录是/var/lib/mysql,可以使用-v参数把它挂载到宿主机任意目录:

-v /opt/mysql_data:/var/lib/mysql

我见过不止一个人贪图方便,创建容器时不加这个挂载参数,跑了几个星期的测试数据,后来升级容器版本时docker rm直接把容器删掉,数据全部归零,欲哭无泪。这是一个绝对不可省略的参数。

另外还要强调:存放数据文件的宿主机目录,建议放在一个独立的、容量充足的磁盘上,而且要定期做物理备份。别把数据目录放在Docker默认的overlayfs存储里,那样即使容器没删,出现存储层损坏时恢复难度也大得多。

2.3 字符集初始化:为什么2024年你还在为乱码烦恼

MySQL 5.7和早期8.0版本的一个通病是默认字符集用了latin1,也就是不支持中文。你在表里插入的中文,要么变成问号,要么变成乱码。解决方式是在容器创建时指定字符集相关参数。

推荐的做法是给容器增加如下参数:

--character-set-server=utf8mb4 --collation-server=utf8mb4_0900_ai_ci

注意8.0默认的排序规则是utf8mb4_0900_ai_ci,5.7里没有这个规则,5.7一般用utf8mb4_general_ci。所以如果你用的是5.7,要写utf8mb4_general_ci;用8.0或8.4,可以直接写utf8mb4_0900_ai_ci。除此之外,建立连接时的字符集也很重要,很多客户端工具连接时默认用了latin1,实际传输中文就出乱码。我建议连接参数里都加上characterEncoding=utf8(Java)或charset=utf8mb4(连接串里常见写法),从源头杜绝问题。

还有一个比较容易忽视的点:MySQL 8.0在初始化数据目录时,如果你没有显式指定字符集参数,它会把默认字符集固化到初始化的系统表里,之后再改配置也不会影响已经建好的系统表的字符集。虽然可以通过ALTER修改库表字符集,但最省心的方式就是初始化时一次性配好。

2.4 时区处理:八小时时差也可能是Docker的锅

很多程序在访问数据库时会发现,明明服务器时间是对的,查出来的时间戳总是差8小时。这往往不是因为MySQL有问题,而是因为容器默认使用UTC时区,没有跟着宿主机的时区走。

官方mysql镜像的时区默认是UTC,解决办法是创建容器时注入时区环境变量:

-e TZ=Asia/Shanghai

同时,MySQL内部的时间函数和连接时区也建议一并配置。可以给容器加启动参数:

--default-time-zone=+8:00

或者进入容器后在MySQL里执行:

SET GLOBAL time_zone = '+8:00';

前者是一劳永逸的配置方法。我自己测过,只加TZ环境变量,操作系统的date命令正确了,但MySQL的SYSTEM时区可能仍显示UTC。所以稳妥起见,把TZ和default-time-zone两个都配上,省得程序里处理时间差。

3. 完整实操记录:两条路线把MySQL跑起来

决策做完了,下面进入正题。我给出两种部署方式,一种是用docker run命令直接创建,另一种是用docker compose定义好配置。两种方案各有适用场景,你可以根据习惯选。这里的步骤是我在Ubuntu和CentOS类系统上都跑过验证的,按顺序执行即可。

3.1 宿主机准备:目录结构和基础环境检查

先做几个基本功。确认Docker已经安装并正常启动:

docker version docker ps

如果docker ps能正常返回一个空列表,说明服务没问题。

接着创建数据目录和配置目录。我习惯把MySQL相关的数据、配置、日志分开目录管理:

mkdir -p /opt/mysql/{data,conf,logs}

这个目录结构的好处是后续备份数据时只需要打包data目录,配置调整只动conf目录,日志轮转单独处理,不用在容器里翻来翻去。

如果你的服务器有SELinux(CentOS/RHEL系默认开启),还需要确认SELinux不会阻止容器写宿主机目录。最简单的方式是临时用chcon给目录设置container_file_t类型:

chcon -Rt container_file_t /opt/mysql

不处理的话,容器启动后MySQL明明写权限正常,却一直提示“Permission denied”,排查方向容易被带偏。

3.2 路线一:docker run一条龙创建MySQL 8.0容器

这是我个人最常用的方式,每一步都能看清楚。完整命令如下:

docker run -d \ --name mysql8 \ --restart=always \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=YourStrongPass123 \ -e MYSQL_DATABASE=appdb \ -e MYSQL_USER=appuser \ -e MYSQL_PASSWORD=AppUserPass456 \ -e TZ=Asia/Shanghai \ -v /opt/mysql/data:/var/lib/mysql \ -v /opt/mysql/conf:/etc/mysql/conf.d \ -v /opt/mysql/logs:/var/log/mysql \ mysql:8.0

逐条解释一下关键参数:

  • -d:后台运行。
  • --name mysql8:给容器命名,后续操作直接用这个名字。
  • --restart=always:容器崩溃或服务器重启时自动重新拉起。生产环境强烈建议加,否则服务器一重启你的应用连不上数据库,还得手动docker start。
  • -p 3306:3306:端口映射。
  • -e MYSQL_ROOT_PASSWORD:设置root用户的初始密码。
  • -e MYSQL_DATABASE:自动创建一个初始数据库。
  • -e MYSQL_USER和MYSQL_PASSWORD:自动创建一个普通用户并赋予这个数据库的全部权限。注意这个用户只在MYSQL_DATABASE对应的库上有权限,不是全局权限。
  • -e TZ=Asia/Shanghai:设置容器时区。
  • -v 挂载目录:分别挂载数据目录、配置目录、日志目录。

执行完等待大概5到15秒,MySQL首次初始化数据目录需要时间。可以执行以下命令确认容器状态:

docker ps docker logs mysql8

如果是“ready for connections”结尾,说明初始化完成。如果日志里出现错误,别慌,后面第四节专门讲排查方法。

3.3 路线二:docker compose配置化管理

如果你有多个服务要一起管理,或者希望配置“写一次到处复用”,那用docker compose更合适。在宿主机任意目录创建一个docker-compose.yml:

version: '3.8' services: mysql: image: mysql:8.0 container_name: mysql8 restart: always ports: - "3306:3306" environment: MYSQL_ROOT_PASSWORD: YourStrongPass123 MYSQL_DATABASE: appdb MYSQL_USER: appuser MYSQL_PASSWORD: AppUserPass456 TZ: Asia/Shanghai command: - --character-set-server=utf8mb4 - --collation-server=utf8mb4_0900_ai_ci - --default-time-zone=+8:00 volumes: - /opt/mysql/data:/var/lib/mysql - /opt/mysql/conf:/etc/mysql/conf.d - /opt/mysql/logs:/var/log/mysql

注意,docker compose方式里如果要在容器启动时指定MySQL的额外启动参数,用command数组的方式。这里我不建议直接改mysql.cnf主配置文件,因为官方镜像升级时可能会覆盖它。推荐的自定义方式是在conf目录下新建一个my.cnf文件,官方镜像会自动加载/etc/mysql/conf.d下的配置片段。

在docker-compose.yml所在目录执行启动:

docker compose up -d

停止和启动用docker compose stop和docker compose start,查看日志用docker compose logs -f mysql。相比docker run一大堆参数,compose方式的管理体验确实顺滑很多。

3.4 连接与验证:几个命令确认MySQL真的可用

容器创建完了,得验证一下。先进入容器内部,用MySQL客户端连接:

docker exec -it mysql8 mysql -uroot -p

输入之前设置的root密码,进入MySQL命令行后执行:

SELECT VERSION(); SHOW VARIABLES LIKE 'character_set_server'; SHOW VARIABLES LIKE 'time_zone';

确认版本为8.0.x,字符集是utf8mb4,时区可能是SYSTEM(取决于配置方式)。再创建一个测试表和插入数据验证中文:

USE appdb; CREATE TABLE demo (id INT PRIMARY KEY AUTO_INCREMENT, content VARCHAR(100)) DEFAULT CHARSET=utf8mb4; INSERT INTO demo (content) VALUES ('你好,Docker'); SELECT * FROM demo;

能正常插入和查询中文,说明字符集链路是通的。接着在宿主机上验证外部连接能力。如果宿主机安装了mysql客户端,直接:

mysql -h 127.0.0.1 -P 3306 -u appuser -p appdb

如果宿主机没有客户端,可以用容器里的客户端测试:

docker exec -it mysql8 mysql -h 127.0.0.1 -P 3306 -u appuser -p appdb

能连接成功,说明账号、端口映射都没问题。

4. 常见问题与排查技巧实录:从头疼到豁然开朗的瞬间

这一部分是我最想写的,因为每一条都是实际踩过坑后沉淀下来的经验。遇到问题别急着删容器重来,先按下面的清单排查,大部分问题一两分钟就能定位。

4.1 容器启动失败:先看日志再动手

我见过最多的一个情形是docker run执行完,docker ps发现容器已经退出了。这时候第一件事是看日志:

docker logs mysql8

常见的错误有三种:

第一种是数据目录权限问题。如果你挂载的宿主机目录权限不对,容器内MySQL进程无法写入,日志里会有“Can't create/write to file '/var/lib/mysql/'”类似的提示。解决办法很简单,宿主机上执行:

chown -R 999:999 /opt/mysql/data

MySQL官方镜像里的mysql用户UID是999,不是0也不是1000,这点最容易忽略。网上很多教程说chmod 777,确实能解决,但这相当于让所有人都有权限修改数据文件,安全上太炸了。正确做法是chown给999。

第二种是初始化失败,日志里出现“Initializing database failed”或“[ERROR] --initialize specified but the data directory has files in it”。这通常是数据目录之前已经初始化过,出现了残留数据。如果你确定是全新环境,把data目录下的内容清空再重建容器:

rm -rf /opt/mysql/data/* docker start mysql8

第三种是端口被占用,日志显示“Bind for 0.0.0.0:3306 failed: port is already in use”。这时候要么换宿主机映射端口,要么清理占用端口的进程。

4.2 外部连接失败:账号、防火墙、认证插件逐个排除

容器起来了,日志也显示ready for connections,但外部程序就是连不上。这类问题排查有一个固定的顺序:

第一,确认容器在运行且端口映射正确:

docker ps --format "table {{.Names}}\t{{.Ports}}"

第二,确认宿主机防火墙没有拦截3306端口。Ubuntu的ufw、CentOS的firewalld都有可能默认拦截。临时放行:

ufw allow 3306/tcp # 或 firewall-cmd --add-port=3306/tcp --permanent && firewall-cmd --reload

第三,确认MySQL用户权限允许远程登录。8.0默认创建的root用户一般只允许localhost登录(如果你用MYSQL_ROOT_PASSWORD初始化的root用户,实际上授权host通常是%,但你自己通过SQL创建的用户就要注意)。给用户授权时,host要写成%而不是localhost:

CREATE USER 'appuser'@'%' IDENTIFIED BY 'pass'; GRANT ALL PRIVILEGES ON appdb.* TO 'appuser'@'%'; FLUSH PRIVILEGES;

第四,检查认证插件兼容性。如果你用很老版本的客户端工具,连接时会报caching_sha2_password错误。把用户改成老的认证方式:

ALTER USER 'appuser'@'%' IDENTIFIED WITH mysql_native_password BY 'pass';

不过我还是建议优先升级客户端和驱动,mysql_native_password在8.0里属于计划废弃的插件,升级是迟早的事。

4.3 数据目录权限与Docker存储驱动的恩怨

有时候容器能正常启动,但数据库的读写性能奇差无比,或者备份数据时发现文件属性异常。这类问题往往和Docker存储驱动有关系。

用docker run直接创建的容器,如果不加volume挂载,数据其实写在Docker的overlay2层里,容器的写操作会有额外的性能损耗,而且随着镜像层数的增加,写放大问题会更明显。这也是我为什么从一开始就强调挂载数据卷。不过光是挂载还不够,宿主机的文件系统格式也有影响,ext4和xfs是主流选择,千万不要在vfat或NTFS挂载目录上放数据库文件,根本跑不动。

另一个经验是:不要对数据目录直接用chmod 777来“省事”,后面数据文件越来越多、目录层级越来越深,权限问题会以各种形式冒出来。像我上面说的,chown -R 999:999 /opt/mysql/data是最正确的修法。

4.4 MySQL 8.0连接慢:DNS解析与skip-name-resolve

这个坑很多人遇到但想不到原因:应用连接MySQL时每次要等好几秒才能建立连接,但连接建立后一切正常。排查发现MySQL默认会尝试反向解析客户端IP的主机名,如果DNS解析超时就会拖慢连接速度。

解决办法是在容器启动参数里加上:

--skip-name-resolve

官方镜像支持在command里追加这个参数。加上之后,MySQL不再解析客户端主机名,直接用IP匹配权限记录。注意,加了skip-name-resolve之后,你的用户授权里如果写了'user'@'localhost',就可能会匹配不上,因为localhost也需要解析。所以生产环境建议把所有用户的host都明确写成IP或者%。

我在实践中也碰到另一种情况:明明加了skip-name-resolve,连接还是慢。结果发现是Docker的网络模式问题,默认bridge模式下容器和宿主机之间的网络层也可能出现延迟。如果是本地应用连接刚部署好的数据库,网络延迟基本不可能,所以还是优先排查DNS解析这条路。

4.5 容器重启数据就丢:Volume挂载验证方法

有朋友吐槽过,明明加了-v参数,容器删了重建数据还是丢失。我复盘后发现,很多人把-v的路径写错了,或者挂载的是容器内的其他路径而非/var/lib/mysql。

验证挂载是否生效,最直接的方式是在宿主机上查看数据目录里是否有真实数据:

ls -l /opt/mysql/data

正常应该能看到mysql、performance_schema、sys等目录,还有一堆.ibd文件。如果宿主目录是空的,而容器里MySQL可以正常建表,那说明你的-v参数没有正确指向/var/lib/mysql。

另外有一种情况我也遇到过:Docker会创建匿名数据卷。当你只写了-v /opt/mysql/data:/var/lib/mysql,但/opt/mysql/data目录不存在时,Docker会自动创建这个目录,并把容器初始化的数据复制进去,这本身没问题。但是如果你的原始命令里写的是-v mysql_data:/var/lib/mysql这种“具名卷”,那数据实际存储在/var/lib/docker/volumes/mysql_data下面,而不是你想象的/opt/mysql/data。两种方式各有好处,具名卷更方便docker volume管理,但可读性不如宿主机目录直挂。新手我会建议全部用宿主机目录,出了问题直接用find查找文件时不至于迷路。

4.6 容器内执行备份:mysqldump的正确姿势

最后分享一个备份技巧。很多人以为备份数据库要在宿主机装一个mysqldump,其实完全不用。MySQL官方镜像自带mysqldump,直接用docker exec执行即可:

docker exec mysql8 sh -c 'exec mysqldump -uroot -p"$MYSQL_ROOT_PASSWORD" --all-databases' > backup_$(date +%F).sql

注意这里用了sh -c包一层,才能把容器内的环境变量$MYSQL_ROOT_PASSWORD传进去。如果直接docker exec mysql8 mysqldump -uroot -p$MYSQL_ROOT_PASSWORD,$MYSQL_ROOT_PASSWORD会被宿主机shell解析,基本上会解析成空字符串,导致认证失败。

要恢复备份,用:

docker exec -i mysql8 sh -c 'exec mysql -uroot -p"$MYSQL_ROOT_PASSWORD"' < backup_$(date +%F).sql

定期备份可以配合系统定时任务,比如每天凌晨执行一次,保留最近15天的备份文件。这个组合方案我用到现在,还从没发生过备份文件发霉的情况。

我个人在实际操作中体会最深的一点是:Docker部署MySQL本身不是难事,难的是你愿不愿意在敲第一条命令之前,把目录规划、字符集、时区、权限这些“慢功夫”想明白。跑起来是几秒钟的事,但运行稳定是长期工程。如果这篇文章里的命令和思路能让你少走两步弯路,那这篇文就没白写。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进