1. 为什么我坚持用 Docker 跑 MySQL,而不是直接装在本地?
MySQL 是我过去十年里写过最多 SQL、调过最多慢查询、也删库跑路(误)过最多次的数据库。它不是最炫的,但绝对是最“顺手”的——就像一把用了十年的瑞士军刀,刃口可能不如新刀锋利,但开瓶、拧螺丝、削铅笔,哪样都比刚领的工具包更趁手。而 Docker,就是给这把刀配了个可拆卸、可复制、可快照的战术鞘。这不是赶时髦,是实打实解决了一堆让我头皮发麻的老问题。
以前在团队里搭测试环境,最怕听到这句话:“你本地能跑,我这连不上。”一查,八成是“我装的是 MySQL 5.7,你用的是 8.0”,或者“我开了 binlog,你没开”,再或者“我改了 max_connections=500,你还是默认151”。这些配置散落在/etc/mysql/my.cnf、/usr/my.cnf、~/.my.cnf里,像三颗定时炸弹,谁碰谁倒霉。Docker 把整个运行时环境——包括 MySQL 二进制、配置文件、数据目录、甚至系统库版本——全部打包进一个镜像。你 pull 下来的mysql:8.2,和我 pull 下来的mysql:8.2,字节级一致。这不是“大概率一样”,是“必然一样”。这种确定性,对开发、测试、CI/CD 来说,价值远超节省那点内存。
还有数据。我见过太多人把数据库直接装在笔记本上,跑着跑着磁盘告急,一查发现/var/lib/mysql里堆了三年前的测试库,删又不敢删,留着又占空间。Docker 的 volume 机制,让数据和程序彻底解耦。你可以把test-mysql-data卷单独备份、迁移、甚至挂载到另一个容器里做数据校验,而不用动 MySQL 进程一根毫毛。这背后不是技术炫技,是运维思维的转变:数据是资产,程序是消耗品。
最关键的一点,是“可丢弃性”。我每天要起 3~5 个 MySQL 实例,分别对应不同分支的代码、不同客户的 schema 变更、不同版本的 ORM 兼容性测试。如果每个都要手动apt install、改配置、初始化、建库、授权……一天就过去了。现在?一条docker run命令,10 秒内一个干净、隔离、带预设密码和库名的实例就 ready。用完docker stop && docker rm,干干净净,不留一丝痕迹。这种“用完即焚”的轻量感,是传统部署方式永远给不了的。
所以,这篇文章不讲“Docker 是什么”、“MySQL 有多牛”,那些官网文档写得比我清楚一百倍。我要带你走一遍我每天真实在做的流程:从拉镜像开始,到最终跑起一个生产就绪(至少是开发就绪)的 MySQL 容器,中间每一步为什么这么写、参数怎么选、坑在哪、怎么填。你会看到的不是教科书式的命令罗列,而是我笔记本里贴着的、被咖啡渍染黄的速查笔记。
2. 核心设计思路:为什么这样组织你的 MySQL 容器?
很多人第一次跑 MySQL 容器,就卡在docker run -d mysql这一步。容器起来了,docker ps看着绿油油的,但一连就报错:“Can't connect to local MySQL server”。问题不在命令本身,而在对容器本质的理解偏差。Docker 不是虚拟机,它不给你一个“完整操作系统”,而是一个“进程沙盒”。MySQL 在里面跑,但它默认只监听容器内部的127.0.0.1:3306,对外部网络是完全封闭的。这就像你家客厅里开了台电脑,但没接网线,也没开 WiFi——它自己运行得好好的,但外面的人根本找不到它。
所以,整个配置的核心逻辑,就围绕三个关键词展开:暴露(Expose)、挂载(Mount)、持久化(Persist)。它们不是并列选项,而是一条必须严格遵循的因果链。
2.1 暴露:让 MySQL “被看见”
-p 3307:3306这个参数,是整条链的起点。它的含义不是“把容器端口映射到宿主机”,而是“在宿主机上开一个‘代理’,把所有打向localhost:3307的请求,原封不动地转发给容器里的3306端口”。这里有两个极易踩的坑:
第一,端口冲突。3306是 MySQL 默认端口,也是很多人的本地 MySQL 占着的。如果你强行docker run -p 3306:3306,Docker 会报错Bind for 0.0.0.0:3306 failed: port is already allocated。解决方案不是硬刚,而是换一个“空闲”的端口,比如3307、3308,甚至13306。我习惯用13306,因为一眼就能看出这是“Docker 版”,不会和本地服务混淆。
第二,bind-address配置。即使你-p映射成功了,如果容器内的 MySQL 配置了bind-address = 127.0.0.1,它依然只接受来自容器内部的连接,拒绝所有外部流量。这就是为什么很多教程强调要改配置文件。但其实,官方mysql镜像默认的bind-address是*(即监听所有接口),所以只要-p映射正确,mysql -h 127.0.0.1 -P 3307 -u root -p就一定能连上。这个细节,省去了新手第一步就去折腾my.cnf的麻烦。
提示:
-p参数的格式是宿主机端口:容器端口,顺序不能颠倒。写成-p 3306:3307是无效的,Docker 会静默忽略。
2.2 挂载:让配置“可编辑”
-v /host/path:/container/path是 Docker 的灵魂功能之一。对于 MySQL,我们主要挂载两个地方:配置文件目录和数据目录。但它们的目的截然不同。
挂载配置目录(如/etc/mysql/conf.d),是为了接管控制权。官方镜像启动时,会按顺序读取/etc/mysql/my.cnf→/etc/mysql/conf.d/*.cnf→/etc/mysql/mysql.conf.d/*.cnf。其中,conf.d目录下的.cnf文件具有最高优先级。这意味着,你不需要去修改镜像里自带的my.cnf,只需在宿主机上创建一个my-custom.cnf,然后把它挂载进去,你的配置就会生效。这种方式的好处是:配置文件独立于镜像,可以 git 管理、可以 diff 对比、可以一键回滚。我所有的项目,conf.d目录下都放着一个00-base.cnf,里面只有一行max_allowed_packet = 64M,这是为了防止导入大 SQL 文件时被截断。
挂载数据目录(/var/lib/mysql),则是为了实现持久化。这是下一节的重点,但这里要先破除一个迷思:挂载数据目录 ≠ 数据就安全了。如果挂载的是一个普通目录(如-v ./data:/var/lib/mysql),当容器被rm掉,这个./data目录还在,数据确实没丢。但问题是,这个目录的权限是宿主机用户的,而 MySQL 容器内运行的是mysql用户(UID 999)。当容器重启时,mysql用户可能没有权限读写这个目录,导致启动失败,报错mysqld: Can't create/write to file '/var/lib/mysql/is_writable' (Errcode: 13 - Permission denied)。这就是为什么官方强烈推荐使用docker volume,而不是绑定挂载。
2.3 持久化:让数据“不消失”
docker volume是 Docker 为数据持久化专门设计的抽象层。它和绑定挂载(bind mount)有本质区别:volume 是由 Docker 守护进程管理的,存储在宿主机的特定位置(Linux 下通常是/var/lib/docker/volumes/),Docker 会自动处理权限、生命周期和备份。当你执行docker volume create myapp-mysql-data,Docker 就在后台为你创建了一个受控的、安全的数据仓库。
这个仓库的“所有权”是明确的:它属于 Docker,不属于任何用户。因此,无论容器内以什么 UID 启动 MySQL,Docker 都能确保它对这个 volume 有完全的读写权限。这才是真正可靠的持久化方案。我所有的生产环境容器,数据目录一律用 volume;只有在需要快速调试、查看原始文件内容时,才会临时用绑定挂载。
这三个环节,构成了一个闭环:-p让你能连上,-v conf.d让你能管住它,-v volume让它掉电也不丢数据。少了任何一个,这个容器就只是个“玩具”,无法进入真实工作流。
3. 实操全流程:从零开始搭建一个可信赖的 MySQL 容器
现在,我们把上面的理论,变成键盘上敲出的每一行命令。我会以一个真实的、可立即复现的场景为例:为一个新项目搭建一个开发用的 MySQL 实例,要求它有自定义配置、数据持久化、且能被本地 IDE 和其他容器访问。整个过程,我保证不跳步、不省略、不假设你知道任何前置知识。
3.1 准备工作:确认 Docker 已就绪
在开始之前,请务必确认你的 Docker 环境是健康的。这不是形式主义,而是避免后续所有问题的基石。打开终端,依次执行以下三条命令:
# 1. 检查 Docker 守护进程是否运行 docker info > /dev/null 2>&1 && echo "✅ Docker daemon is running" || echo "❌ Docker daemon is NOT running" # 2. 检查是否有足够权限(尤其在 Linux 上) docker ps > /dev/null 2>&1 && echo "✅ You have permission to use Docker" || echo "❌ Permission denied. Try 'sudo docker ps' or add user to docker group" # 3. 检查镜像缓存(可选,但能加速后续步骤) docker images | grep mysql || echo "No MySQL images found. Will pull now."如果第一条命令报错,说明 Docker Desktop 没启动,或者 Linux 上dockerd服务没开。第二条报错,则意味着你的用户不在docker用户组里,需要执行sudo usermod -aG docker $USER并重新登录。这两步,我曾经在客户现场花了整整两小时才排查出来,教训深刻。
3.2 拉取并验证镜像:选择哪个版本?
官方 MySQL 镜像在 Docker Hub 上有上百个标签(tag),从5.7到8.4,还有latest、8、8.0等别名。latest听起来很诱人,但它指向的是最新发布的稳定版,可能包含你尚未适配的 breaking change。例如,MySQL 8.0 引入了新的默认认证插件caching_sha2_password,而很多老的 JDBC 驱动不支持它,会导致连接失败。
我的经验是:永远显式指定主版本号。如果你的项目代码明确要求 MySQL 5.7,就用mysql:5.7;如果是新项目,追求性能和新特性,就用mysql:8.2(截至本文写作时的最新 LTS 版本)。执行:
# 拉取 MySQL 8.2 镜像 docker pull mysql:8.2 # 查看已拉取的镜像,确认其存在且大小合理(约 500MB) docker images | grep mysql你会看到类似这样的输出:
mysql 8.2 7e8b5c4d3a2f 2 weeks ago 524MB这个7e8b5c4d3a2f就是镜像的 ID,它是该镜像在全球范围内的唯一指纹。记住它,后面排查问题时会用到。
3.3 创建初始容器:最小可行配置
现在,我们来运行第一个容器。目标是:让它能启动、能连上、密码可控。这是所有后续操作的基础。
# 创建一个名为 'dev-mysql' 的容器 docker run \ --name dev-mysql \ -e MYSQL_ROOT_PASSWORD=mysecretpassword123 \ -p 13306:3306 \ -d mysql:8.2逐个解析这个命令:
--name dev-mysql:给容器起个好记的名字。不要用mysql或db这种泛泛的名字,因为一个宿主机上可能同时跑着多个 MySQL 实例。-e MYSQL_ROOT_PASSWORD=...:设置 root 用户密码。这是强制要求的环境变量,没有它,容器会启动失败,并打印错误日志。密码强度没有硬性要求,但为了安全,建议至少 8 位,包含大小写字母和数字。-p 13306:3306:将宿主机的13306端口映射到容器的3306端口。如前所述,避免使用3306。-d:后台运行(detached mode)。没有它,终端会被容器日志霸占,你无法输入其他命令。
命令执行后,会返回一长串字符,比如a1b2c3d4e5f6...,这就是容器的 ID。接着,检查它是否真的在运行:
# 查看所有正在运行的容器 docker ps # 应该能看到一行,包含 'dev-mysql' 和 'Up X seconds' 的状态 # 如果看不到,说明启动失败,立刻看日志 docker logs dev-mysql如果docker logs dev-mysql输出中包含mysqld: ready for connections,恭喜,MySQL 服务已经启动成功。现在,用本地的 MySQL 客户端连接它:
# 使用本地安装的 mysql 客户端连接 mysql -h 127.0.0.1 -P 13306 -u root -p # 输入密码 'mysecretpassword123',你应该会看到 MySQL 的欢迎界面 # 然后执行一个简单查询验证 mysql> SELECT VERSION(); +-----------+ | VERSION() | +-----------+ | 8.2.0 | +-----------+ 1 row in set (0.00 sec)这一步成功,证明你的基础环境完全 OK。接下来,我们就要给它“加装装备”了。
3.4 挂载配置文件:定制你的 MySQL 行为
现在,我们的dev-mysql容器是“裸奔”状态,用的是官方镜像的默认配置。默认配置对学习够用,但对开发或生产,远远不够。比如,默认的max_connections是 151,对于一个并发稍高的应用,很快就会连接池耗尽;默认的wait_timeout是 28800 秒(8 小时),可能导致大量空闲连接长期占用资源。
我们需要一个自定义的my.cnf。首先,停止并删除当前的容器,因为配置只能在容器创建时挂载:
docker stop dev-mysql && docker rm dev-mysql然后,在宿主机上创建一个目录来存放配置文件:
# 创建配置目录(Linux/macOS) mkdir -p ~/docker-mysql/conf.d # 创建配置文件 cat > ~/docker-mysql/conf.d/custom.cnf << 'EOF' [mysqld] # 基础安全 skip-host-cache skip-name-resolve # 连接与超时 max_connections = 200 wait_timeout = 600 interactive_timeout = 600 # 字符集(强烈推荐,避免中文乱码) character-set-server = utf8mb4 collation-server = utf8mb4_unicode_ci # 日志(开发时很有用) log-error = /var/log/mysql/error.log slow_query_log = 1 slow_query_log_file = /var/log/mysql/slow.log long_query_time = 2 # 性能相关 innodb_buffer_pool_size = 256M innodb_log_file_size = 64M EOF这段配置解释如下:
skip-host-cache和skip-name-resolve:禁用 DNS 反向解析,能显著加快连接速度,尤其是在网络环境不佳时。这是线上环境的标配。max_connections = 200:将最大连接数提升到 200,足够应付大多数开发场景。character-set-server = utf8mb4:这是关键!utf8在 MySQL 中实际是utf8mb3,不支持 emoji 和部分生僻汉字。utf8mb4才是真正的 UTF-8。collation-server设为utf8mb4_unicode_ci,提供更准确的中文排序和比较。slow_query_log:开启慢查询日志,long_query_time = 2表示执行时间超过 2 秒的查询会被记录。这对性能调优至关重要。
创建好文件后,用-v参数将其挂载到容器的/etc/mysql/conf.d目录:
docker run \ --name dev-mysql \ -e MYSQL_ROOT_PASSWORD=mysecretpassword123 \ -p 13306:3306 \ -v ~/docker-mysql/conf.d:/etc/mysql/conf.d \ -d mysql:8.2再次docker logs dev-mysql,你应该能看到 MySQL 启动时加载了这个配置文件的日志。连接上去,验证配置是否生效:
mysql -h 127.0.0.1 -P 13306 -u root -p -e "SHOW VARIABLES LIKE 'max_connections';" # 输出应为 200 mysql -h 127.0.0.1 -P 13306 -u root -p -e "SHOW VARIABLES LIKE 'character_set_server';" # 输出应为 utf8mb43.5 挂载数据卷:让数据坚如磐石
最后一步,也是最重要的一步:数据持久化。我们不再使用-v ./data:/var/lib/mysql这种危险的绑定挂载,而是创建一个专用的 Docker volume。
# 创建一个名为 'dev-mysql-data' 的 volume docker volume create dev-mysql-data # 停止并删除旧容器 docker stop dev-mysql && docker rm dev-mysql # 用 volume 启动新容器 docker run \ --name dev-mysql \ -e MYSQL_ROOT_PASSWORD=mysecretpassword123 \ -p 13306:3306 \ -v ~/docker-mysql/conf.d:/etc/mysql/conf.d \ -v dev-mysql-data:/var/lib/mysql \ -d mysql:8.2注意-v dev-mysql-data:/var/lib/mysql这一行。dev-mysql-data是 volume 的名字,/var/lib/mysql是容器内 MySQL 数据的绝对路径。Docker 会自动将这个 volume 初始化为一个空的 MySQL 数据目录。
现在,你可以放心地创建数据库、导入数据了。为了验证持久化是否有效,我们来做个破坏性测试:
# 1. 连接并创建一个测试库 mysql -h 127.0.0.1 -P 13306 -u root -p -e "CREATE DATABASE test_persistence;" # 2. 查看 volume 的内容(Linux/macOS) sudo ls -l /var/lib/docker/volumes/dev-mysql-data/_data/ # 你应该能看到 'test_persistence' 这个目录 # 3. 删除容器 docker stop dev-mysql && docker rm dev-mysql # 4. 用完全相同的命令,重新创建一个同名容器 docker run \ --name dev-mysql \ -e MYSQL_ROOT_PASSWORD=mysecretpassword123 \ -p 13306:3306 \ -v ~/docker-mysql/conf.d:/etc/mysql/conf.d \ -v dev-mysql-data:/var/lib/mysql \ -d mysql:8.2 # 5. 再次连接,检查库是否还在 mysql -h 127.0.0.1 -P 13306 -u root -p -e "SHOW DATABASES LIKE 'test_persistence';" # 输出应该显示 'test_persistence',证明数据完好无损。这个测试,是我每次给新同事培训时必做的。它直观地展示了 volume 的魔力:容器是易失的,数据是永恒的。
4. 进阶实战:用 Docker Compose 管理复杂应用栈
当你的项目不再只是一个孤零零的 MySQL,而是需要搭配一个 Web 应用(比如用 Flask 写的 API)、一个 Redis 缓存、一个 Nginx 反向代理时,docker run命令就会变得无比冗长和难以维护。想象一下,你要同时管理 5 个容器,每个都有自己的-p、-v、-e,还要确保它们在同一个网络里通信……这已经不是运维,是行为艺术。
Docker Compose 就是为此而生的。它用一个 YAML 文件,声明式地定义整个应用栈。下面,我就以一个最典型的“Web + MySQL”组合为例,展示如何用docker-compose.yml替代一长串docker run。
4.1 编写 docker-compose.yml:一份声明,全局生效
在你的项目根目录下,创建一个名为docker-compose.yml的文件。内容如下:
version: '3.9' services: # MySQL 服务 db: image: mysql:8.2 container_name: app-mysql restart: unless-stopped environment: MYSQL_ROOT_PASSWORD: mysecretpassword123 MYSQL_DATABASE: app_production MYSQL_USER: app_user MYSQL_PASSWORD: app_user_password ports: - "13306:3306" volumes: - mysql-data:/var/lib/mysql - ./docker-mysql/conf.d:/etc/mysql/conf.d healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-pmysecretpassword123"] timeout: 20s retries: 10 start_period: 40s # Web 应用服务(以一个简单的 Python HTTP 服务器为例) web: image: python:3.11-slim container_name: app-web restart: unless-stopped depends_on: db: condition: service_healthy volumes: - .:/app working_dir: /app command: python3 -m http.server 8000 ports: - "8000:8000" # 定义数据卷 volumes: mysql-data:这份配置文件,信息量巨大,我们逐段解读:
version: '3.9':指定了 Compose 文件的语法版本。3.9是目前最稳定、功能最全的版本。services:定义了所有要启动的服务。这里我们定义了db(MySQL)和web(Python Web 服务器)两个服务。db服务的environment:除了MYSQL_ROOT_PASSWORD,还额外设置了MYSQL_DATABASE(启动时自动创建的数据库名)、MYSQL_USER和MYSQL_PASSWORD(创建一个非 root 的普通用户)。这是最佳实践,生产环境绝不应该用 root 连接应用。healthcheck:这是 Compose 的高级功能。它定义了一个探针,定期执行mysqladmin ping命令来检查 MySQL 是否真的“健康”(即能响应查询),而不仅仅是进程在运行。depends_on中的condition: service_healthy就依赖于此,确保web服务只在db真正就绪后才启动。没有这个,web可能会因为db还在初始化就去连接,导致启动失败。volumes:在文件末尾统一定义了mysql-data这个 volume。它会在docker-compose up时自动创建,无需手动docker volume create。
4.2 启动与管理:三行命令,掌控全局
有了这个文件,整个应用栈的启停就变得极其简单:
# 1. 启动所有服务(-d 表示后台运行) docker-compose up -d # 2. 查看所有服务的状态 docker-compose ps # 输出会清晰地列出每个服务的名称、状态、端口映射 # Name Command State Ports # ----------------------------------------------------------------- # app-mysql docker-entrypoint.sh ... Up 2 seconds 3306/tcp, 0.0.0.0:13306->13306/tcp # app-web python3 -m http.server ... Up 2 seconds 0.0.0.0:8000->8000/tcp # 3. 查看某个服务的日志(实时跟踪) docker-compose logs -f dbdocker-compose logs -f db是我排查问题的第一利器。它会实时输出 MySQL 的启动日志,从初始化、创建数据库、到最终ready for connections,一目了然。如果启动失败,错误信息也会在这里第一时间出现。
停止服务同样简单:
# 停止并删除所有容器、网络,但保留 volume(数据还在!) docker-compose down # 如果你想连 volume 一起删掉(慎用!),加 --volumes 参数 docker-compose down --volumes4.3 网络通信:容器之间如何“打电话”
在 Compose 中,所有定义在同一个docker-compose.yml文件里的服务,会自动加入一个名为<project-name>_default的私有 Docker 网络。在这个网络里,服务名就是 DNS 主机名。
这意味着,你的web服务,如果要用 Python 连接 MySQL,它的数据库连接字符串(connection string)应该是:
# Python 代码中的连接字符串 DATABASE_URL = "mysql+pymysql://app_user:app_user_password@app-mysql:3306/app_production"注意这里的host是app-mysql,而不是127.0.0.1或localhost。127.0.0.1在web容器里,指的是web自己的回环地址,不是db。app-mysql这个名字,正是我们在docker-compose.yml里给db服务定义的container_name。
你可以用docker-compose exec进入web容器,手动测试这个连接:
# 进入 web 容器的 shell docker-compose exec web sh # 在容器内,尝试用 mysql 客户端连接 db 服务 # 注意:这里用的是 'app-mysql' 作为 host,端口是 3306(容器内端口) mysql -h app-mysql -P 3306 -u app_user -papp_user_password app_production -e "SELECT 'Connection OK';"如果输出Connection OK,恭喜,你的服务间网络已经打通。这是微服务架构的基石,也是 Docker Compose 最强大的地方。
5. 常见问题与独家避坑指南:那些文档里不会写的真相
在过去的几百次 MySQL 容器部署中,我总结出了一套“血泪经验”。这些问题,往往不会出现在官方文档的 FAQ 里,但却是新手最容易栽跟头的地方。我把它们整理成一张速查表,并附上最直接的解决方案。
5.1 连接被拒绝(Connection refused)
现象:mysql -h 127.0.0.1 -P 13306 -u root -p报错ERROR 2003 (HY000): Can't connect to MySQL server on '127.0.0.1' (111)。
排查思路与解决方案:
- 检查容器是否在运行:
docker ps | grep dev-mysql。如果没看到,说明容器启动失败,立刻docker logs dev-mysql。 - 检查端口映射是否正确:
docker port dev-mysql。输出应该是3306/tcp -> 0.0.0.0:13306。如果不是,说明-p参数写错了。 - 检查 MySQL 是否真的在监听:进入容器
docker exec -it dev-mysql bash,然后执行netstat -tlnp | grep :3306。如果没有任何输出,说明 MySQL 进程没起来,或者bind-address配置错误。 - 终极检查:在宿主机上,用
telnet 127.0.0.1 13306。如果连接失败,说明端口没通;如果连接成功(光标闪烁),说明端口是通的,问题出在 MySQL 认证上(比如密码错了)。
5.2 权限被拒绝(Permission denied)
现象:容器启动失败,docker logs dev-mysql显示mysqld: Can't create/write to file '/var/lib/mysql/is_writable' (Errcode: 13)。
原因与解决方案: 这是绑定挂载(bind mount)的典型陷阱。你用了-v ./data:/var/lib/mysql,但./data目录的所有者是你的宿主机用户(比如 UID 1000),而 MySQL 容器内运行的是mysql用户(UID 999)。UID 不匹配,导致权限不足。
正确做法:永远使用docker volume。如前所述,docker volume create myapp-data创建的 volume,Docker 会自动处理好所有权限问题。如果你非要用绑定挂载(比如为了方便查看文件),请在创建目录时,显式指定 UID:
# 创建目录,并将其所有者设为 UID 999(mysql 用户的 UID) sudo mkdir -p ./mysql-data sudo chown -R 999:999 ./mysql-data5.3 中文乱码(Garbled Chinese)
现象:插入中文后,查询出来是????,或者 ``。
原因与解决方案: 这几乎 100% 是字符集配置问题。你需要确保四个地方的字符集都统一为utf8mb4:
- MySQL 服务器:通过
my.cnf设置character-set-server = utf8mb4。 - MySQL 数据库:创建数据库时指定
CREATE DATABASE mydb CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci;。 - MySQL 表:创建表时指定
ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;。 - 客户端连接:在连接字符串中,添加
charset=utf8mb4参数。例如,Python 的 PyMySQL 连接字符串是pymysql.connect(..., charset='utf8mb4')。
最简单的一劳永逸方案,就是在my.cnf的[client]和[mysqld]两个 section 下,都加上default-character-set = utf8mb4。
5.4 容器启动后立即退出(Exited immediately)
现象:docker ps -a显示容器状态是Exited (1) 2 seconds ago。
原因与解决方案: 这是 Docker 新手的“头号杀手”。根本原因是:容器的主进程(PID 1)退出了,Docker 就认为容器完成了使命,自动关闭。
对于 MySQL,主进程就是mysqld。它退出,通常是因为初始化失败。最常见的初始化失败原因有:
- 数据目录不为空,且格式不兼容:比如你之前用
mysql:5.7创建了一个 volume,现在想用mysql:8.2启动,但 8.2 无法识别 5.7 的数据文件格式。解决方案:docker volume rm your-volume-name,然后重新docker-compose up。 - 配置文件语法错误:
my.cnf里多了一个逗号,或者少了一个等号。MySQL 启动时会解析失败并退出。解决方案:docker run -it --rm -v $(pwd)/conf.d:/etc/mysql/conf.d mysql:8.2 mysqld --verbose --help,这条命令会尝试启动 MySQL 并打印详细错误,其中就包含配置文件的语法错误提示。 - 内存不足:
innodb_buffer_pool_size设置得太大,超过了宿主机可用内存。解决方案:降低该值,或增加宿主机内存。
5.5 如何备份和恢复数据?
备份:最可靠的方式,是使用mysqldump工具,它能生成标准的 SQL 文件。
# 备份整个数据库(需要在宿主机上执行) docker exec dev-mysql mysqldump -u root -pmysecretpassword123 app_production > backup.sql # 备份单个表 docker exec dev-mysql mysqldump -u root -pmysecretpassword123 app_production users > users_backup.sql恢复:将 SQL 文件导入容器。
# 恢复(需要在宿主机上执行) docker exec -i dev-mysql mysql -u root -pmysecretpassword123 app_production < backup.sql注意:
mysqldump生成的 SQL 文件,包含了CREATE DATABASE语句。如果你只想恢复到一个已存在的数据库,记得先用文本编辑器删掉文件开头的CREATE DATABASE和USE语句,否则会报错。
这是我每天都在用的流程,没有花哨的自动化脚本,只有最朴实、最可靠的命令。它可能不够“云原生”,但足够让你在任何一台有 Docker 的机器上,10 分钟内搭起一个可信赖的 MySQL 开发环境。