Docker 运行 MySQL 与 Navicat 连接:持久化、字符集和排错实践
2026/9/17 3:57:31 网站建设 项目流程

1. 把 MySQL 装进 Docker 之后,我的开发机终于干净了

三年前我的笔记本上同时躺着 MySQL 5.7、MySQL 8.0 和一套 XAMPP 自带的 MariaDB,配置文件散在/etc/mysql/usr/local/etc和某个我早就忘了的目录里,服务开机自启三个抢 3306 端口,卸载的时候注册表残留删了半天。后来我把它们全卸了,改成 Docker 起容器,用 Navicat 连。到现在为止,换机器、切版本、给同事复现环境,基本都是一条命令的事。

这篇东西想聊的就是这个组合:用 Docker 跑 MySQL,再用 Navicat 作为图形客户端连上去。听起来简单,但真动手的人会撞上一堆细节——虚拟化没开导致 Docker Desktop 起不来、3306 端口被占用、容器里密码拿到了却连不上、Navicat 报 1251 或 2059 一脸懵、重启之后数据没了。这些坑我基本都踩过一遍,所以下面不打算只给一条docker run命令就完事,而是把每个参数为什么这么写、连接失败该按什么顺序排查、数据怎么保证不丢,都摊开讲。

适合读的人群很明确:刚接触容器但已经会写 SQL 的后端或数据方向的同学、需要在本机搭一套可随时推倒重来的测试库的开发者、以及被"卸载 MySQL 卸不干净"折磨过的人。如果你完全没碰过命令行,也可以跟着做,我会把每一步的意图说清楚,而不是让你复制粘贴然后祈祷它能跑。

先说结论性的经验:Docker 跑数据库最容易被低估的不是安装,是数据持久化和字符集。安装本身十分钟能搞定,但如果你没把数据目录挂到宿主机、没设utf8mb4,等到上线前发现 emoji 存不进去或者容器重建后表全没了,返工成本就高了。所以我把持久化和字符集放在比较靠前的位置讲。

2. Docker 这一步真正卡住人的,是虚拟化和镜像源

大部分人装 Docker 失败,卡的不是 Docker 本身,是它依赖的底层能力。Windows 上尤其明显。

2.1 Windows 上那两个必须先开的开关

Docker Desktop 在 Windows 上跑 Linux 容器,靠的是 WSL2 或者 Hyper-V 这套虚拟化后端。如果主板 BIOS 里的虚拟化(Intel 叫 VT-x,AMD 叫 SVM)没开,你会看到那句很经典的报错:virtualisation support wasn't detected,或者 Docker Desktop failed to start 之类的提示,界面转圈圈然后卡死。

处理顺序是这样:

  1. 重启进 BIOS/UEFI,找到 CPU 配置里的虚拟化选项,打开。这一步不做,后面全是白费。
  2. 回到 Windows,在"启用或关闭 Windows 功能"里勾上"虚拟机平台"和"适用于 Linux 的 Windows 子系统",重启。
  3. 如果装了 Hyper-V 又同时用 WSL2 后端,一般没问题;但如果你之前装过 VMware 或安卓模拟器,它们可能和 Hyper-V 冲突,表现是 Docker 能起但容器网络异常。这个时候要么统一用 WSL2 后端,要么把冲突的那个虚拟化平台关掉。

安装包从官方渠道下就行,装完先在终端敲docker versiondocker info,能看到 Server 段的信息才算真的通了。只看到 Client 段说明守护进程没起来,别急着往下走。

macOS 相对省事,装 Docker Desktop 或者用更轻量的方案都行,Apple Silicon 机器注意拉镜像时要选支持 arm64 的,MySQL 官方镜像早就支持多架构了,一般不用手动指定平台。Linux 上直接装 Docker Engine 就够,不需要 Desktop 那套图形界面,服务器上跑更合适。

2.2 镜像拉不动的处理逻辑

国内网络环境下docker pull mysql:8.0经常几分钟没反应,这不是 Docker 坏了,是默认的镜像仓库地址在境外。解决办法是配加速地址:在 Docker Desktop 的设置里找到 Docker Engine 那一栏,编辑 JSON,往registry-mirrors里加几个可用的加速地址,保存后重启 Docker。Linux 上改/etc/docker/daemon.json,然后systemctl restart docker

改完之后验证一下,用docker pull hello-world拉个小镜像试试,能拉到再拉 MySQL。MySQL 8.0 的镜像解压后大概几百 MB,第一次拉需要点耐心。拉完用docker images看一下有没有mysql这个仓库名和对应的 TAG。

注意:加速地址是会失效的,网上抄来的地址某天突然不工作很正常。判断依据是docker pulldial tcp ... i/o timeout或者TLS handshake timeout。遇到这种情况先换地址,不要怀疑自己的命令写错了。

另外提一个容易被忽略的点:Docker Desktop 默认给虚拟磁盘的空间是有限的。MySQL 跑久了,binlog 和数据文件会慢慢吃掉空间,表现是容器突然写不进去、报磁盘满。在设置里把磁盘镜像的大小调大一些,或者养成定期清理的习惯(docker system df看占用,docker system prune清无用资源,但注意别把有用的卷一起删了)。

3. 一条 docker run 命令里,每个参数都在解决一个具体问题

网上流传的命令版本很多,但很多人是抄完就跑,压根不知道自己写了什么。我拆开讲一遍,这样出问题时你知道该改哪。

3.1 版本选择:先想清楚你要 8.0 还是 5.7

MySQL 官方镜像的 tag 就是版本号。mysql:latest目前指向 8.x 的某个小版本,但不建议在生产性质的场景里用 latest,因为某天你随手 pull 一下就升级了,行为可能变。写死mysql:8.0更稳妥。

5.7 在 2023 年 10 月已经停止官方维护了,如果是老项目要兼容,可以用mysql:5.7,但心里要清楚这是没有安全更新的分支。新项目直接上 8.0,别犹豫。

8.0 和 5.7 有几个会让新手懵的差异:默认认证插件变成了caching_sha2_password,默认字符集变成了utf8mb4,还多了个utf8mb4_0900_ai_ci的排序规则。第三点后面单独说。

3.2 端口映射:3306 冲突是最高频的翻车点

容器内部的 3306 和宿主机的端口是两回事。-p 3306:3306的意思是"把宿主机的 3306 转发到容器的 3306"。如果宿主机上本来就有个本机安装的 MySQL 在跑,这个映射会直接报port is already allocated

两个选择:要么把宿主机那个 MySQL 停掉/卸载掉,要么换个宿主机端口,比如-p 13306:3306。后者更灵活,Navicat 里主机填127.0.0.1、端口填13306就通了。我在同一台机器上跑多个版本做兼容测试时,就是这么干的:8.0 占 3306,5.7 占 13307,互不干扰。

排查端口占用,Windows 上用netstat -ano | findstr 3306,Linux/macOS 上用lsof -i :3306或者ss -lntp | grep 3306,找到 PID 再决定是杀进程还是换端口。

3.3 数据卷:不挂载就是给自己埋雷

这是我认为全篇最重要的一条。如果你不把/var/lib/mysql挂到宿主机目录,容器一删,你所有数据就没了。容器的文件系统是临时的,这句话不是吓唬人。

docker run -d \ --name mysql8 \ --restart unless-stopped \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=Root@123456 \ -e TZ=Asia/Shanghai \ -v /data/mysql8/data:/var/lib/mysql \ -v /data/mysql8/conf:/etc/mysql/conf.d \ -v /data/mysql8/logs:/var/log/mysql \ --memory=2g \ mysql:8.0 \ --character-set-server=utf8mb4 \ --collation-server=utf8mb4_0900_ai_ci

Windows 上路径写成-v D:/docker/mysql8/data:/var/lib/mysql,注意 Docker Desktop 需要你有权限访问这个盘符(设置里的 File Sharing)。macOS 上默认只能挂载用户目录下的路径,挂在/Users/xxx下面最省事。

挂载方式有命名卷和绑定挂载两种。-v mysql_data:/var/lib/mysql是命名卷,由 Docker 管理,省心但不好直接翻文件;-v /data/mysql8/data:/var/lib/mysql是绑定挂载,目录在哪你看得见,备份和迁移方便。我个人偏好绑定挂载,出问题时能直接冲进目录看日志和文件。

权限这块也有坑。Linux 上如果你用一个普通用户跑,挂载目录属主不对,容器会因为无法写入数据目录而反复重启,日志里能看到Permission denied或者Can't create/write to file。做法是把宿主机目录属主设成容器里 mysql 用户的 UID(官方镜像里是 999),或者干脆在测试环境允许容器以指定用户运行。

3.4 字符集和排序规则:utf8mb4 是底线

MySQL 里那个古老的utf8其实只支持三个字节,存不了 emoji,也存不了一些生僻字。新库一律用 utf8mb4,这是没有商量余地的。

在启动参数里加--character-set-server=utf8mb4 --collation-server=utf8mb4_0900_ai_ci只是把服务端默认值改了。建库的时候建议还是显式写一遍:

CREATE DATABASE demo DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_0900_ai_ci;

客户端连接层也有一层字符集,Navicat 里新建连接时可以指定"使用 UTF-8",JDBC 连接串里则要带characterEncoding=utf8。三层对不齐,表现就是中文变问号或者乱码。这个问题的本质是:服务端、表、连接,三处编码必须一致,缺一处都会出幺蛾子

另外提醒一句lower_case_table_names。这个参数决定表名是否大小写敏感,Linux 上默认是 0(敏感),Windows 和 macOS 上是 1。如果你从 Windows 开发环境迁到 Linux 容器里,Useruser会被当成两张表,直接报"表不存在"。这个参数必须在初始化数据目录之前设定,事后改会导致 MySQL 启动失败。所以如果你有跨平台迁移的打算,起容器时就加上--lower-case-table-names=1,并且写进配置文件。

3.5 认证插件:Navicat 连不上的头号原因

MySQL 8.0 默认用caching_sha2_password,新版 Navicat、MySQL Workbench、JDBC 8.x 驱动都支持,但一些旧客户端会握手失败,报错通常是 1251 或者 2059,提示Authentication plugin 'caching_sha2_password' cannot be loaded

两条路。一是升级客户端,这是首选,毕竟缓存式认证更安全。二是退回旧插件,在启动参数里加--default-authentication-plugin=mysql_native_password。要注意,MySQL 8.0.34 之后这个参数已经被标记为废弃,8.4 版本里换成了--mysql-native-password=ON。如果你用的是很新的镜像,参数名写错了容器会直接起不来,报 unknown variable。

更精细的做法是只给特定用户改插件,服务端默认值不动:

CREATE USER 'dev'@'%' IDENTIFIED WITH mysql_native_password BY 'Dev@123456'; GRANT ALL PRIVILEGES ON demo.* TO 'dev'@'%'; FLUSH PRIVILEGES;

这样既照顾了老客户端,也没把整个服务端降级。

3.6 用配置文件代替长命令,docker-compose 版

命令越敲越长不是好事,容易漏参数。我的习惯是把 MySQL 的配置写到conf/my.cnf

[mysqld] character-set-server=utf8mb4 collation-server=utf8mb4_0900_ai_ci default-time-zone='+08:00' max_connections=500 innodb_buffer_pool_size=1G lower_case_table_names=1 slow_query_log=1 long_query_time=2

这个文件挂到容器的/etc/mysql/conf.d/下面就会自动被读取,不需要改镜像里的主配置。

再用 docker-compose 管起来:

services: mysql8: image: mysql:8.0 container_name: mysql8 restart: unless-stopped ports: - "3306:3306" environment: MYSQL_ROOT_PASSWORD: Root@123456 TZ: Asia/Shanghai volumes: - ./data:/var/lib/mysql - ./conf:/etc/mysql/conf.d - ./logs:/var/log/mysql command: - --default-authentication-plugin=mysql_native_password

docker compose up -d起来,改配置就docker compose downup -d,比记一长串参数靠谱得多。

4. Navicat 连不上时的排查顺序,从下往上查比瞎试快

报错信息只是最后一道症状,根因可能在很下面一层。我一般按这个顺序走,基本三分钟能定位。

4.1 第一层:容器到底活着没有

docker ps -a

mysql8的状态是Up还是Exited。如果一直重启,看日志:

docker logs --tail 200 mysql8

常见的启动失败原因就那几个:挂载目录没权限、配置文件语法错、lower_case_table_names和数据目录不匹配、内存不够被 OOM 杀掉。日志里关键词很直白,Can't create/write to fileunknown variabledifferent lower_case_table_names settings,照着搜基本都能找到答案。

4.2 第二层:3306 在宿主机上通不通

容器活着不代表端口通。先在容器内部确认监听:

docker exec -it mysql8 bash # 进去之后 ss -lntp | grep 3306

然后再从宿主机试:

# Linux/macOS nc -vz 127.0.0.1 3306 # Windows PowerShell Test-NetConnection 127.0.0.1 -Port 3306

宿主机连不上,通常是三种情况:端口映射写错了(比如写了-p 3306:3307)、宿主机防火墙拦了、Docker Desktop 的网络代理配置有干扰。Windows 上还常见一个情况:装过某些安全软件,它会把回环地址上的非常规端口握手中断。

4.3 第三层:容器里能不能登进去

docker exec -it mysql8 mysql -uroot -p

能进去说明服务本身是好的,问题在客户端或网络。进不去说明密码不对或者用户权限有问题。注意官方镜像的 root 默认只允许从 localhost 登录,如果你从别的地方连,需要显式授权远程访问:

SELECT user, host, plugin FROM mysql.user; -- 如果 root 的 host 是 localhost,按需调整 CREATE USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY 'Root@123456'; GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' WITH GRANT OPTION; FLUSH PRIVILEGES;

注意:把 root 开放到%只适合本地开发环境。容器如果暴露在公网,这么干风险很高,正确做法是建一个权限受限的业务账号。

4.4 第四层:Navicat 报错码对照

到这一步基本就是客户端侧的问题了,报错码很有信息量:

报错码提示关键字常见根因处理方式
2003Can't connect to MySQL server端口不通、服务没起、主机填错回到第 2 层验证端口,确认主机填 127.0.0.1 而不是 localhost 解析异常
2013Lost connection during query连接被中断、超时设置过短检查网络与防火墙,Navicat 里调大超时
1045Access denied for user用户名密码错、来源 host 不匹配确认账号密码,检查mysql.user里的 host
1251Authentication plugin cannot be loaded客户端不支持 caching_sha2_password升级 Navicat,或改用 mysql_native_password 用户
2059Authentication plugin error同上,多见于旧版驱动同上
1049Unknown database库名不存在或大小写不匹配确认库里真实存在的库名,注意 lower_case_table_names
1130Host is not allowed to connect账号不允许从该主机连新建或修改账号的 host 为%

Navicat 这边还有两个细节。一是"主机名或 IP 地址"这一栏,我习惯填127.0.0.1,因为在某些 Windows 环境下localhost会优先解析成 IPv6 的::1,而 Docker 的端口映射默认在 IPv4 上,结果就是明明端口开着却连不上。二是连接属性里的"保持连接间隔",设成 240 秒左右,能避免长时间闲置后被服务端断开。

4.5 从 Navicat 侧确认成功的样子

连上之后,先在查询窗口跑一句:

SELECT VERSION(), @@character_set_server, @@collation_server, NOW();

VERSION()显示 8.0.x 说明连的是新容器不是残留的本机 MySQL(这个坑我踩过,连了半天其实是老服务),字符集显示 utf8mb4 说明编码配置生效了,NOW()如果比你的手表时间慢 8 小时,那就是时区问题,见下一节。

5. 容器里丢了数据,基本只能怪自己没备份

5.1 逻辑备份:mysqldump 是通用解

只要数据量不是特别夸张,mysqldump都是首选,因为产物是 SQL 文本,跨版本、跨平台都能用。

docker exec mysql8 sh -c \ 'exec mysqldump -uroot -p"$MYSQL_ROOT_PASSWORD" \ --single-transaction --default-character-set=utf8mb4 \ --databases demo' > /backup/demo_$(date +%F).sql

几个参数值得说明。--single-transaction让 InnoDB 表在一致性快照下导出,不锁表,业务能继续跑;--default-character-set=utf8mb4保证导出文件里中文不乱码,这个参数漏了是中文变问号的最常见原因;--databases会把CREATE DATABASE语句一起带出来,恢复时不用手动建库。

恢复:

docker exec -i mysql8 sh -c \ 'exec mysql -uroot -p"$MYSQL_ROOT_PASSWORD" --default-character-set=utf8mb4' \ < /backup/demo_2025-01-01.sql

注意这边用的是-i不是-it。加了-t分配伪终端,重定向进去的数据流会被搅乱,表现是恢复一半报语法错误。这个细节文档里一般不写,但坑过不少人。

5.2 物理备份与卷的取舍

直接把宿主机上的data目录打包,属于物理备份,快,但要求 MySQL 停掉或者处于干净关闭状态,否则 InnoDB 的文件处于不一致状态,恢复后大概率起不来。我的做法是:日常用 mysqldump,机器迁移或者大版本升级前,先停容器再打包整个目录,双保险。

还有一点,不要用docker volume rm去清理你不确定的卷。这个命令不带确认,删了就没了。清理前先docker volume ls看名字,确认不是数据库在用的那个。

5.3 升级的正确姿势

很多人升级就是docker pull mysql:8.4然后拿新镜像挂旧数据目录,结果容器起不来。MySQL 不支持降级,大版本升级也不能简单换镜像。

我的流程是这样:

  1. 用 mysqldump 完整导出所有库,确认文件大小正常。
  2. 记录当前版本:docker exec mysql8 mysql -uroot -p -e "SELECT VERSION();"
  3. 停掉旧容器,但保留数据目录不删,最好复制一份到别的路径作为回滚点。
  4. 起新版本容器,挂一个全新的空目录,让它初始化。
  5. 把备份 SQL 导入新容器。
  6. 业务侧改连接配置,验证通过后,旧目录再留几天才删。

小版本升级(比如 8.0.32 到 8.0.40)可以直接换镜像复用数据目录,MySQL 会自动处理。但同样建议先备份,因为升级过程中断电或者磁盘满,数据目录可能就废了。

5.4 日常会用到的运维命令

目的命令
看容器状态docker ps -a --filter name=mysql8
看实时日志docker logs -f --tail 100 mysql8
进容器命令行docker exec -it mysql8 bash
进 MySQL 客户端docker exec -it mysql8 mysql -uroot -p
重启服务docker restart mysql8
看资源占用docker stats mysql8
停机但保留容器docker stop mysql8
删容器但保留数据docker rm mysql8(前提是数据已挂载到宿主机)

最后一行是关键点:因为数据挂在宿主机目录,删容器是安全的。这是前面强调挂载的回报。

6. 几个只有真跑起来才会遇到的细节

6.1 初始密码和"我根本没设密码"的情况

如果你在docker run时忘了写MYSQL_ROOT_PASSWORD,官方镜像会拒绝启动,日志里明确告诉你必须提供其中一个环境变量。已经跑起来的容器想改密码,进客户端执行:

ALTER USER 'root'@'localhost' IDENTIFIED BY 'NewPass@123456';

刷新权限这步在 MySQL 8.0 里其实可以省,但养成习惯没坏处。

还有一种情况:用MYSQL_ALLOW_EMPTY_PASSWORD=yes起了个空密码容器,后来想加密码。改完之后记得同步更新 Navicat 里的连接配置,否则下次连接就是 1045。

6.2 时区差 8 小时,问题在容器不在 Navicat

MySQL 容器默认走 UTC,NOW()出来比北京时间慢 8 小时,插入的时间字段自然也是错的。解决方式有几种,最简单的是启动时加-e TZ=Asia/Shanghai,再加上配置里的default-time-zone='+08:00'。两个都做最保险,因为TZ影响的是容器系统时间,default-time-zone影响的是 MySQL 会话时区。

顺带说一个容易搞混的:NOW()返回的是会话时区的时间,UTC_TIMESTAMP()永远返回 UTC,timestamp类型会做时区转换,datetime不会。如果你的表里有datetime字段并期望它是北京时间,那服务端时区就必须配对,否则存进去什么就是什么,看着"没错",实际含义已经偏了。这个差异在做跨时区业务时非常要命,本地开发阶段先把时区理顺,能省掉后面大量对不上时间的数据排查。

6.3 内存和连接数,别让一个测试库拖垮机器

Docker Desktop 在 Windows/macOS 上是跑在一个虚拟机里的,默认给的内存可能是 2G 到 8G。MySQL 8.0 的innodb_buffer_pool_size默认值在容器里会按可用内存自适应,如果你的机器同时开着 IDE、浏览器一堆标签、再加几个容器,很容易触发 OOM,表现是容器毫无征兆地重启。

我的做法是给容器加--memory=2g限制,同时把innodb_buffer_pool_size显式设成 1G 左右,别让它自己乱猜。连接数用max_connections控制,本地开发 200 到 500 足够,Navicat 每次开连接池可能占掉好几个,设太小会出现"Too many connections"。

--restart unless-stopped这个参数我也建议加上。机器重启或者 Docker 重启之后,容器会自动回来,不用每次都手动docker start。用了它你才真正体会到"数据库像本机服务一样一直在那儿"的感觉。

6.4 Navicat 侧那些让效率翻倍的功能

连上之后,Navicat 本身还有几个能力值得用起来,尤其是做数据迁移和对比的时候。

一个是数据传输,能把一个连接里的库整表搬到另一个连接,跨版本迁移特别好使。源选旧容器、目标选新容器,勾选要迁的表,跑完就完事,比手写 SQL 省事。字符集不一致时它会做转换,但转换有损,迁移前后最好抽样比几行中文和 emoji。

另一个是结构同步,开发库和测试库的 DDL 不一致时,它能生成差异脚本。注意它生成的脚本只是建议,带索引变更和字段类型调整的语句一定要人工过一遍,尤其是DROP类的操作。

还有计划任务,可以配置定时把库导出成 SQL 文件,落到本地目录,相当于一个轻量备份。这个不能替代正式的备份策略,但作为开发环境的安全网足够了。

关于授权,Navicat 是商业软件,官方是有免费版可以用的(功能做了精简,日常连 MySQL 跑查询够用),也有官方的试用期。别去网上找来源不明的安装包和序列号,改过的客户端里塞东西是老问题了,为了省这点钱把开发机搭进去不划算。预算有限就用免费版或者换个开源客户端,功能上差别没大到影响开发。

6.5 一个关于"连的到底是不是我想的那个库"的习惯

我现在每次新建连接,第一件事就是在 Navicat 里跑SELECT VERSION(), @@hostname, @@port;@@hostname在容器里通常是容器 ID 的前几位,一眼就能确认连的是容器而不是本机残留的服务。这个习惯帮我避免过好几次"改了半天的配置其实改错了地方"的尴尬。

另外,连接名建议带上端口号,比如local-mysql8-3306test-mysql57-13307。本机同时跑好几个数据库实例的时候,Navicat 左侧一列连接看着都差不多,出了事容易连错环境执行错 SQL,一个清晰的名字能救命。

我个人在长期使用这套组合后的体会是:Docker 跑 MySQL 最大的价值不是"安装方便",而是环境可丢弃。想换个版本测兼容性,起个新容器挂个新目录,十分钟的事,测完直接删掉,宿主机上不留一丝痕迹。真正需要你花心思的地方只有三处——数据目录有没有挂出来、字符集有没有对齐、备份有没有真的跑通并验证过能恢复。这三件事做完,剩下的就是享受随时推倒重来的自由了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询