
1. 为什么我用Docker装MySQL而不是直接在机器上装先交代一下背景。之前我一直是本机安装派MySQL、Nginx这些数据库中间件全部老老实实装到系统里。直到某天要在三台配置不同的机器上搭同一套环境传统安装方式的问题一次性全暴露了一台是Ubuntu包管理器不一样一台是刚装的系统缺各种依赖库还有一台跑着旧版本MySQL端口和服务冲突处理得我头大。最终折腾了两天才把所有机器的环境对齐。从那以后我在本地开发环境里基本不再直接装数据库而是统一走Docker。这个转变的核心原因是Docker把MySQL的运行环境整个打包了——不只是可执行文件还包括它依赖的系统库、配置文件、数据目录结构全部封装在镜像里。对刚接触Docker的读者我需要先把概念缩小到一个最小可用的范围。你不需要一开始就理解镜像分层、联合文件系统这些底层机制。你只需要知道镜像是模板容器是模板运行出来的实例。就像你手机里的App安装包是模板点安装之后出现的应用图标就是实例。Docker跑MySQL本质就是下载一个官方做好的MySQL模板然后在Docker里启动一个MySQL实例。适合读这篇内容的人主要是这几类第一次接触Docker想在Docker里把MySQL跑起来的新手本机装了MySQL但总出问题想切换到Docker环境的开发者需要在多台机器上快速部署相同数据库环境不想重复踩坑的人。这篇文章覆盖的路径是完整的从零开始拉镜像、启动容器、配置文件挂载、字符集设置、端口映射再到最终进入容器执行SQL查询。每一步我都会解释为什么这么做以及常见的坑在哪里。我在实际帮助同事排查问题时发现一个现象很多人能跑起来但不清楚每条命令的参数到底做了什么。这导致一旦环境出问题完全没有排查头绪。所以我这篇文章的重点不只是怎么操作更在于帮你理解为什么这么操作。2. 动手前的三分钟镜像、容器和数据卷到底是什么关系2.1 从安装包思维跳转到镜像思维要理解Docker下的MySQL建议先忘掉安装这个词改用拉取镜像和运行容器这两个词。传统安装MySQL做的是把二进制文件、系统服务脚本、配置文件放到指定目录然后注册成系统服务。整个过程侵入系统不同版本之间容易留有残留。而Docker的思路完全不同镜像就是一份不可更改的MySQL运行环境快照你拉取的是什么版本的MySQL镜像里就是什么样的MySQL环境不会因为你宿主机系统的差异而出现行为不一致。举个我经常用来打比方的例子传统安装像租房家具是房东放的你搬进去之后还得自己补各种东西退租的时候还得恢复原样Docker像住酒店硬件设施齐全你住进去只动自己需要动的退房关门房间恢复原样。这个比喻不完全准确数据卷可以持久化但第一次接触这个概念时先用这个思路理解没错。2.2 容器生命周期启动、停止、删除都不动你的数据容器不是虚拟机但它有类似虚拟机的隔离特性。你在容器里改配置文件、装软件、建立数据库这些操作默认只影响这个容器不影响宿主机。关键点在这里容器本身是临时的。如果你跑了一个容器往里面建了表插了数据然后把这个容器删了那么这些数据就没了——除非你把数据存储在数据卷Volume里。我见过很多初学者在这里踩坑起了一个MySQL容器创建了业务库然后对着容器执行了重建操作数据全丢。这不是Docker的缺陷而是没有理解容器的生命周期。容器可以被随意销毁和重建但数据必须放在数据卷中。2.3 数据卷和端口映射的直觉理解数据卷的作用可以理解为宿主机上的一个外部硬盘你这个容器用完了删掉了外部硬盘上的数据仍然在。下次再起新容器时把这个外部硬盘挂到新容器上数据就回来了。命令行里对应的是-v参数。比如docker run -v /home/user/mysql-data:/var/lib/mysql斜杠左边的/home/user/mysql-data是宿主机目录右边的/var/lib/mysql是容器内MySQL默认的数据存储目录。意思就是容器内写的数据库文件实际保存在宿主机这个目录里。端口映射也是类似的思路。MySQL默认监听3306端口但容器是隔离的宿主机默认访问不到容器内的端口。你需要把宿主机的某个端口和容器内的3306建立映射外部程序才能通过宿主机IP:映射端口访问到容器里的MySQL。命令参数是-pdocker run -p 3306:3306前面的3306是宿主机端口后面的3306是容器内的MySQL端口。熟悉了这两个参数Docker跑MySQL的基本骨架你就掌握了。3. 从拉镜像到成功启动一条命令背后的每个参数3.1 版本选择mysql:8.0 与 mysql:5.7 怎么选我建议新项目直接选8.0系列而且明确指定小版本号而不是用mysql:latest。原因是latest标签指向的版本会随着官方发布而变化今天拉的是8.0.36过段时间可能就变成8.4或者更高版本这种不确定性在复现环境时非常难受。拉取镜像的命令docker pull mysql:8.0如果你需要5.7做旧项目兼容就拉mysql:5.7。两个版本在容器运行上差别不大但SQL行为有差异类型默认值、字符集排序规则这些细节会不同。这里多提一句5.7是旧时代的经典8.0是当前主流两者的认证插件机制不同这点执行SQL时影响不大但用客户端工具连接时会遇到后面我会单独说。3.2 启动命令逐参数拆解我一般用这样一个命令启动MySQL容器以8.0为例docker run -d \ --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD123456 \ -e TZAsia/Shanghai \ -v /data/mysql8/conf:/etc/mysql/conf.d \ -v /data/mysql8/data:/var/lib/mysql \ --restart always \ mysql:8.0这里逐一说一下每个参数的作用参数作用说明-d后台运行容器在后台运行不会占用你的终端--name mysql8给容器命名后续所有docker命令都用这个名称操作比记一长串容器ID方便得多-p 3306:3306端口映射宿主机3306映射到容器3306宿主机端口可以改成其他值如-p 3307:3306-e MYSQL_ROOT_PASSWORD设置root账号初始密码这是官方镜像提供的最直接环境变量方式-e TZAsia/Shanghai设置时区不设的话容器内默认是UTC时间比北京时间慢8小时排查时间类问题时特别容易被坑-v /data/mysql8/conf:/etc/mysql/conf.d挂载配置目录把宿主机上的配置目录挂进容器后续调整MySQL配置不用进容器改文件-v /data/mysql8/data:/var/lib/mysql挂载数据目录数据持久化的关键删除容器数据也不丢--restart always自动重启策略Docker守护进程启动时自动拉起这个容器机器重启后你不需要手动再启动MySQL正式环境我还会追加一个--privilegedfalse默认就是false显式写出来只是强调安全意识并且限制容器资源如-m 1g之类但入门阶段先不追求这些把上面这张表里的内容吃透就够用了。3.3 启动校验的三层检查容器启动后很多人直接就开始连数据库连不上就一脸懵。正确的做法是分层校验第一步看容器状态docker ps -aSTATUS列显示Up表示容器正在运行显示Exited说明容器退出了。如果刚启动就退出基本是配置错误或端口冲突。用docker logs mysql8看日志MySQL启动失败的常见报错里面都会写清楚原因。第二步看MySQL日志docker logs mysql8 --tail 100看到类似ready for connections的日志说明MySQL服务已经在容器内监听端口了。第三步实际连接docker exec -it mysql8 mysql -uroot -p输入你设置的root密码进入mysql交互界面才算真正确认能用了。这三步检查可以帮你快速把问题定位到容器没起来还是数据库没起来还是密码不对。4. 首次配置字符集、时区、权限与远程连接的连锁反应4.1 字符集为什么必须用utf8mb4而不是utf8很多新手都会踩这个坑建库时用了utf8然后插入表情符号emoji或者某些生僻字时报错。原因在于MySQL的utf8字符集实际上最多只支持3字节而表情符号需要4字节存储utf8mb4才是真正完整的UTF-8实现。配置字符集最稳妥的方式是在容器启动时就挂载一个自定义配置文件。我通常会在宿主机/data/mysql8/conf目录下新建一个名为my.cnf的文件[mysqld] character-set-serverutf8mb4 collation-serverutf8mb4_unicode_ci skip-character-set-client-handshake注意点这个目录是启动命令里通过-v挂载到/etc/mysql/conf.d的。MySQL启动时主配置读取完后会继续读取/etc/mysql/conf.d/目录下的配置文件。所以你在宿主机上放的文件容器启动时会自动生效。skip-character-set-client-handshake这一行的含义是忽略客户端连接时指定的字符集。这样不管客户端用什么字符集握手服务端都强制使用utf8mb4避免出现同一个库在不同连接工具下看到的乱码不一样的问题。修改完配置后需要重启容器docker restart mysql8然后进容器验证SHOW VARIABLES LIKE character_set%;重点看character_set_server和character_set_database这两项都应该是utf8mb4。4.2 时区问题比想象中更容易出问题镜像默认的时区是UTC这个在开发环境测试时通常感知不到等到了联调阶段别人问你为什么插入的时间差了8小时你才开始查。提前在启动命令里加-e TZAsia/Shanghai是最省事的方法。如果容器已经启动了也可以进容器内改配置但不如启动时就设好。验证时区SELECT NOW();看返回的时间是不是当前本地时间。如果是UTC时间就再检查一下你的启动命令和系统环境变量。4.3 远程连接失败的两个隐藏原因默认情况下官方镜像的MySQL root用户被限定只能从容器内部连接。你在宿主机上执行mysql -h127.0.0.1 -P3306 -uroot -p都可能连不上取决于具体版本和权限设置用Navicat或DataGrip这类图形工具连接更是大概率失败。第一个原因是权限。MySQL 8.0默认的root账号认证插件是caching_sha2_password而很多客户端工具和旧版本驱动默认使用mysql_native_password两者不兼容导致连接报错Authentication plugin caching_sha2_password cannot be loaded。解决办法有两种我按推荐程度排列升级客户端工具到最新版本新版工具基本都支持8.0的认证方式如果客户端无法升级则给远程连接的用户指定旧的认证插件CREATE USER app_user% IDENTIFIED WITH mysql_native_password BY app_password; GRANT ALL PRIVILEGES ON *.* TO app_user%; FLUSH PRIVILEGES;第二个原因是防火墙。宿主机未放行你映射出去的端口比如3306外部机器自然无法连接。这个不属于Docker范畴但排查顺序排在很靠前别忽略。4.4 开放远程权限到什么程度合适还有一个常见操作误区为了省事把所有权限都给了root%。这样做在开发环境或许可行但一旦这个数据库可以被内网其他机器访问风险就成倍增加。我的建议是root只保留容器内部使用外部连接单独建专用账号权限按需授予。比如某个应用的库只读账号CREATE USER readonly_user% IDENTIFIED BY readonly_password; GRANT SELECT ON business_db.* TO readonly_user%; FLUSH PRIVILEGES;这样即使密码泄露攻击者能做的也只是读取无法删除和篡改。5. 进入容器执行SQL的四种姿势和它们的适用场景5.1 最常用docker exec进入交互式命令行这是我在日常开发中使用频率最高的方式一条命令直接进入MySQL命令行docker exec -it mysql8 mysql -uroot -p拆开来看docker exec的含义是在运行中的容器里执行命令-it是保持终端交互模式mysql8是容器名后面的mysql -uroot -p是在容器内执行的MySQL客户端命令。执行后输入密码就进入了熟悉的mysql交互界面。进入交互界面后你就可以正常使用SQL了。查看所有数据库SHOW DATABASES;切换数据库USE business_db;查看当前库所有表SHOW TABLES;查询某张表的数据SELECT * FROM users LIMIT 10;5.2 不经交互界面的单行命令如果只是想快速执行一条SQL并拿到结果不需要进入交互界面可以这样写docker exec -it mysql8 mysql -uroot -p123456 -e SELECT VERSION(); SELECT NOW();-e参数后面直接跟SQL语句。这种方式非常适合在脚本里调用比如定时检查DB状态。需要注意把密码直接写在命令行里在开发环境图省事没问题但生产机器上会被shell历史记录留下痕迹也有被其他正在查看进程列表的人看到的风险。建议生产环境配合MYSQL_PWD环境变量或使用配置文件管理认证信息。5.3 进入容器内部再操作的不常见场景有些场合需要先在容器内做一些检查比如查看MySQL日志文件、检查数据目录大小。这时先进入容器docker exec -it mysql8 bash进入容器内部后你会发现自己身处一个精简版的Linux环境。此时MySQL客户端也是可用的mysql -uroot -p容器内直接执行ls /var/lib/mysql可以看到数据库的物理文件这在排查数据目录占用大、文件权限异常等场景下非常有用。用完输入exit退出容器。5.4 批处理方式从宿主机导入SQL文件你有没有遇到过这种情况拿到一个几百MB的SQL备份文件要通过source方式导入数据库。传统方式在宿主机上执行mysql dump.sql引入但在Docker下由于mysql客户端在容器里不能用宿主机的shell直接重定向进去。正确的方式是借助docker exec -i的交互参数让标准输入重定向进容器内的mysql命令docker exec -i mysql8 mysql -uroot -p123456 business_db /path/to/backup.sql注意这里用的是-i而不是-it因为不需要终端交互只需要标准输入管道。加上-t反而会在管道场景下输出乱码提示。导入较大SQL文件时建议先在目标库创建好库CREATE DATABASE IF NOT EXISTS business_db DEFAULT CHARSET utf8mb4;再执行导入命令。5.5 这四种方式的适用情况对比方式命令示例适用场景交互式命令行docker exec -it mysql8 mysql -uroot -p日常查询、调试、手动操作单行命令docker exec mysql8 mysql -e SQL脚本化执行、快速验证进入容器docker exec -it mysql8 bash排查容器内文件、日志、环境问题输入重定向docker exec -i mysql8 mysql ... 文件导入SQL文件、批量执行6. 跑起来之后我替你踩过的坑和排查套路6.1 端口冲突宿主机3306已被占用这是最经典的问题。你的机器上可能已经装过MySQL或者另一个容器占了3306端口运行新容器时报错Error starting userland proxy: listen tcp4 0.0.0.0:3306: bind: address already in use排查方法# 查看宿主机端口占用 lsof -i:3306 # 如果是已存在的mysql服务占用可以停掉或改新容器的映射端口 docker run -d --name mysql8 -p 3307:3306 ...把宿主机端口改成3307外部连接时就用-P3307参数。这个处理方式不优雅但能最快解决问题。长期方案是统一规划端口使用别让容器端口随机分配。6.2 数据卷权限容器启动后立即退出常见的报错日志[ERROR] [MY-010457] mysqld: Cannot change permissions of the file /var/lib/mysql/.mysql.lock [ERROR] [MY-012574] InnoDB: Unable to lock ./ibdata1 error: 11原因通常是挂载目录的权限不对。容器内部的mysql用户UID是999而宿主机上你创建的目录和文件属主可能是root或你自己的用户导致容器没权限写入。排查手段# 查看挂载目录属主和权限 ls -ld /data/mysql8/data解决办法一是把宿主目录属主改成999chown -R 999:999 /data/mysql8/data二是把挂载目录先改成宽松权限再启动容器。不过更推荐第一种能避免后续文件属主混乱的问题。6.3 caching_sha2_password认证插件导致连接失败前面提到过这里展开讲。MySQL 8.0默认使用caching_sha2_password认证但部分旧版客户端不支持。最典型的现象用Navicat 11或旧版JDBC连接时报错Public Key Retrieval is not allowed本地开发时最简单的办法是调整连接字符串参数以JDBC为例加上allowPublicKeyRetrievaltrueuseSSLfalse。但如果受影响的不止一个客户端我更建议给该应用单独建一个mysql_native_password的账号CREATE USER legacy_app% IDENTIFIED WITH mysql_native_password BY legacy_pass; GRANT SELECT, INSERT, UPDATE, DELETE ON business_db.* TO legacy_app%; FLUSH PRIVILEGES;这样既兼容了旧客户端又避免了为所有账号开低安全级别的认证插件。6.4 Linux下MySQL表名大小写敏感同一套SQL在Windows本机和Linux的Docker容器里跑却报Table xxx doesnt exist。原因是Linux下MySQL的lower_case_table_names默认值是0即表名区分大小写而Windows默认值是1不区分。我个人强烈建议在配置文件中显式声明[mysqld] lower_case_table_names1注意这个参数在已经初始化过的MySQL数据目录上修改可能不生效或引发问题。正确的做法是在第一次启动容器、数据目录为空、还在初始化阶段时就把这个配置挂载进去。如果已经跑起来了就只能通过导出数据、清空数据目录、重新初始化来解决。我在这个坑上吃过亏所以这里特意强调。6.5 忘记root密码的容器级重置方案传统安装方式忘记密码要改配置跳过授权表、重启服务、再改回来步骤繁琐。Docker容器下的重置思路类似但更干净第一步在当前容器配置目录里加一行跳过授权验证echo skip-grant-tables /data/mysql8/conf/my.cnf docker restart mysql8第二步此时无需密码即可进入docker exec -it mysql8 mysql -uroot第三步重设密码并退出然后删掉配置项再重启ALTER USER rootlocalhost IDENTIFIED BY new_password;sed -i /skip-grant-tables/d /data/mysql8/conf/my.cnf docker restart mysql8这套方法屡试不爽特别适合开发环境的恢复场景。6.6 用Docker Compose固定整套运行参数到了多台机器都要跑同一套MySQL环境时一条长长的docker run命令复制起来容易抄错。我这时就会把参数固化到docker-compose.yml里services: mysql: image: mysql:8.0 container_name: mysql8 restart: always ports: - 3306:3306 environment: - MYSQL_ROOT_PASSWORD123456 - TZAsia/Shanghai volumes: - /data/mysql8/conf:/etc/mysql/conf.d - /data/mysql8/data:/var/lib/mysql启动命令docker compose up -d这个文件提交到代码仓库后换一台机器拉代码后只需要执行这一条命令就能把MySQL环境带起来。配置变更、环境复现都变得可追踪。我从本地开发转到团队协作之后这是Docker带给我最直接的生产力提升。这套容器化的工作流核心就是镜像保证环境一致数据卷保证数据不丢配置文件挂载保证可修改Compose文件保证可复现。想明白这四件事MySQL在Docker下的安装、启动、配置、进入容器执行SQL就再也不会是困扰你的问题。