上周有个朋友给我发来一张截图,说他用 Docker 装了 MySQL 8.0,容器状态明明显示 Up,Navicat 却怎么也连不上,报了个“10060 未知错误”。他抱怨了半天“Docker 真难用”,我一看他贴出来的 docker run 命令,立刻明白了问题在哪——端口是映射了,但是 MySQL 8.0 默认的 root 用户根本不允许远程登录,更别提他还踩了认证插件这个新坑。
说实话,这类问题我前前后后帮人排查过不下二十次。很多教程都只写到“docker run 拉起来就完事了”,但实际部署过一个数据库的人都懂,容器起来只是万里长征第一步,后面要面对的是权限、认证、防火墙、端口映射这一连串连锁问题。这篇文章我会把我在 Linux 上通过 Docker 安装 MySQL 8.0,并成功开启远程连接的完整过程写出来,包括每一步背后的原理、会遇到哪些坑、每一类报错该怎么查,尽量做到你跟着走一遍就能搞定。
1. 装之前先想明白三件事:镜像、目录、版本策略
1.1 为什么选 Docker 而不是直接在宿主机装 MySQL
我知道肯定有人会问:既然要装在 Linux 上,为什么不直接用 apt 或 yum 装,非要绕一层 Docker?我的回答是:Docker 容器带来的环境隔离和可复现性,对数据库这种需要长期维护的服务来说,价值远远大于那一点点性能损耗。
在 CentOS 上直接装 MySQL 8.0,你大概率要先跟系统自带的 MariaDB 库做斗争,卸载、添加官方 Yum 源、处理依赖冲突,一套操作下来少说折腾半小时。在 Ubuntu 上稍微好一点,但升级系统版本时搞不好就给你来个 MySQL 无法启动。而 Docker 方式下,MySQL 运行在自己的容器环境里,宿主机上的系统库怎么变都不会影响它,卸载的时候一个 docker rm -f 就干净利落,不会在系统里残留一堆配置文件碎片。
另一个现实的好处是多版本共存。我有时候要同时调试 5.7 和 8.0 的项目,Docker 里起两个容器,一个映射 3306 端口,一个映射 3307 端口,互不干扰。这在宿主机直装方式下基本不可能实现。
当然,Docker 部署也有代价:排查问题的时候要多绕一层,得学会看容器日志、进容器里操作。如果你完全没有 Docker 基础,建议先在测试机捣鼓几遍再上生产。
1.2 镜像 Tag 别乱选:mysql:8.0 与 mysql:latest 的区别
Docker 镜像的 Tag 选择是个很容易被忽略的细节。很多人图省事直接拉 mysql:latest,我强烈不建议你这么干——latest 是滚动更新的,每次 docker pull 可能拉到不同小版本的镜像,也许今天和明天的行为就有微妙差异,这在生产环境是大忌。
我的建议是:固定 Tag,锁死大版本。比如 mysql:8.0,这个 Tag 会指向 8.0 系列的最新小版本(比如 8.0.36),虽然比 latest 收敛了很多,但在 docker pull 时依然可能拉到新版。如果你追求极致稳定,可以精确到 mysql:8.0.36,这样每次拉下来的镜像内容完全一致。
另外要注意,MySQL 官方镜像其实分好几类:mysql:8.0 默认是基于 Oracle Linux 的版本,体积相对大一些;还有 mysql:8.0-debian,基于 Debian,体积更小。我日常使用 mysql:8.0 就足够了,除非你有明确的体积或依赖要求,否则不必纠结。
1.3 数据目录和配置目录的挂载规划
容器本质上是临时性的,如果哪天误操作把容器删了,而 MySQL 数据又恰好存在容器内部,那整个数据库就直接没了。所以不管你是自己测试还是给公司搭服务,数据目录必须挂载到宿主机,这是底线。
我习惯先规划出一个统一的目录结构,比如放在 /data/mysql 下:
bash复制mkdir -p /data/mysql/{data,conf,logs}
data目录对应容器内的/var/lib/mysql,存放 MySQL 的库表数据文件。conf目录对应容器内的/etc/mysql/conf.d,放自定义的配置文件。logs目录用于存放容器的日志输出或者 MySQL 自身的错误日志。
这样做的好处是:即使整个容器被删掉重建,只要数据目录和配置目录还在,重新 docker run 一次,MySQL 的所有数据都原封不动。我在很多生产环境就是这么干的,曾经遇到一次宿主机强制重启导致异常关机,容器起来后 MySQL 照样自动恢复,数据完好无损。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 落到地上的完整安装过程:从拉镜像到容器初始化
2.1 用 docker run 一次性跑起来,每个参数都讲清楚
环境准备就绪后,先拉取镜像。这里我以 mysql:8.0 为例:
bash复制docker pull mysql:8.0
拉完后,执行创建容器命令。下面是我最常用的完整命令,逐行解释清楚每个参数的含义,你看完就能举一反三:
bash复制docker run -d \
--name mysql8 \
-p 3306:3306 \
-e MYSQL_ROOT_PASSWORD='YourStrongPassWord' \
-e TZ=Asia/Shanghai \
-v /data/mysql/data:/var/lib/mysql \
-v /data/mysql/conf:/etc/mysql/conf.d \
-v /data/mysql/logs:/logs \
--restart unless-stopped \
mysql:8.0
逐个拆解:
-d:后台运行容器,命令行不会一直挂在前台。--name mysql8:给容器起个名字,后续用docker logs mysql8、docker exec -it mysql8都靠它定位。-p 3306:3306:端口映射,宿主机 3306 端口转发到容器内 3306。注意,这里有个常见误区,如果宿主机 3306 端口已经被别的进程占用,容器会启动失败,所以装之前先执行ss -tlnp | grep 3306确认一下端口空闲。-e MYSQL_ROOT_PASSWORD:设置 root 用户的初始密码。这个环境变量只在容器首次初始化时生效,后面你想改密码,进容器用 SQL 改。-e TZ=Asia/Shanghai:设置容器时区为上海。如果不加这个,容器默认 UTC 时区,数据库里的NOW()会和北京时间相差 8 小时,排查问题时会很迷惑。-v:把宿主机的三个目录挂载进容器,道理前面已经说过。--restart unless-stopped:设置容器的重启策略为“除非手动停止,否则开机自动拉起”。服务器意外重启后不用人工干预,数据库就自己恢复了。
2.2 容器起来了不等于安装完:日志、版本、连接三连校验
执行完 docker run,第一件事是看容器状态:
bash复制docker ps
正常能看到 mysql8 的状态是 Up。但这里要提醒一句:状态是 Up 不代表 MySQL 服务已经就绪。MySQL 容器初始化需要一点时间,刚启动那几十秒里,容器进程在跑,但数据库还没准备好接受连接。
最可靠的方式是看日志:
bash复制docker logs mysql8
当你看到日志末尾出现类似这样的内容,才说明 MySQL 真正完成了初始化,可以接受连接了:
log复制[System] [MY-010931] [Server] /usr/sbin/mysqld: ready for connections.
Version: '8.0.36' socket: '/var/run/mysqld/mysqld.sock' port: 3306 MySQL Community Server - GPL.
光看日志还不够,必须实际连接一次验证。先在宿主机上确认能不能连进容器内的 MySQL:
bash复制docker exec -it mysql8 mysql -uroot -p
输入你设置的 MYSQL_ROOT_PASSWORD,能进入 MySQL 命令行说明基础安装没问题。退出登录时输入 exit 即可。
然后再检查一个容易忽略的细节——容器内 MySQL 的 root 用户到底允许从哪里连接。进入 MySQL 命令行后执行:
sql复制SELECT user, host, plugin FROM mysql.user;
默认情况下,你会看到 root 用户有两行:一行 host 是 localhost,一行 host 是 %。如果你用的是上面这种 docker run 标准方式初始化,官方镜像默认会创建 root@'%';但如果你是用旧版本镜像初始化,或者初始化方式不同,可能只有 localhost,那远程连接就不可能成功。这一步是后面远程连接能不能通的先决条件,先搞清楚现状再谈修改。
2.3 时区、字符集、下划线规则这些配置怎么调
MySQL 8.0 默认字符集已经是 utf8mb4,对中文场景很友好,基本不用改。但时区问题需要确认一下,进入容器内执行:
sql复制SELECT NOW();
如果输出的时间和你本地北京时间一致,那就没问题。如果你忘了加 TZ=Asia/Shanghai,也可以通过修改配置文件来补救。
在宿主机 /data/mysql/conf 目录下新建一个配置文件,例如 my-custom.cnf:
ini复制[mysqld]
default-time-zone = '+08:00'
然后重启容器:
bash复制docker restart mysql8
这里插一句关于 lower_case_table_names 的事,这是个很坑的参数。它在 Linux 上默认值是 0,也就是说表名区分大小写,而在 Windows 上默认是不区分。如果你之前是在 Windows 上开发的,把 SQL 脚本拿到 Linux 上跑,可能会出现“表不存在”的诡异报错。这个参数必须在数据库初始化之前,也就是第一次启动容器时就通过映射配置文件指定,初始化完成后修改需要重建容器,而且旧数据目录可能因此无法使用,所以务必提前想好。
3. 远程连接失败的根因排查:一条命令一条命令查
3.1 先分清错误类型:Access denied、超时、拒绝连接各代表什么
这一步是整个远程连接场景里最关键的部分。很多人一看到“连接失败”就懵了,其实不同的报错信息对应着完全不同的故障层面,排查路径天差地别。我整理了最常见的几类报错,你先对照一下自己的报错属于哪一类:
| 报错信息(节选) | 故障层面 | 核心排查方向 |
|---|---|---|
Access denied for user 'root'@'...' |
认证层 | 账号、密码、host 授权 |
Can't connect to MySQL server ... (10060) |
网络层 | 防火墙、安全组、端口映射 |
Can't connect to MySQL server ... (10061) |
网络层 | 端口未监听、容器未启动 |
Authentication plugin 'caching_sha2_password' cannot be loaded |
客户端兼容层 | 客户端版本过旧、认证插件不一致 |
Public Key Retrieval is not allowed |
客户端兼容层 | 连接参数中未允许获取公钥 |
你自己对照完,大概率能锁定一个大致方向。但实际情况中,很多人的报错是“时好时坏”,比如用命令行能连,用图形化客户端连不上;或者本机能连,远程不能连。所以还需要按下面的顺序逐层排查。
3.2 授权与认证插件:MySQL 8.0 最容易绊倒人的地方
先说结论:MySQL 8.0 的默认认证插件是 caching_sha2_password,而很多旧版本的图形化客户端(比如老版本的 Navicat)或者老语言驱动只支持 mysql_native_password。这就是为什么很多人执行完所有步骤,端口也通了,防火墙也放行了,最后还是连不上的原因。
我在很多服务器上实测过,完整操作流程是这样:
第一步,进入容器内的 MySQL:
bash复制docker exec -it mysql8 mysql -uroot -p
第二步,查看当前 root 用户的 host 和 plugin:
sql复制SELECT user, host, plugin FROM mysql.user WHERE user = 'root';
正常情况下你会看到 root@'%' 或 root@'localhost'。如果你确认只能从本机连接,说明 host 有问题,先执行授权:
sql复制ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY '你的密码';
如果你用的初始化命令没有生成 root@'%',就需要手动创建:
sql复制CREATE USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY '你的密码';
GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' WITH GRANT OPTION;
FLUSH PRIVILEGES;
这里我推荐在生产环境用独立专用账号而不是 root 远程,具体方案在第 4 章会说。如果你只是本地测试学习,那上面这套把 root 改成 mysql_native_password 的操作足够用了。
第三步(重要),MySQL 8.0 默认的 caching_sha2_password 本身没有任何安全问题,只是兼容性差。如果你的客户端版本足够新(比如最新版 DBeaver、MySQL Workbench、Navicat 16+),完全可以继续用默认插件,不需要强行切换回旧插件。如何判断呢?直接用命令行客户端连一下远程地址,命令行客户端一般是最新版本,能兼容新插件。如果命令行能连,而图形化客户端连不了,那十有八九就是插件兼容问题,解决办法要么升级客户端,要么把账号插件改成 mysql_native_password,二选一。
3.3 防火墙、端口映射、安全组三层网络关卡逐个验
如果报错是 10060 或者 10061,说明网络链路还没打通,按照下面顺序一层一层查。
第一层,确认 Docker 端口映射真的生效了:
bash复制docker ps
输出中的 PORTS 列应该有类似 0.0.0.0:3306->3306/tcp 的内容。如果这里显示的是 127.0.0.1:3306->3306/tcp,说明容器只监听在本地回环地址上,外部根本无法访问,这就是某些特殊 docker run 参数导致的问题。正常情况下应该绑定 0.0.0.0。
第二层,确认宿主机真的在监听 3306 端口:
bash复制ss -tlnp | grep 3306
看到 LISTEN 状态说明进程在监听。如果这里都没有输出,说明容器内部 MySQL 可能根本没启动成功,或者端口映射配置不对。
第三层,检查宿主机防火墙。CentOS 7+ 系统上用 firewalld:
bash复制systemctl status firewalld
firewall-cmd --zone=public --add-port=3306/tcp --permanent
firewall-cmd --reload
Ubuntu 系统用 ufw:
bash复制ufw status
ufw allow 3306/tcp
如果你用的是云服务器,既要检查系统内防火墙,还要检查云平台控制台里的安全组规则。这个我遇到过太多回了:系统防火墙全关了,端口也在听,但云平安全组默认只放行 22、80、443,3306 根本没放行,外部永远连不上。
安全组规则的优先级大于系统防火墙,所以务必主动去云控制台确认一下。
3.4 客户端工具的兼容性细节
网络通了、授权也没问题,最后一步才是客户端连接参数的坑。这一层的问题很隐蔽,因为报错信息往往不直观。
最常见的坑是 JDBC 连接串。如果你是用 Java 程序连 MySQL 8.0,遇到 Public Key Retrieval is not allowed 这个报错,通常是因为 JDBC 驱动版本较新,而连接串里没有加参数。解决办法是在连接串的 URL 最后追加:
code复制?useSSL=false&allowPublicKeyRetrieval=true
这两个参数的含义分别是:关闭 SSL,以及允许客户端从服务器获取公钥。因为 caching_sha2_password 插件在非 SSL 协议下认证时需要 RSA 公钥交换,默认 JDBC 驱动为了安全不允许这个操作。
如果你用 Python 的 pymysql 连接,可能遇到的坑是直接报插件不支持,解决方案要么升级到 mysql-connector-python,要么同样把账号认证插件换成 mysql_native_password。这点在生产环境很常见,我在两个项目的遗留代码里都遇到过。
4. 生产环境下的收尾:权限收敛、备份、容器运维
4.1 别拿 root 远程,创建一个专用账号并限制来源
很多人图方便,直接把 root 开放成 %,然后用 root 远程连。我劝你谨慎,尤其是生产环境,root 一旦被爆破,整个数据库就裸奔了。
正确做法是创建专用账号,按需授权。比如应用只需要操作某个业务库,那就只给它该库的权限:
sql复制CREATE USER 'app_user'@'192.168.1.%' IDENTIFIED BY 'Str0ngPassword';
GRANT SELECT, INSERT, UPDATE, DELETE ON bizdb.* TO 'app_user'@'192.168.1.%';
FLUSH PRIVILEGES;
这里 host 我只授权了 192.168.1.% 网段,而不是 %,这样即使密码泄露,攻击者也只能从内网网段连接,外网无法访问。如果你需要从某个固定的办公网 IP 连,那就更精确地设置成 'app_user'@'123.45.67.89'。
另外,生产环境建议把 root 用户的远程访问权限收回,只保留本地。如果之前已经运行过 GRANT ALL PRIVILEGES ON *.* TO 'root'@'%',就把它删掉:
sql复制DROP USER 'root'@'%';
权限这东西,原则就是最小化,够用就好。多给一个登不上不用的账号,就多一分暴露面。
4.2 用 mysqldump 做定时备份与恢复演练
备份是每个数据库管理员的基本功。Docker 容器里的 MySQL 备份方式和宿主机直装的差别在于,mysqldump 命令要在容器内执行,然后想办法把备份文件弄到宿主机上。
最简单的备份命令:
bash复制docker exec mysql8 sh -c 'exec mysqldump --all-databases -uroot -p"$MYSQL_ROOT_PASSWORD"' > /data/mysql/backup/all_$(date +%F).sql
这个命令将容器的标准输出重定向到宿主机文件。注意,$MYSQL_ROOT_PASSWORD 是在容器内引用环境变量,所以外层用单引号包住了整个 sh -c 命令,防止宿主机 shell 提前展开。
恢复的时候,把备份文件导入容器:
bash复制cat /data/mysql/backup/all_2025-06-01.sql | docker exec -i mysql8 mysql -uroot -p你的密码
注意这里用的是 -i,不带 -t,因为要输入重定向,不能分配伪终端。
光会备份还不够,我强烈建议每隔一段时间做一次恢复演练——在测试机上把备份文件重新导入一遍,验证备份文件可用。很多人备份脚本跑了半年,真出事恢复时才发现文件早已损坏,那才是最惨的。
4.3 容器升级、重启策略、日志处理这些日常运维
容器部署的运维,本质上就是“养”容器。几个日常高频操作:
查看容器日志,定位问题:
bash复制docker logs mysql8 --tail 100
docker logs mysql8 --since 30m
--tail 指定最近多少行,--since 指定最近多少分钟,排错时非常实用。
关于升级:如果官方发布了新小版本,想升级 MySQL 容器,我的做法是:
- 先执行一次全量备份(上面那个 mysqldump 命令)。
- 用
docker pull mysql:8.0.37拉取新镜像。 - 停掉并删除旧容器:
docker stop mysql8 && docker rm mysql8。 - 用同样的
docker run命令重新创建容器,数据目录不变。
这里最核心的是,数据目录挂载没有变,所以数据不会丢失。也正因为这样,第三步的容器删除并不会导致数据丢失,只要挂载的宿主机目录还在。万一升级后出问题,只需要把命令中的镜像 Tag 改回旧版本,重新 docker run 即可。
最后说说日志处理。MySQL 8.0 默认开启 binlog,如果业务量大,日志会持续增长,可能会占满磁盘。可以定期清理或在配置文件中设置过期时间:
ini复制[mysqld]
expire_logs_days = 7
不过要注意,MySQL 8.0 中这个参数已经更名为 binlog_expire_logs_seconds,建议直接设置成秒数,比如 7 天就是:
ini复制[mysqld]
binlog_expire_logs_seconds = 604800
修改配置后重启容器生效。配置写哪?就是前面挂载的 /data/mysql/conf 目录下的自定义配置文件。
再说个题外话,我经常被人问到容器日志和 MySQL 日志的区别。docker logs mysql8 看到的是容器进程的标准输出,也就是 mysqld 的错误日志;binlog 是 MySQL 自己的二进制日志,存的是 SQL 变更历史,两者完全不是一回事。清理的时候别搞混,binlog 能不能删?能,但前提是你确认不需要用它做基于时间点的恢复。删除前一定先备份或确认需求,这坑我见别人踩过。
我自己在服务器上装 MySQL 8.0 的次数已经数不清了,最后分享一个个人习惯:每次装完之后,我会在宿主机上先用命令行走一遍完整链路——docker ps 看状态、docker logs 看初始化、mysql -h127.0.0.1 -P3306 -uroot -p 测 TCP 连通性、再执行 SELECT user, host, plugin FROM mysql.user; 确认权限配置。这一套下来如果都正常,再用远程客户端去连,基本不会出问题。很多人远程连不上,就是因为跳过了宿主机本地测试这一步,出了问题不知道该从哪里查起。这套流程你装得多了就会明白,省下来的是成倍的排查时间。
