☰
Docker部署MySQL从入门到实战:数据卷、端口映射与权限排查
2026/10/3 3:27:59 网站建设 项目流程

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.4MySQL 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 images

docker 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-server

docker 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 ...

解决思路分两步:

  1. 优先升级客户端工具或者数据库驱动版本。
  2. 如果只是本地开发环境,保证网络可信的前提下,可以在连接参数里关闭 SSL。

以 mysql 自带的命令行客户端为例:

mysql -h 127.0.0.1 -P 3306 -uroot -p --ssl-mode=DISABLED

DBeaver 的连接设置里也有 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 filebind 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 本身不复杂,复杂的是把数据持久化、权限控制、备份恢复、连接兼容性这些底层问题想清楚。把这篇文章里的关键命令亲手敲一遍,再故意删掉容器重建一次,体验一下数据还在的感觉,你对容器和数据卷的理解就会完全不同。

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

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

立即咨询