1. 为什么我把MySQL搬进了Docker:Mac本地安装的四个真实痛点
先说说我自己的经历。早几年我用Mac做开发,项目里需要MySQL,第一反应肯定是去官网下个dmg安装包,或者用Homebrew执行一条brew install mysql。听起来很简单对吧?但真正操作起来,你会发现事情远没那么顺利。
第一个痛点是版本管理混乱。今天这个项目要5.7,明天那个项目要8.0,本机装了一个版本之后,想再装另一个版本就得很小心地处理端口、数据目录、配置文件,稍不留神两个版本的数据目录就冲突了。更别提有时候跑着跑着,因为系统升级或者依赖库更新,MySQL服务突然起不来了,排查半天发现是某个动态库版本对不上,那种挫败感真是无法形容。
第二个痛点是安装过程脏乱差。dmg安装包会把一堆文件散落到/usr/local、/Library/LaunchDaemons这些目录里。等你不需要这个版本了,想彻底卸载干净,那可是个费时费力的工程。Homebrew稍微好一点,但也经常因为依赖关系拉进来一堆你根本用不上的库,直接把系统环境搞乱。
第三个痛点是升级风险高。Mac系统一旦做大版本升级,比如从macOS 13升到14,原来好好的MySQL服务很可能就起不来了。原因可能是启动脚本路径变了,也可能是因为系统自带的库文件变动导致兼容性问题。这种问题特别难查,因为错误日志往往语焉不详,你只能反复尝试各种网上搜来的偏方,能不能修好全看运气。
第四个痛点是隔离性为零。本机MySQL一旦出问题,所有依赖它的本地项目全部瘫痪。你正在调一个接口,突然连接数据库报错,结果发现是另一个项目的人改了本机MySQL的配置,这种事在团队协作里太常见了。
后来我开始把数据库这类依赖型基础服务全部迁到Docker里面跑,这些问题基本一次性解决了。Docker容器就像一个个独立的集装箱——每个容器里运行着各自版本的MySQL,互不干扰;想换版本就换镜像重开一个容器;不用了就直接把容器删掉,一点痕迹都不留。
这篇文章我把整个流程完整地展开讲,从Docker Desktop安装、镜像选择、容器启动参数拆解,到连接验证、数据持久化、常见问题排查,再到docker-compose编排和一主一从测试环境搭建。适合第一次接触Docker的初学者,也适合已经在用Docker但MySQL容器总是跑不顺的朋友。我会把那些官网文档里不会写的坑、报错信息的真实含义、以及我实际操作中的验证结果,都一次性交代清楚。
2. Docker Desktop先装对跑稳:从下载到启动的完整闭环
2.1 下载前必须先确认的一件事:芯片架构
现在Mac分两种CPU架构——Apple Silicon(M1、M2、M3系列)和Intel。这个区别比你想的更重要,因为Docker Desktop针对这两种架构提供了不同的安装包,镜像拉取时也会区分arm64和amd64两种平台版本。
查看方法很简单:点屏幕左上角的苹果图标,选“关于本机”,看“芯片”那一栏。如果是“Apple M1”之类,就是ARM架构;如果是“Intel Core i7”之类,就是x86架构。从2020年底之后苹果新出的机器基本都是Apple Silicon,但仍有部分用户在用Intel老款Mac。
Docker官方下载页的链接这里不放了,你自己搜索“Docker Desktop download”就能找到。下载的时候注意选择对应的芯片版本——页面上会明确标注“Apple Silicon”和“Intel Chip”两个按钮,别点错。点错的情况其实挺常见,下载完双击dmg发现提示“无法打开”或者“架构不匹配”,就说明你拿错包了。
2.2 安装到启动的完整操作步骤
下载完成之后,安装过程本身并不复杂,但有几个细节容易出问题。
第一,把Docker.app拖进Applications文件夹后,第一次启动时系统会弹窗提示“无法打开,因为无法验证开发者”。这个并不是安装失败,而是macOS的Gatekeeper安全机制在拦截。解决办法就是去“系统设置”→“隐私与安全性”,往下翻,看到“仍要打开”的按钮,点它,再确认一次,应用就能正常启动了。
第二,打开Docker Desktop之后,它会在菜单栏显示一个小鲸鱼图标,同时应用窗口会显示Docker Engine正在启动的状态。第一次启动通常比较慢,因为要初始化虚拟机内核和网络栈,等1到2分钟都算正常。如果窗口一直停在“Docker Engine starting...”超过5分钟,多半是虚拟化层出了问题。这时候看菜单栏鲸鱼图标有没有变化,右键点它会显示“Restart”等选项,尝试重启,或者直接重启整个Mac,大部分情况下都能解决。
第三,启动成功后,在终端里跑一下docker --version和docker info验证环境是否可用。能返回版本号就说明Docker CLI已正常工作。docker info会显示更详细的信息,包括容器数量、镜像数量、存储驱动类型、操作系统平台等。
2.3 资源配额和开机自启的设置建议
Docker Desktop安装完成后,我建议你进一次Settings,把两个项目调整一下。
内存配额:在“Settings”→“Resources”→“Advanced”里,有一个Memory滑块。默认值通常是2GB,但对MySQL这类需要稳定内存的数据库来说,2GB很容易捉襟见肘。我的建议是给Docker分配4GB以上,如果你的Mac内存是16GB,放心分8GB给Docker。MySQL 8.0默认的innodb_buffer_pool_size是128MB,容器本身跑起来不算特别吃内存,但加上系统其他开销,给足配额才能避免运行一段时间后因为资源不足被强制杀掉。
开机自启:在“Settings”→“General”里勾选“Start Docker Desktop when you sign in”。这样一开机Docker就在后台跑起来了,省的每次开发前手动点开。实际工作中,因为忘了启动Docker导致连接不上数据库,然后排查半天才想起来这事,真的会让人崩溃。
3. 启动MySQL容器:镜像选择、参数逐个拆解、5分钟跑起来
3.1 镜像版本到底怎么选:8.0还是5.7
进到这一步,Docker环境已经没问题了,接下来就要拉MySQL镜像。打开终端,直接执行:
docker pull mysql:8.0我为什么推荐8.0而不是最新标签mysql:latest?因为latest指向的通常是8.x的最新小版本,版本号会随着官方发布不断变化。你这次拉的是8.0.36,三个月后再拉可能就是8.0.39,虽然8.0系列内部兼容性基本没问题,但对于需要稳定复现的环境来说,固定到一个明确的小版本更稳妥。如果你手头有老项目依赖5.7,那就用docker pull mysql:5.7,这两个镜像都能直接拉。
说到5.7,顺便提一句:MySQL 5.7在2023年10月已经官方停止维护,不再有安全更新。除非你的项目确实因为兼容性原因锁死在5.7,否则新项目建议直接上8.0。网上还有大量教程在教5.7的安装配置,不是说不能用,而是你得清楚自己在用什么。
3.2 docker run命令逐项拆解
镜像拉下来之后,重点来了——启动容器的那条命令。我把完整的命令写出来,然后逐项解释每个参数到底是什么意思。
docker run -d \ --name mysql-dev \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=123456 \ -e MYSQL_CHARACTER_SET_SERVER=utf8mb4 \ -e MYSQL_COLLATION_SERVER=utf8mb4_unicode_ci \ -v ~/docker/mysql-data:/var/lib/mysql \ --restart=always \ mysql:8.0逐个拆解:
-d:后台模式运行容器,终端不会被占用。不加这个参数的话,容器会以前台方式运行,日志直接刷在终端上,Ctrl+C就停止容器了。--name mysql-dev:给容器起个名字。后续docker stop mysql-dev、docker exec -it mysql-dev bash这些操作都要用这个名字,所以取一个好认的名字很重要。-p 3306:3306:端口映射。冒号左边是宿主机端口,右边是容器内端口。因为MySQL默认监听3306,所以右边固定3306,左边可以按照宿主机实际情况改。比如宿主机3306已经被占用,就改成-p 3307:3306,外部访问时使用3307端口。这一点在后面的排错章节还会详细展开。-e MYSQL_ROOT_PASSWORD=123456:设置MySQL root用户的初始密码。首次启动容器时,MySQL会初始化数据目录并创建root用户,密码就是这个值。如果容器已经初始化过,这个变量再改也不会生效,因为密码已经写入数据目录了。-e MYSQL_CHARACTER_SET_SERVER=utf8mb4和-e MYSQL_COLLATION_SERVER=utf8mb4_unicode_ci:这两个环境变量用于设置数据库默认字符集和排序规则。用utf8mb4而不是utf8,是因为utf8在MySQL里实际上是utf8mb3,只能存储基本多语言平面字符,存不了emoji和一些特殊汉字。既然都上8.0了,直接utf8mb4不会错。-v ~/docker/mysql-data:/var/lib/mysql:数据目录挂载。这是整个命令里我认为最重要的一个参数,后面单独用一节来讲。--restart=always:设置自动重启策略。Docker服务重启时,这个容器会自动跟着启动,省去手动操作的麻烦。Docker Desktop本身设置开机启动后,配合这个参数,MySQL服务基本上就实现了开机即用。
3.3 启动后的验证三步走
命令执行后,先用docker ps查看容器状态。如果STATUS列是Up,说明容器在运行。如果看到Exited,说明启动失败,可以用docker logs mysql-dev查看详细日志。
然后进入容器内部,用MySQL自带的客户端验证数据库本身工作正常:
docker exec -it mysql-dev mysql -uroot -p输入密码后如果能进入mysql>提示符,说明数据库连接正常。在这个提示符里执行SELECT VERSION();可以看到MySQL版本号,执行SHOW DATABASES;可以看到初始的数据库列表。
第三步是从宿主机验证端口映射。新开一个终端窗口,执行mysql -h127.0.0.1 -P3306 -uroot -p。如果宿主机已经装了MySQL客户端,这一步能直接连上;如果没装也没关系,用后面第4章讲的GUI工具来验证也行。
4. 连不上、密码错、时区乱:容器启动后的高频问题与排查链路
容器能跑起来只是个开始。我在实际使用过程中遇到过不少连接层面的问题,这些问题在官方文档里并不会专门写,但几乎每个人都会碰到其中一个。我按排查链路来梳理,这样以后你遇到类似问题,会知道该往哪个方向找。
4.1 宿主机3306端口被占用的排查方法
启动容器时如果报错port is already allocated,或者容器状态反复重启,日志里能看到bind: address already in use,那就是宿主机3306端口已经被别的进程占用了。
可能的原因主要有两个:第一个是你之前在本机装过MySQL或者MariaDB,服务还在运行;第二个是其他容器已经映射了3306端口,比如之前用Docker装过一个MySQL容器,忘了删掉,新容器再绑定同一个端口自然失败。
排查命令如下:
lsof -i :3306这条命令会列出占用3306端口的进程。如果COMMAND列显示mysqld,说明宿主机确实有MySQL在跑。解决办法要么停掉那个服务,要么改Docker端口映射。我个人更倾向于改端口映射,毕竟本机残留的MySQL说不准哪天还要用。改法很简单,把-p 3306:3306改成-p 3307:3306,然后重新创建容器:
docker rm -f mysql-dev docker run -d --name mysql-dev -p 3307:3306 -e MYSQL_ROOT_PASSWORD=123456 -v ~/docker/mysql-data:/var/lib/mysql mysql:8.0这种情况下,后续所有客户端连接都要用3307端口。
4.2 密码验证插件不兼容:caching_sha2_password引发的连接失败
这个坑我相信每个用MySQL 8.0的人都踩过。症状是:容器正常启动、端口映射没问题、用docker exec进容器连MySQL也没问题,但宿主机上的旧版MySQL客户端连的时候报错:
Authentication plugin 'caching_sha2_password' cannot be loaded:原因很简单——MySQL 8.0默认的身份认证插件是caching_sha2_password,而太老的客户端(比如5.x时代的libmysqlclient)不认识这个插件,自然就握手失败。
解决办法有三种:
第一种,给新建的MySQL用户指定使用旧的mysql_native_password插件:
CREATE USER 'app'@'%' IDENTIFIED WITH mysql_native_password BY 'app_password'; GRANT ALL PRIVILEGES ON *.* TO 'app'@'%'; FLUSH PRIVILEGES;第二种,给root用户改认证插件:
ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY '123456'; FLUSH PRIVILEGES;第三种,干脆换新版客户端。比如升级到MySQL 8.x的客户端,或者使用支持新认证插件的GUI工具。
我个人的建议是:如果你的应用代码是自己控制的,优先考虑升级客户端驱动;如果是和别的团队共享服务、没法控制客户端版本,那就用第一种方法给应用单独建一个兼容旧插件的用户,而不是把root改成旧的认证方式。
4.3 时区问题:容器时钟和宿主机差8个小时
启动MySQL容器后,如果你执行SELECT NOW();发现时间不对,比本地时间晚了8个小时,那多半是容器默认使用了UTC时区。
Docker官方MySQL镜像的/etc/mysql/conf.d/目录里没有默认时区配置,容器内的时间环境继承了镜像构建时的UTC设置。要解决这个问题,最干净的方式是在创建容器时加一个环境变量:
-e TZ=Asia/Shanghai如果你容器已经启动了,也可以通过docker exec临时设置,但重启后就会丢失。还是建议把容器删了按正确的参数重建:
docker rm -f mysql-dev docker run -d --name mysql-dev \ -e TZ=Asia/Shanghai \ -e MYSQL_ROOT_PASSWORD=123456 \ -p 3306:3306 \ -v ~/docker/mysql-data:/var/lib/mysql \ mysql:8.0时区不一致在开发阶段可能看不出什么问题,但如果你用NOW()函数写时间字段,或者做日志分析、数据统计,时间差8小时会导致数据错位,排查起来特别费劲。
4.4 容器反复重启的日志排查链路
容器起不来的原因很多,但排查路径是固定的。第一步先看容器状态和执行日志:
docker ps -a docker logs mysql-devdocker logs输出的日志包含MySQL初始化和启动过程的全部信息。最常见的失败原因有两个:一是数据目录权限不对,二是配置参数冲突。
数据目录权限问题很好认,日志里会有chown: changing ownership of '/var/lib/mysql/': Permission denied或者mysqld: Can't create/write to file这类报错。这是因为挂载到本地的~/docker/mysql-data目录的所有者不是容器内mysql用户。最简单的解决办法是把数据目录改个权限:
chmod -R 777 ~/docker/mysql-data更规范一些的做法是找到容器内mysql用户的UID和GID:
docker run --rm mysql:8.0 id mysql输出类似uid=999(mysql) gid=999(mysql),然后宿主机上执行chown -R 999:999 ~/docker/mysql-data。这样权限就对了。
5. 数据不能丢:目录挂载、容器备份与恢复的实操方案
5.1 为什么必须做数据目录挂载
回到第3章那个-v参数。不夸张地说,这是运行数据库容器和运行一个普通应用容器最大的区别:容器可以随时删掉重建,但数据必须留在宿主机上。
Docker容器本身是“无状态”的设计哲学——你可以把容器想象成一个一次性的工作台,用坏了就扔,重新换一个。但数据库存储的数据是核心资产,一旦工作台被销毁,里面的东西就全没了。
如果你创建MySQL容器时没有挂载数据目录(也就是不加-v参数),所有数据库文件都会写在容器可写层里。这个层和容器生命周期绑定,docker rm删除容器后,数据就彻底丢失了。即使不删容器,Docker对容器可写层也有大小限制,尤其是数据量增长较快时,可能直接导致容器写入失败。
挂了-v ~/docker/mysql-data:/var/lib/mysql之后,容器内MySQL的数据文件实际写入到宿主机~/docker/mysql-data目录。这时候删掉容器重新创建一个相同挂载的新容器,数据仍然在,新容器启动后所有库表都还在。
5.2 逻辑备份和恢复的完整命令
数据目录挂载解决的是“容器生命周期”层面的持久化,但它不是银弹。如果你的宿主机硬盘坏了、~/docker/mysql-data目录被误删了,那就麻烦了。所以定期做逻辑备份依然必要,尤其是在比较重要的开发环境。
最常用的备份工具是mysqldump。在Docker环境下的用法如下:
docker exec mysql-dev mysqldump -uroot -p123456 --all-databases > backup_$(date +%F).sql这条命令在宿主机执行,把容器内MySQL的全部数据库导出到一个SQL文件里。文件会直接生成在宿主机当前目录。如果想单库备份,用--databases指定库名:
docker exec mysql-dev mysqldump -uroot -p123456 --databases testdb > testdb_backup.sql恢复操作也很直接。把SQL文件导入到容器内,有两种方式。
第一种,用docker exec配合管道:
cat testdb_backup.sql | docker exec -i mysql-dev mysql -uroot -p123456注意这里的-i参数不能少,它表示交互模式,让容器内的mysql客户端能从标准输入读取SQL。
第二种,先把备份文件复制进容器,再在容器内部执行导入:
docker cp testdb_backup.sql mysql-dev:/tmp/testdb_backup.sql docker exec -it mysql-dev bash mysql -uroot -p123456 < /tmp/testdb_backup.sql两种方式都可以,如果备份文件特别大,第二种方式相对更稳定一些,不会因为终端断开导致导入中断。
5.3 另一种备份思路:直接冷拷贝数据目录
mysqldump是逻辑备份,生成的都是SQL语句。还有一种更贴合Docker特性的备份方式——直接把挂载目录~/docker/mysql-data压缩打包。这种方式相当于冷备份,适合在停止MySQL服务后操作,保证文件一致性。
docker stop mysql-dev tar -czf mysql-data-backup.tar.gz ~/docker/mysql-data docker start mysql-dev恢复时把备份解压回原目录,然后重新启动容器即可。这个方案的好处是恢复速度快、不需要重新执行SQL,适合数据量大或者结构复杂的场景。缺点是必须在数据库停止状态下操作,否则文件可能不一致。
它和mysqldump是互补关系,不是替代关系。实际使用中我的习惯是:日常定期用mysqldump做逻辑备份保证可以按时间点恢复;在重大变更之前用冷拷贝做快照,万一变更出问题可以直接整体回滚。
6. 继续折腾:docker-compose编排与一主一从复制测试
6.1 用docker-compose把配置固化下来
手动敲docker run命令跑单个容器倒还好,但如果你的机器上有多个Docker服务,比如MySQL、Redis、Nginx一起跑,每次重启机器后靠记忆一个个执行命令,就相当痛苦了。这时候应该用docker-compose.yml把配置固化下来,这也更接近真实团队协作的做法。
在你的项目目录下创建docker-compose.yml:
version: '3.8' services: mysql: image: mysql:8.0 container_name: mysql-dev restart: always environment: MYSQL_ROOT_PASSWORD: 123456 TZ: Asia/Shanghai ports: - "3306:3306" volumes: - ~/docker/mysql-data:/var/lib/mysql - ./conf:/etc/mysql/conf.d./conf这个挂载是可选的,用于放自定义的MySQL配置文件。比如你想修改max_connections或者innodb_buffer_pool_size,不用进容器改文件,直接在宿主机./conf目录下建my.cnf,写配置项,重启容器就生效了。
启动方式:
docker compose up -d这时候容器起来了。查看状态用docker compose ps,停止用docker compose down,删除容器但保留数据用docker compose down(因为数据在挂载目录里,不会丢)。
设置container_name: mysql-dev有一个容器的命名规范问题:如果之前已经有同名容器,docker compose up会报错冲突。先docker rm -f mysql-dev清理一下旧的,再重来就行。
6.2 用两个MySQL容器搭建一主一从复制测试环境
Docker最让我觉得方便的场景之一,就是可以在本机快速拉起一套主从复制环境。以前搭主从需要准备两台机器或者两个虚拟机,现在只需要启动两个容器,各配不同的端口和数据目录就行。
先建两个目录和两份配置文件:
mkdir -p ~/docker/mysql-master ~/docker/mysql-slave主库配置文件master.cnf:
[mysqld] server-id=1 log-bin=mysql-bin binlog_format=ROW从库配置文件slave.cnf:
[mysqld] server-id=2 relay-log=relay-bin read_only=ON然后创建主库容器,并挂载对应配置:
docker run -d \ --name mysql-master \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=123456 \ -v ~/docker/mysql-master:/var/lib/mysql \ -v ~/docker/mysql-master/master.cnf:/etc/mysql/conf.d/master.cnf \ mysql:8.0从库容器端口用3307:
docker run -d \ --name mysql-slave \ -p 3307:3306 \ -e MYSQL_ROOT_PASSWORD=123456 \ -v ~/docker/mysql-slave:/var/lib/mysql \ -v ~/docker/mysql-slave/slave.cnf:/etc/mysql/conf.d/slave.cnf \ mysql:8.0两个容器起来后,进主库容器创建复制账号:
docker exec -it mysql-master mysql -uroot -p在MySQL提示符里执行:
CREATE USER 'repl'@'%' IDENTIFIED BY 'repl_password'; GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%'; FLUSH PRIVILEGES; SHOW MASTER STATUS;记下File和Position的值,比如mysql-bin.000003和157。
然后进从库容器配置主库连接:
docker exec -it mysql-slave mysql -uroot -p接着执行:
CHANGE MASTER TO MASTER_HOST='mysql-master', MASTER_USER='repl', MASTER_PASSWORD='repl_password', MASTER_LOG_FILE='mysql-bin.000003', MASTER_LOG_POS=157; START SLAVE; SHOW SLAVE STATUS\G;这里面最关键的一个细节是MASTER_HOST='mysql-master'。Docker容器之间通信时,可以直接用容器名作为主机名,因为Docker内置的DNS服务会自动解析到对应的容器IP。前提是这两个容器在同一个Docker网络中——默认的bridge网络就满足这个条件。
如果SHOW SLAVE STATUS\G输出中的Slave_IO_Running和Slave_SQL_Running都是Yes,说明主从复制已经正常工作了。这时候在主库建一张表、插入数据,然后去从库查一下,数据应该已经同步过来了。
这套环境对学习复制原理、测试读写分离方案、验证数据同步逻辑都很有用,而且用完随时可以docker compose down整体清理,不会污染宿主机环境。
6.3 从本机服务到“开发环境即代码”
最后多说一句。当你习惯了用Docker跑MySQL,会慢慢发现一个更大的好处:整个开发环境变成了“代码”。数据库版本、配置参数、初始化脚本、端口映射,全部固化在docker-compose.yml或者容器启动脚本里。
新同事加入团队,不需要再花半天时间装MySQL、改配置、建库建用户,直接把配置文件拿过来,执行docker compose up -d,一套和团队完全一致的数据库环境就绪了。换电脑时也是一样,不用重新折腾环境,把脚本和数据目录搬过去就行。
依赖环境的可复现性,很多时候比优化几个SQL查询带来的收益更大——因为它节省的是团队里每个人的重复劳动。
写在最后:给你的三个实操建议
这篇文章写到这里,该讲的流程和坑基本都覆盖到了。最后分享三个我个人实际使用中沉淀下来的小建议。
第一,不要偷懒用latest版本号,拉MySQL镜像时直接指定大版本,比如mysql:8.0,能有效避免过一段时间镜像更新带来的意外变化。我经历过一次latest升级后,连接方式发生变化,排查了半天才发现是镜像版本变了。
第二,数据目录的挂载路径尽量保持稳定,不要今天挂到这个目录、明天换到那个目录。换挂载目录意味着数据文件整体搬移,搬移过程的耗时和数据一致性风险都是没必要承担的,提前规划好路径能省很多事。
第三,Docker化的MySQL适合开发环境和测试环境,但如果要跑生产环境,依然要注意性能和持久化层面的配置调优,这和使用物理机部署没有区别。最终你追求的应该是环境可复现、数据不丢失、服务可替换这三件事的平衡。
希望这篇基于实际操作经验写出来的文章,能帮你少走一些弯路。如果后续在创建容器、数据备份或者主从复制搭建过程中遇到具体问题,欢迎在评论区和大家交流讨论。