多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

用Docker快速构建本地MySQL环境:容器化开发实践

用Docker快速构建本地MySQL环境:容器化开发实践 我最早在本地折腾 MySQL走的还是下载安装包那套流程。装完要改配置、设环境变量、注册成系统服务想换版本就得先卸载卸载完还要手动扫残留目录。后来换成 Docker 跑 MySQL从第一次把容器拉起来到现在我再也没在本机直接装过 MySQL 数据库。如果你也经常被装环境这件事打断这篇内容就是给你准备的——用 Docker 在本地快速构建一个 MySQL 实例整个过程不依赖任何安装向导一条命令就能跑起来想换版本就换版本想删掉就删掉数据还都在。这份实操笔记面向的开发场景包括后端接口联调、本地跑测试用例、学 SQL、复现线上慢查询以及任何需要一个干净的 MySQL 环境的时刻。注意我聊的是本地开发环境生产环境的数据库部署不在这篇文章范围内别把图省事的思路直接搬去生产线。1. 为什么放着安装包不用非要用 Docker 跑 MySQL1.1 传统安装方式的五种痛苦说实话直接装 MySQL 不是不能用只是维护成本高。我总结过五种常见痛苦安装流程繁琐。不同系统有不同安装包Windows 有 MSImacOS 有 dmgLinux 各发行版包管理还不一样装完还要处理依赖、初始化 datadir、设置密码策略每一步都可能出错。版本管理混乱。项目 A 用 MySQL 5.7项目 B 用 8.0本地只能装一个要么来回切换要么装个旧版本硬扛很多新特性用不上。卸载不干净。卸载完 MySQL 后数据目录、配置文件、系统服务注册表经常残留下次装新版本时端口被占、配置文件路径冲突排查起来非常费劲。配置文件散落多处。Windows 上是 my.iniLinux 上是 /etc/mysql/my.cnf数据目录、日志目录各有位置。想换一个实例配置得记住一堆路径换台机器全部重来。重装系统成本高。笔记本一换所有环境变量、初始化参数、数据库用户重新配一遍时间就这么耗没了。1.2 容器方案到底好在哪Docker 把上面这些麻烦基本都抹平了。核心思路是把 MySQL 连同它的运行环境一起装进一个轻量级容器里你只需要管理镜像和容器不需要关心 MySQL 内部文件散落在哪里。具体好处是这些一条命令拉起实例不用走安装向导不用手动初始化数据目录可以同时跑多个版本MySQL 5.7 和 8.0 各占一个容器端口错开就行容器可以随便删、随便重建发现配置写错了直接 rm 再来宿主系统干干净净连接参数、数据卷、初始化库都写进 docker-compose.yml可以提交到代码仓库团队其他成员拉下来一条命令就得到一致环境。1.3 性能损耗问题其实可以忽略有人会担心多套一层 DockerMySQL 会不会变慢本地开发环境下真不用焦虑。Linux 上运行 Docker 容器本质就是进程隔离MySQL 进程直接跑在宿主机内核之上没有虚拟机那层硬件模拟性能损耗非常小。macOS 和 Windows 上因为 Docker 依赖虚拟机IO 上会有一点开销但日常开发、联调、跑测试的数据量根本感知不到差异。本地数据库的瓶颈绝大多数情况下在 SQL 本身不在容器化。你要的是能快速起一个环境来验证问题不是在生产环境压测。2. 开始之前必须搞懂的五个基础概念2.1 镜像与容器安装包与运行实例镜像可以理解成一个只读模板docker pull mysql:8.0 拉下来的镜像是 MySQL 服务端程序、默认配置和初始化脚本的组合体。容器则是这个模板运行起来的实例每次 docker run 都会基于镜像创建一个新的独立运行环境。同一个镜像可以启动多个容器互相不干扰。容器里产生的文件是临时的除非你把数据放到外部挂载目录否则容器一删容器里新写入的东西就全丢了。这个临时性是理解后面所有操作的关键。2.2 端口映射没有 -p连接就会失败容器有自己独立的网络空间宿主机默认访问不到容器里的端口。要让本机程序连接 MySQL就需要把容器内 MySQL 监听的 3306 端口映射到宿主机的某个端口。命令写法是-p 3306:3306语法是宿主机端口:容器端口。左边是宿主机这边对外开放的端口右边是容器内部 MySQL 真正监听的端口。左边可以随意改比如-p 3307:3306这样外部通过 3307 访问容器内 MySQL 依旧监听 3306。这个设计最大的好处是想跑两个 MySQL 实例只要把宿主机端口错开其他完全不冲突。2.3 数据卷容器删除后数据去哪MySQL 容器内的数据目录是 /var/lib/mysql。如果你不挂载数据卷容器一旦删除里面的数据文件跟着一起消失这就违背了数据库要持久化的原则。数据卷就是把宿主机的一个目录映射到容器内的 /var/lib/mysql。容器往这个目录写入数据实际是写到宿主机磁盘上容器删掉重建只要挂载同一个目录数据就还在。挂载方式我推荐直接指定宿主机路径-v ~/docker/mysql/data:/var/lib/mysql左边是宿主机目录右边是容器目录。别用匿名卷就是只写-v /var/lib/mysql这种形式虽然也能持久化但数据目录藏得很深备份、恢复和排查都不方便。2.4 初始化环境变量首次启动时的那点事镜像里的初始化脚本会在数据目录为空的首次启动时读取环境变量帮你自动完成数据库初始化这是快速构建的关键之一。环境变量作用MYSQL_ROOT_PASSWORD设置 root 用户密码必填MYSQL_DATABASE首次启动时自动创建指定数据库MYSQL_USER / MYSQL_PASSWORD额外创建一个普通用户并授权给 MYSQL_DATABASEMYSQL_ROOT_HOST控制 root 允许从哪些主机连接默认 localhost填 % 表示允许任意主机有一点必须注意这些环境变量只在首次启动、数据目录为空时生效。如果数据目录已经初始化过了再改 MYSQL_ROOT_PASSWORD 是不会生效的得用 SQL 去改密码。2.5 字符集与时区从创建时就定好中文场景下最烦的就是乱码。MySQL 8.0 默认字符集就是 utf8mb4基本不用操心但如果你用的镜像版本偏老或者项目里要兼容老系统最好在创建容器时就把字符集显式指定好免得建完表再改。时区也一样。官方镜像默认时区是 UTC直接查 NOW() 会比北京时间少 8 小时。可以加环境变量-e TZAsia/Shanghai再配合 MySQL 服务端的default-time-zone参数一起解决。这些配置在容器创建初期定好最省事等数据写进去了再调整字符集或时区代价会大得多。3. 核心实操一条命令把 MySQL 拉起来3.1 第一步确认 Docker 环境与镜像版本先确认 Docker 已经装好docker --version docker info第一条命令看客户端版本第二条确认 Docker 服务端正常响应。镜像版本的选择我建议直接用主版本号而不是 latest。latest 会跟随镜像维护节奏变化今天启动可能和上周启动的版本不一样开发环境最好保持可复现。常用选择mysql:8.0当前主流默认 utf8mb4新项目直接选mysql:5.7老项目兼容场景还在用mysql:8.0.34 这种具体小版本需要精确复现问题的时候用。本地开发个人建议 8.0除非你要匹配公司线上版本。3.2 最简启动命令逐项拆解先建好数据目录然后执行启动命令mkdir -p ~/docker/mysql/data ~/docker/mysql/conf docker run -d \ --name local-mysql \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORDroot123456 \ -e MYSQL_DATABASEtestdb \ -v ~/docker/mysql/data:/var/lib/mysql \ --restartalways \ mysql:8.0逐项说下每个参数-d后台运行不占住当前终端--name local-mysql给容器起名后面所有操作都用这个名字引用不用记容器 ID-p 3306:3306端口映射外部通过 3306 访问容器内 MySQL-e MYSQL_ROOT_PASSWORDroot123456设置 root 密码本地开发用简单密码没问题别用到生产环境-e MYSQL_DATABASEtestdb首次启动自动创建 testdb 库-v ~/docker/mysql/data:/var/lib/mysql持久化数据目录--restartalwaysDocker 启动时自动拉起容器电脑重启后不用手动 docker startmysql:8.0要使用的镜像名和 tag。跑起来之后MySQL 的初始配置可能还需要微调。我习惯再挂一个自定义配置文件改成这样mkdir -p ~/docker/mysql/conf cat ~/docker/mysql/conf/my.cnf EOF [mysqld] character-set-serverutf8mb4 collation-serverutf8mb4_unicode_ci default-time-zone08:00 EOF docker run -d \ --name local-mysql \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORDroot123456 \ -e MYSQL_DATABASEtestdb \ -v ~/docker/mysql/data:/var/lib/mysql \ -v ~/docker/mysql/conf/my.cnf:/etc/mysql/conf.d/my.cnf \ --restartalways \ mysql:8.0这里有个坑挂载单个配置文件时宿主机上的文件必须真实存在否则 Docker 会在宿主机上生成一个同名目录容器起不来。3.3 验证容器状态与连接数据库容器启动后先看状态和日志docker ps | grep local-mysql docker logs local-mysql --tail 50看到日志里有ready for connections之类的字样基本就成功一半了。再进容器里用 mysql 客户端验证docker exec -it local-mysql mysql -uroot -p然后执行SELECT VERSION(); SHOW DATABASES;能查到数据库列表就说明服务正常。接下来用你习惯的图形客户端连也可以连接参数如下项目值主机127.0.0.1端口3306用户root密码root123456如果图形客户端报错说 caching_sha2_password 相关别慌这是 MySQL 8.0 默认认证插件比较新老客户端不认。要么换新版本客户端要么在容器里把用户认证方式改成 mysql_native_password本地开发怎么方便怎么来。3.4 进阶用 docker compose 固化启动配置docker run 参数多、容易记串而且团队成员要复现环境还得复制你的命令。我强烈建议本地开发直接上 docker compose。新建目录后写一份 docker-compose.ymlservices: mysql: image: mysql:8.0 container_name: local-mysql ports: - 3306:3306 environment: MYSQL_ROOT_PASSWORD: root123456 MYSQL_DATABASE: testdb volumes: - ./data:/var/lib/mysql - ./conf/my.cnf:/etc/mysql/conf.d/my.cnf restart: always注意新版 Docker Compose 不需要写 version 字段直接写 services 即可。启动和停止命令docker compose up -d docker compose downdocker compose down 只停止并删除容器不会删除挂载在 ./data 下的数据。这点很重要容器可以随便重建数据目录一直保留在项目文件夹里。compose 文件还能提交到 git换机器拉下来直接 up -d环境就恢复了。我自己本地所有中间件包括 MySQL、Redis、消息队列全部用 compose 管理配置集中、可提交、可复现比一条条记 docker run 命令省心太多。4. 数据不死持久化挂载、备份恢复与版本升级4.1 挂载数据目录时最容易踩的权限坑Docker 挂载数据目录有一个很经典的坑容器起不来。症状通常是这样docker logs local-mysql --tail 100日志里出现[ERROR] [MY-011446] [Server] Directory /var/lib/mysql/ creation failed. (Errcode: 13 - Permission denied)原因是容器内 MySQL 进程以 mysql 用户运行这个用户 uid 通常是 999而宿主机上你创建的目录属主是你自己的用户uid 可能是一千多mysql 用户没有写入权限。解决办法是把宿主数据目录的属主改成 uid 999sudo chown -R 999:999 ~/docker/mysql/data开发机上的临时方案也可以 chmod 777但不太优雅。注意如果第一次初始化就因为权限失败数据目录里可能残留半成品文件重新修好权限后要先清空目录再启动否则初始化脚本不会重新跑。4.2 怎么确认容器重建后数据还在挂载数据卷这件事值得你亲手验证一遍这样以后删容器、重建容器才有底气。在容器里建库建表插数据CREATE DATABASE demo; USE demo; CREATE TABLE t ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(32) ); INSERT INTO t(name) VALUES (hello);然后做一次破坏性测试docker stop local-mysql docker rm local-mysql再用最开始那条 docker run 命令重新启动容器连进去查SELECT * FROM demo.t;数据还在就说明持久化链路没问题。我每次换新环境都会做一遍这个验证虽然多花一分钟但能排除掉以为自己挂载了但是挂错了的隐患。4.3 mysqldump 逻辑备份与恢复实操数据卷保证的是容器生命周期内不丢数据但它不防手误删库、不防误改数据。备份还是要做。最简单的是逻辑备份用 mysqldump 在容器里导出 SQL 文件docker exec local-mysql sh -c exec mysqldump -uroot -p$MYSQL_ROOT_PASSWORD --all-databases --single-transaction --routines --events backup.sql参数说明--single-transactionInnoDB 表在线备份时不锁表--routines导出存储过程--events导出定时事件--all-databases导出全部库。恢复时把 SQL 文件导回容器docker exec -i local-mysql sh -c exec mysql -uroot -p$MYSQL_ROOT_PASSWORD backup.sql逻辑备份是纯文本可读性强跨版本迁移基本都靠它。另一种是直接物理备份数据目录停掉容器后把 entire data 文件打包docker stop local-mysql tar -czf mysql-data-backup.tar.gz ~/docker/mysql/data docker start local-mysql物理备份恢复速度快但对版本兼容性要求高新版本 MySQL 不一定能直接用旧版本的物理文件。本地开发我习惯每周一次逻辑备份重要操作前再手动 dump 一次。4.4 更换镜像版本时数据如何平稳迁移如果只是从 MySQL 8.0 的一个小版本换到另一个小版本比如 8.0.30 换到 8.0.40可以直接改 docker-compose.yml 里的 image 版本然后docker compose down docker compose up -d数据目录不用动小版本升级通常向前兼容。跨大版本就完全不同了比如从 5.7 升到 8.0千万不要直接把 5.7 的数据目录挂给 8.0 的镜像系统表结构不兼容镜像初始化容易失败。正确做法是先 mysqldump 导出再启动一个 8.0 的新容器把 dump 文件导进去docker exec -i new-mysql sh -c exec mysql -uroot -p$MYSQL_ROOT_PASSWORD backup.sql本地开发没有在线升级压力dump 导入是最稳的路子慢一点没关系。5. 本地开发视角的高频坑位与排查记录5.1 连不上数据库按这个链路查下去遇到连不上我的排查顺序固定是四条第一条看容器是不是活着docker ps -a | grep local-mysqlSTATUS 不是 Up 状态先看下面的日志排查。第二条看日志docker logs local-mysql --tail 100启动失败的原因基本都集中在日志最后几十行。第三条看宿主机的端口占用lsof -i:3306Linux 或者 macOS 用 lsofWindows 用 netstat -ano | findstr 3306。看到端口被其他进程占了就把容器的宿主机端口换成 3307 重新映射。第四条检查连接串。常见错误是 host 写成了容器名或者 localhost端口写错用户密码不对。本地连接统一写 127.0.0.1:3306别用 localhost有的客户端会解析成 IPv6 导致连接失败。5.2 容器一直在重启多半是数据目录权限docker ps 里看到 STATUS 显示 Restarting说明容器在启动脚本阶段就不正常退出基本是初始化时出了问题。先看日志确定原因docker logs --tail 50 local-mysql常见原因我整理了三种原因特征处理数据目录权限不足日志报 Errcode 13 - Permission deniedchown -R 999:999 数据目录配置文件错误日志报 Unknown variable 或参数段错误检查 my.cnf去掉不支持的参数缺少初始化变量日志报 root password required设置 MYSQL_ROOT_PASSWORD 或用 MYSQL_ALLOW_EMPTY_PASSWORDyes日志里如果什么都没报就退出再查一下镜像的架构和宿主系统是否匹配比如 arm64 镜像拉到 x86 机器上现象也会很诡异。5.3 电脑重启后容器没起来restart 策略Docker 本身没启动容器当然不会跟着起。先把 Docker Desktop 设置里的开机自启动开了。Docker 起来之后容器是否自动启动取决于 restart 策略。创建容器时加了--restartalways的Docker 服务启动后会自动拉起没用这个参数的重启后容器状态是 Exited。已经创建好的容器可以用 docker update 补上docker update --restartalways local-mysql这个命令可以修改已有容器的策略不用重新创建容器。5.4 时间与中文乱码时区、字符集和连接串如果查数据时发现中文变成问号按顺序排查三处库表字符集、连接字符集、容器服务端字符集。检查库表字符集SHOW CREATE DATABASE testdb; SHOW CREATE TABLE testdb.t;如果显示字符集不是 utf8mb4本地开发最简单的做法是重建表数据量小没必要折腾 ALTER CONVERT。连接串也要带编码参数比如 JDBC 的写法jdbc:mysql://127.0.0.1:3306/testdb?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai时间差 8 小时的问题检查服务端时区SELECT NOW(); SELECT global.time_zone, session.time_zone;显示 SYSTEM 就说明跟着容器系统时区走需要检查启动时有没有设置 TZ 环境变量。最稳妥还是在 my.cnf 里显式加default-time-zone08:00换容器重启都不会丢。最后分享两个我坚持到现在的使用习惯。第一镜像 tag 固定到主版本或具体小版本绝不追 latest这样换机器重装的时候能保证起的是同一个版本排查问题不会出现我这边正常你那边报错的诡异差异。第二docker-compose.yml 和 data 目录永远放在同一个项目文件夹里整个文件夹就是一个可迁移的数据库环境换电脑直接拷过去up -d 就恢复原来的数据比重新安装、重新导入要省太多时间。本地这套方案就是给开发提速用的数据库出了问题推倒重来的成本极低这也是我敢在本地一直用容器跑数据库的真正原因。
返回列表