1. 开始动手前,先想清楚:容器不是虚拟机
有人一上来就在 Linux 服务器上执行docker run mysql:8.0,跑起来之后发现一切正常,然后第二天满怀期待地去查数据,结果容器被删了,数据也没了。这种情况我见过太多次。Docker 和虚拟机最大的区别就是:容器本身是“一次性”的,镜像只是静态模板,容器只是在模板上运行的一层读写环境。容器删了,这一层里写的东西也跟着没了,除非你用数据卷把内容留在宿主机上。
另外一个容易绕晕的点是“端口映射”。MySQL 默认监听 3306 端口,这是容器内部的 3306。宿主机上的 3306 如果不做映射,你在宿主机上用mysql -h 127.0.0.1 -P 3306去连,永远连不上。需要理解一个核心概念:容器有自己独立的网络命名空间,端口映射只是在宿主机上“开了一道门”,把宿主机的某个端口转发到容器内部端口。这道门开在哪里、怎么开,都是由docker run -p决定的。
学 Docker 下的 MySQL,最好的方式不是先背命令,而是先在脑子里建立一个模型:镜像、容器、数据卷、端口映射、环境变量、容器内客户端工具。这篇文章就是围绕这几个词展开的。你会从拉取镜像开始,逐步搞懂第一次初始化密码是怎么发生的、为什么数据能持久化、为什么要用docker exec进入容器执行 SQL,以及最常见的连接和权限问题到底出在哪个环节。
1.1 MySQL 官方镜像的启动逻辑
官方镜像mysql:8.0并不是下载下来就能直接用,它内部有一套“初始化流程”。第一次启动时,如果发现数据目录/var/lib/mysql是空的,就会自动执行初始化脚本:创建系统表、创建 root 用户、设置密码,并且处理你通过环境变量传进来的各种配置。
这套流程里最常用的几个环境变量:
MYSQL_ROOT_PASSWORD:初始化时给 root 用户设置的密码,仅在数据目录为空时生效。MYSQL_DATABASE:初始化时自动创建的数据库。MYSQL_USER和MYSQL_PASSWORD:初始化时创建的业务账号和密码。MYSQL_ROOT_HOST:允许从哪些主机用 root 连接,默认不设置时,容器外部通常连不上,这也是很多人本地 Navicat 连不上的重要原因之一。
你还需要知道,容器的/docker-entrypoint-initdb.d/目录里可以放.sql、.sh脚本。第一次初始化时,这些脚本会按文件名顺序自动执行,适合放表结构初始化、种子数据一类的文件。后面我会演示一个更直观的用法:通过docker exec进入容器,再调用容器内的mysql客户端执行 SQL。
1.2 选镜像版本:不是越新越好
官方镜像的标签非常多,新手不用全看,记住三个就够:
| 镜像标签 | 特点 | 建议 |
|---|---|---|
mysql:5.7 | 老项目常见,兼容性好 | 已进入维护末期,新项目不建议再用 |
mysql:8.0 | 当前大多数教程和云厂商默认版本 | 学习和新项目首选 |
mysql:8.4 | MySQL 8.4 LTS 长期支持版 | 生产环境如果追求稳定,可以考虑 |
选版本这件事,最怕的是“照抄网上教程但版本不同,行为还不一样”。比如 MySQL 5.7 默认认证插件是mysql_native_password,到了 MySQL 8.0 默认变成caching_sha2_password,旧客户端可能会出现认证插件不兼容的问题。文章后面我专门说这个坑。
2. 拉取镜像和启动容器:一步步把 MySQL 跑起来
2.1 基础环境检查
开始之前,先在终端里确认 Docker 正常工作:
docker version docker info docker imagesdocker version能看到客户端和服务端版本,docker info能看到数据目录、容器数量、存储驱动等全局信息,docker images用来确认本地已经有哪些镜像。
如果你在 Windows 或 macOS 上,通常用的都是 Docker Desktop。它自带图形界面,容器跑没跑一眼就能看到。Linux 上用systemctl status docker看 Docker 服务状态。确认没问题再继续。
2.2 拉取 MySQL 8.0 镜像
docker pull mysql:8.0拉取完以后,用docker images查看:
docker images | grep mysql会看到类似mysql 8.0 xxxxxxxxxxxx的记录。为什么建议用官方镜像而不是第三方打包镜像?因为官方镜像的 Dockerfile 是公开的,版本标签语义明确,底层也是常见的基础系统。网上有些“全功能”镜像看起来省事,实际里面加了很多你不知道的东西,镜像安全和容器安全都不好保障。学习阶段选官方镜像,也是在养成一个安全习惯。
2.3 用 docker run 启动 MySQL 容器
下面这条命令是核心中的核心:
docker run -d \ --name mysql-server \ -e MYSQL_ROOT_PASSWORD='你的密码' \ -p 3306:3306 \ -v mysql-data:/var/lib/mysql \ --restart unless-stopped \ mysql:8.0逐项拆解一下:
-d:后台运行,不在终端里一直挂着日志。--name mysql-server:给容器起一个固定名字,之后不管docker exec、docker stop都直接用这个名字,不用记一串容器 ID。-e MYSQL_ROOT_PASSWORD='你的密码':把密码传给容器的初始化脚本。注意它只在第一次创建数据目录时生效,后面改密码不能靠改这个环境变量实现。-p 3306:3306:把宿主机 3306 端口映射到容器 3306 端口。左边是宿主机端口,右边是容器端口。如果宿主机 3306 已经被别的程序占了,可以把左边改成3307,外部客户端用 3307 连接。-v mysql-data:/var/lib/mysql:创建名为mysql-data的命名数据卷,并挂载到容器内的 MySQL 数据目录。只要这个卷还在,删容器、重建容器,数据都不会丢。--restart unless-stopped:Docker 服务重启或主机重启时,自动把容器拉起来。这个参数对实际使用特别重要,不加的话,服务器重启后你得手动去启动容器。
我自己一开始踩过的坑就是漏了-v。容器删掉以后,所有数据库全没了,只能对着空空如也的卷发呆。后来养成的习惯是:先创建命名卷,再跑docker run。万一要把容器彻底删了重来,数据还在,重启一个新的容器挂同一个卷就行。
2.4 查看启动日志和容器状态
docker ps docker logs mysql-serverdocker ps看容器状态,STATUS 如果是Up说明还在跑。docker logs mysql-server看日志,第一次启动时能看到初始化信息。等日志里出现类似ready for connections的提示,就说明 MySQL 真正就绪了。
如果容器启动失败,日志会直接告诉你原因。哪怕你一开始看不懂 MySQL 内部报错,至少也能看到是权限、端口还是磁盘问题,这样排查方向就对了。
有一个非常常见的坑:容器启动成功后,日志里显示初始化完成,但过一会儿容器停了。多数原因是数据目录权限不对,或者宿主机端口被占用。前者我后面专门用一节的篇幅讲。
2.5 数据卷和 bind mount 怎么选
Docker 提供两种常见挂载方式:
| 方式 | 写法示例 | 适合场景 |
|---|---|---|
| 命名卷 | -v mysql-data:/var/lib/mysql | 最推荐,Docker 自动管理路径,权限问题少 |
| 绑定挂载 | -v /home/user/mysql-data:/var/lib/mysql | 需要直接查看宿主机文件,或者已经有现成数据 |
如果你是在 Linux 上使用绑定挂载,要注意 MySQL 容器内部以mysql用户运行,这个用户的 UID 通常是999。你创建的目录如果默认属于 root,MySQL 可能根本没权限写数据。解决办法是给目录授权:
sudo chown -R 999:999 /home/user/mysql-data用命名卷就不会遇到这种权限问题,所以我建议新手首选命名卷。等以后有经验了,再根据自己的习惯选择 bind mount。
3. 配置 MySQL:字符集、排序规则和认证方式
3.1 通过启动参数指定字符集
数据库出现乱码,绝大多数都是字符集设置问题。MySQL 8.0 默认字符集其实已经是utf8mb4,但我还是建议在启动参数里显式指定,避免不同的版本、不同的初始化行为带来意外。
docker run -d \ --name mysql-server \ -e MYSQL_ROOT_PASSWORD='你的密码' \ -p 3306:3306 \ -v mysql-data:/var/lib/mysql \ mysql:8.0 \ --character-set-server=utf8mb4 \ --collation-server=utf8mb4_unicode_ci这里的关键是:--character-set-server和--collation-server是mysqld的参数,不是docker run的参数。把它们放在镜像名后边,Docker 启动容器时会把它们传给 MySQL 的启动入口。
为什么选utf8mb4?因为它能完整支持中文、表情符号,而老的utf8在 MySQL 里实际上只能存一部分 Unicode 字符。排序规则选utf8mb4_unicode_ci比较通用,对大多数中文项目都够用。
3.2 挂载自定义 my.cnf
命令行传参数虽然方便,但配置一多就不合适了。更常见的做法是准备一个自定义配置文件,挂载到容器里。
先在宿主机创建一个配置文件:
mkdir -p ~/docker/mysql/conf cat <<'EOF' > ~/docker/mysql/conf/mysql-custom.cnf [mysqld] character-set-server = utf8mb4 collation-server = utf8mb4_unicode_ci max_connections = 200 EOF然后启动容器时挂载到/etc/mysql/conf.d/目录下:
docker run -d \ --name mysql-server \ -e MYSQL_ROOT_PASSWORD='你的密码' \ -p 3306:3306 \ -v mysql-data:/var/lib/mysql \ -v ~/docker/mysql/conf/mysql-custom.cnf:/etc/mysql/conf.d/mysql-custom.cnf:ro \ mysql:8.0注意,官方镜像的/etc/mysql/my.cnf最后会 include/etc/mysql/conf.d/下所有.cnf文件。把自定义配置放在这个目录里,不会覆盖原始配置,只做追加。挂载时我特意加了:ro,容器内只读,避免被误改。
改完配置文件后,需要重启容器:
docker restart mysql-server然后验证配置是否生效:
docker exec mysql-server mysql -uroot -p'你的密码' -e "SHOW VARIABLES LIKE 'character%';"如果你看到character_set_server的值是utf8mb4,就说明配置生效了。
3.3 MySQL 8.0 的认证插件问题
MySQL 8.0 默认的认证插件是caching_sha2_password,加密强度更高。但这会带来一个兼容性问题:一些老版本的客户端、旧版语言驱动不认识这个插件,连接时报错:
Authentication plugin 'caching_sha2_password' cannot be loaded遇到这种情况,第一选择应该是升级客户端工具。MySQL 8.0 发布已经很多年了,新版本工具都支持这个插件。万一你没法升级客户端,退而求其次的做法是在 MySQL 里创建一个使用旧认证方式的账号:
CREATE USER 'app'@'%' IDENTIFIED WITH mysql_native_password BY 'App@123'; GRANT ALL PRIVILEGES ON app_db.* TO 'app'@'%'; FLUSH PRIVILEGES;或者直接在容器启动参数里加上:
--default-authentication-plugin=mysql_native_password但要清楚,mysql_native_password的加密方式和安全性都比caching_sha2_password弱。只能作为过渡手段,不要长期依赖。
4. 进入容器执行 SQL:交互式与非交互式
4.1 交互式进入容器,打开 mysql 客户端
容器跑起来之后,最直接的 SQL 执行方式就是进入到容器内部,使用容器里自带的mysql客户端。
docker exec -it mysql-server bash进入 bash 后,再执行:
mysql -uroot -p输入密码,就能看到mysql>提示符。接下来像在本地操作 MySQL 一样执行 SQL 就行:
SHOW DATABASES; USE mysql; SELECT user, host, plugin FROM user;这里docker exec -it里的-i表示保持标准输入打开,-t表示分配一个伪终端。少了-t,命令能执行但不会有交互式排版;少了-i,你可能没法正常输入内容和回车。所以查询、登录这种需要用户输入的交互场景,-it最好一起用。
4.2 非交互式直接执行 SQL
在自动化场景里,你不可能每次都手动登录再敲 SQL。可以用-e参数直接在容器里执行 SQL:
docker exec mysql-server mysql -uroot -p'你的密码' -e "SHOW DATABASES; SELECT NOW();"输出结果会直接打印在终端里。如果你要执行一个 SQL 文件,可以先把文件放到宿主机,然后通过 stdin 传入:
docker exec -i mysql-server mysql -uroot -p'你的密码' app_db < init.sql这里-i是必须的,因为<重定向把宿主机文件内容作为标准输入传给了容器内进程。不要写成docker exec -it,因为不需要分配伪终端,反而可能引发奇怪的换行问题。
如果你觉得密码直接出现在命令行里不安全,还可以这样:
docker exec -e MYSQL_PWD='你的密码' mysql-server mysql -uroot -e "SELECT 1"MYSQL_PWD是读取的 MySQL 客户端环境变量,能避免在命令行参数里暴露密码。不过docker exec -e传递的变量会出现在容器进程环境里,也谈不上绝对安全。真正在生产环境做自动化,更稳妥的方式是使用 MySQL 的--defaults-extra-file配置临时文件,并限制文件权限,这里不展开说了。
4.3 在宿主机上直接连接容器
如果你用的是 Navicat、DBeaver、MySQL Workbench 这类可视化工具,不需要先进容器,直接连宿主机映射端口就行:
mysql -h 127.0.0.1 -P 3306 -uroot -p这里的-h 127.0.0.1指向宿主机,-P 3306指向映射出来的端口。注意,-P是大写,-p是小写,一个是端口,一个是密码。这个问题看着低级,但我真见过有人因为大小写写错,白白折腾了半小时。
5. 外部客户端连接与权限排查
5.1 用 DBeaver、Navicat 连接容器
连接参数基本固定:
| 参数 | 值 |
|---|---|
| 主机 | 127.0.0.1或localhost |
| 端口 | 3306(如果宿主机端口映射成 3307 就写 3307) |
| 用户名 | root |
| 密码 | 初始化时MYSQL_ROOT_PASSWORD设的密码 |
这里有个细节:很多人把“容器里的 3306”和“宿主机连接端口”弄混。docker run -p 3307:3306之后,数据库实际监听的是容器内 3306,但外部客户端要填 3307。填 3306 反而连不上。
5.2 为什么 root 连接总是被拒绝
在官方 MySQL 镜像里,root 账号默认只允许本地连接。所谓“本地”,指的是容器内部。你从宿主机用 Navicat 连接时,MySQL 看到的是来自一个陌生 IP 的连接,自然拒绝。
解决方法有两种。
第一种是我们在启动容器时直接设置 root 允许所有主机:
docker run -d \ --name mysql-server \ -e MYSQL_ROOT_PASSWORD='你的密码' \ -e MYSQL_ROOT_HOST='%' \ -p 3306:3306 \ -v mysql-data:/var/lib/mysql \ mysql:8.0%是 MySQL 里的通配符,表示允许从任何主机连接。适合个人开发环境。如果是公司内网环境,建议把%换成具体网段,比如192.168.1.%,降低暴露风险。
第二种是创建专门的应用账号:
CREATE USER 'app'@'%' IDENTIFIED BY 'App@123'; GRANT ALL PRIVILEGES ON app_db.* TO 'app'@'%'; FLUSH PRIVILEGES;这样你就不需要把 root 暴露给外部,业务服务一律用 app 账号连接。我对这个做法的评价是:简单、省事,而且更符合最小权限原则。
5.3 MySQL SSL 连接错误
另一个高频问题就是 SSL。MySQL 8.0 默认启用 SSL 相关配置,新客户端一般能自动适配,但旧驱动或者连接工具版本太老时,会报类似:
SSL connection error: error:1425F102 ...解决思路分两步:
- 优先升级客户端工具或者数据库驱动版本。
- 如果只是本地开发环境,保证网络可信的前提下,可以在连接参数里关闭 SSL。
以 mysql 自带的命令行客户端为例:
mysql -h 127.0.0.1 -P 3306 -uroot -p --ssl-mode=DISABLEDDBeaver 的连接设置里也有 SSL 配置页,选“不使用 SSL”即可。生产环境不要随便关,这是连接层面的最后一道防线,能开就开。
6. 常见问题与排查实录
我在用 Docker 跑 MySQL 的过程中,踩过不少别人也大概率会踩的坑。整理成一张速查表,语言不绕弯,直接给原因和解法:
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
容器启动失败,端口报错bind: address already in use | 宿主机 3306 端口被其他进程占用 | 换宿主机端口,如-p 3307:3306 |
| 容器起来了,但外部客户端连不上 | 未设置MYSQL_ROOT_HOST,root 默认只允许容器内连接 | 启动时加-e MYSQL_ROOT_HOST='%',或创建远程账号 |
连接时报Access denied for user 'root'@'...' | 账号 host 权限不匹配,或密码错误 | 用docker exec进入容器检查账号 host 和认证方式 |
报认证插件caching_sha2_password无法加载 | 客户端版本过旧 | 升级客户端,或创建mysql_native_password用户临时过渡 |
日志里提示Can't create/write to file | bind mount 目录权限不对 | sudo chown -R 999:999 数据目录 |
| 中文乱码 | 字符集或客户端连接字符集没统一 | 服务端配置utf8mb4,连接串也指定utf8mb4 |
| 删掉容器重新创建后,修改环境变量密码不生效 | 数据卷已初始化,root 密码不会因为环境变量变化而重置 | 正确的做法是备份后使用 SQL 修改密码,或做密码恢复 |
6.1 端口占用的处理
宿主机上如果已经装了 MySQL,或者另外的容器已经在用 3306,再启动一个映射到 3306 的容器,一定会报错。这种时候不需要纠结,把宿主机端口换掉就行:
docker run -d \ --name mysql-server \ -p 3307:3306 \ ...容器内部仍然是 3306,外部连接改成-P 3307。修改端口只会影响外部访问,容器里的 MySQL 客户端仍然通过内部 socket 连接,不受影响。
6.2 bind mount 权限问题
用-v /home/user/mysql-data:/var/lib/mysql这种方式挂载数据目录,在 Linux 上经常遇到权限问题。原因在于容器内的 MySQL 进程不是以 root 运行,而是以mysql用户运行。宿主机上你这个目录原来属于 root,MySQL 无法写入。
解决办法:
sudo chown -R 999:999 /home/user/mysql-data如果部署环境开启了 SELinux,还可能遇到更隐蔽的权限拒绝,需要在挂载参数里加上:z之类的标签。第一次遇到这种问题不要慌,先看docker logs mysql-server,日志里会明确写到哪个路径、哪个权限出错。顺着路径走,基本都能解决。
6.3 忘记 root 密码怎么办
这个场景让人头大,但先说一个误区:不要以为重新docker run一个新的容器,配一个新的环境变量就能把密码改掉。只要你的数据卷不是空的,MySQL 初始化脚本就会跳过初始化过程,MYSQL_ROOT_PASSWORD不会再起作用。
正确的恢复思路是:在容器启动命令里临时加上--skip-grant-tables,绕过权限表,然后进入容器把密码改回来。实际操作起来流程比较复杂,新手如果操作失误,可能把权限表搞坏,所以我更建议提前做好备份,不要等到忘记密码再救。如果数据不重要,最简单粗暴的方式就是把旧卷备份后删掉,重新初始化一个全新容器。
6.4 用 docker logs 做第一排查工具
无论遇到什么问题,第一步永远是:
docker logs mysql-server日志会告诉我:是端口冲突,还是数据目录权限,还是初始化 SQL 脚本语法错误,还是内存不足。不要一上来就改配置、删容器,先看日志。日志里没有有效信息的时候,再考虑进入容器排查环境。
7. 日常维护:备份、恢复和容器管理习惯
7.1 用 mysqldump 做备份
备份这件事,再强调都不为过。Docker 里的数据库备份逻辑,其实就是把宿主机上的备份请求放进容器,让容器内的mysqldump执行,然后把标准输出重定向到宿主机文件。
单库备份:
docker exec mysql-server sh -c 'exec mysqldump -uroot -p"$MYSQL_ROOT_PASSWORD" --single-transaction --routines --triggers app_db' > app_db_$(date +%F).sql这里的关键是sh -c:因为我需要在容器内展开$MYSQL_ROOT_PASSWORD这个环境变量。--single-transaction适合 InnoDB,备份过程中不会锁表,对线上影响小。--routines和--triggers会带上存储过程和触发器。
恢复备份:
docker exec -i mysql-server sh -c 'exec mysql -uroot -p"$MYSQL_ROOT_PASSWORD" app_db' < app_db_$(date +%F).sql注意恢复时用的是-i而不是-it,因为文件重定向进来的是标准输入流,不需要分配交互式终端。
7.2 容器启动策略和资源限制
生产服务器重启后,MySQL 容器能不能自动恢复,取决于--restart参数。我习惯用unless-stopped,而不是always。区别在于:如果某次手动docker stop了容器,unless-stopped不会在 Docker 重启后自作主张把它拉起来;always不管你为什么停,只要 Docker 启动就会拉起来。对于数据库这类需要人工判断的组件,unless-stopped更合理。
已经启动的容器也能改策略:
docker update --restart unless-stopped mysql-server另外,在服务器上跑 MySQL 容器,最好限制资源,避免它把宿主机内存吃满:
docker run -d \ --name mysql-server \ --memory=512m \ --cpus=1 \ ...--memory=512m限制容器最多使用 512MB 内存,--cpus=1限制使用 1 个 CPU 核心。具体数值看业务量,我这里给的只是学习环境的参考。生产环境不要拍脑袋定,要看实际监控数据。
7.3 把启动命令固化下来
最后再说一个我自己的使用习惯。我刚开始学 Docker 的时候,每创建一个容器都靠手敲一条长长的docker run。结果过两天要重建容器,参数记不全,又翻历史记录,还容易漏掉配置。
后来我的做法是:每个项目建一个目录,把docker run命令写成一个start.sh脚本,配置文件和初始化 SQL 也放在同一个目录下。重建容器时直接执行脚本,参数不会丢,别人接手你的环境也能一眼看懂你当初是怎么启动的。
比如:
#!/bin/bash docker run -d \ --name mysql-server \ -e MYSQL_ROOT_PASSWORD='你的密码' \ -e MYSQL_ROOT_HOST='%' \ -p 3306:3306 \ -v mysql-data:/var/lib/mysql \ -v ~/docker/mysql/conf/mysql-custom.cnf:/etc/mysql/conf.d/mysql-custom.cnf:ro \ --restart unless-stopped \ mysql:8.0这个脚本给自己用,也给团队用,比口头交代“我就是这么启动的”要靠谱得多。
最后再说一句:Docker 管理 MySQL 本身不复杂,复杂的是把数据持久化、权限控制、备份恢复、连接兼容性这些底层问题想清楚。把这篇文章里的关键命令亲手敲一遍,再故意删掉容器重建一次,体验一下数据还在的感觉,你对容器和数据卷的理解就会完全不同。