CentOS 7上使用Docker部署MySQL 5.7的完整指南 1. 为什么这次部署我选了Docker而不是直接装MySQL如果你之前在CentOS 7上手工装过MySQL应该对那一套流程有印象先找个靠谱的rpm包或者用yum源然后处理依赖、初始化数据库、配my.cnf、启动服务、设密码、开远程权限……每一步都还行连起来就是一两个小时打底。要是中间遇到什么版本冲突、依赖缺失、防火墙拦截整个下午搭进去也不奇怪。后来我把这套流程换成了Docker容器化部署同样的CentOS 7环境从零到能连上数据库基本控制在十分钟左右。这篇文章就把我这次部署的完整过程、踩过的坑以及每个关键参数背后的原因都写出来给正在学Docker、或者打算用Docker在CentOS上跑MySQL的朋友一个可以直接照着操作的参考。我默认读这篇文章的同学已经有基本的Linux操作基础知道怎么用vim、怎么执行命令、知道systemctl是干嘛的。如果这些还没摸熟建议先补一下基础再上手不然过程中出了问题不太好判断是Docker的问题还是系统的问题。用Docker部署MySQL本质上解决的是三件事环境隔离。MySQL跑在容器里宿主机的系统库里不会多一堆乱七八糟的依赖和配置文件想删掉的时候一条命令全部带走不留垃圾。部署方式统一。不管你是在CentOS 7上部署还是换到别的Linux发行版Docker的命令几乎一模一样原先那些换个系统就要换一套安装方式的麻烦不存在了。版本切换方便。想测MySQL 5.7和8.0的差异只需要拉两个不同的镜像分别跑两个容器几分钟就能把对比环境搭出来这在传统安装方式下是很费劲的。在动手之前还有几件事得先想清楚不然部署到一半容易卡住。第一你打算用哪个版本的MySQL。我这次用的是5.7原因很实在很多现有的业务代码和SQL写法都是基于5.7调优的直接上8.0可能会出现认证插件、默认字符集之类的兼容性问题。如果是新项目用8.0没问题如果是老项目迁移建议保守一点选5.7。第二数据放在哪里。容器本质上是一个临时环境默认情况下容器删了里面的数据也跟着没了。所以必须在创建容器的时候就规划好数据目录的挂载把MySQL的数据文件放到宿主机上。这个细节特别关键后文我会专门用一个章节展开讲。第三宿主机端口能不能空出来。如果CentOS 7上已经装了系统自带的MariaDB或者曾经装过MySQL3306端口很容易被占用。部署前建议先查一下端口情况避免创建容器时因为端口冲突直接报错。2. CentOS 7上安装Docker的完整过程与坑点规避2.1 环境检查与内核确认先说环境检查。CentOS 7要跑Docker核心要件有两个64位架构、内核版本不低于3.10。绝大多数CentOS 7安装时默认的内核就是3.10.x所以一般不会出问题但我还是建议动手前先确认一下命令很简单uname -r cat /etc/centos-release如果内核版本低于3.10后面的Docker安装虽然能进行但运行容器时会出现各种诡异问题比如容器启动失败、文件系统操作异常。碰到这种情况别浪费时间调试直接先把内核升级到3.10以上再继续。还有一点容易忽略如果之前装过旧版Docker或者Docker相关组件需要先清干净。CentOS 7自带源里有一个叫做docker的旧包版本比较老直接拿它装出来的Docker功能不完整。如果你不想搞出一堆历史遗留问题建议用官方推荐的docker-ce源来安装。2.2 安装Docker的两种方式对比CentOS 7上装Docker目前主流有两种方式我把它们放在一起对比一下安装方式优点缺点适用场景系统自带源安装yum install docker命令最简单一条装完版本很老功能不全后续跑新镜像容易出兼容问题只做体验、不考虑生产官方docker-ce源安装版本新、更新及时、功能完整需要额外配置yum源多几步操作所有正经使用场景我强烈建议直接上docker-ce源。遇到过太多因为用了自带旧版本而排查半天的情况纯属浪费生命。docker-ce源的配置步骤如下。首先安装yum-utils工具包它提供yum-config-manager命令用来管理yum源yum install -y yum-utils然后添加Docker官方源并完成安装yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo yum install -y docker-ce docker-ce-cli containerd.io如果你的服务器访问官方源比较慢可以换用国内镜像源来加速做法是先把上面的repo地址替换成对应的镜像站地址。这一步不是必须的但如果安装过程一直卡在下载上就可以考虑换源。安装完成后启动Docker并设置开机自启systemctl start docker systemctl enable docker然后确认一下版本确保装的是我们想要的版本docker version看到Client和Server两段都输出了版本号就说明Docker服务已经在正常工作了。如果只有Client有版本号、Server没有那通常是docker服务没起来用systemctl status docker看一下服务的状态。2.3 镜像加速配置这一步别跳过安装完Docker第一件事不是急着拉镜像而是配置镜像加速。Docker默认从Docker Hub拉取镜像在国内网络环境下拉取速度不稳定是常态尤其是几百MB的MySQL镜像慢的时候能把人等崩溃。配置加速的做法是编辑/etc/docker/daemon.json文件。这个文件可能不存在不存在就直接创建{ registry-mirrors: [https://你的加速地址.mirror] }配置好之后重启Docker让配置生效systemctl daemon-reload systemctl restart docker验证一下配置是否生效可以执行docker info在输出信息里能看到Registry Mirrors这一项后面跟的就是你配置的地址。看到这个就说明加速已经生效了。这里有个很实在的经验配置完加速之后拉镜像的速度差别是非常明显的。我最早没配加速时拉一个MySQL 5.7的镜像花了大半个小时配完之后同样一个镜像只要几分钟。所以这个步骤虽然看起来不起眼但对后续体验的影响很大。3. 拉取MySQL镜像与创建容器的参数拆解3.1 镜像版本选择的实际问题Docker Hub上有两个和MySQL相关的镜像一个叫mysql由MySQL官方团队维护另一个叫mariadb是MySQL分支。如果你要跑的是标准的MySQL直接用mysql这个镜像就行。版本选择上我用的是mysql:5.7。有人可能会问为什么不直接用latest标签latest指向的是8.0对现在很多跑5.7的业务来说直接升级会有风险。而且latest标签本身是个移动的标签它指向的版本会随着官方发布而改变这导致一个很尴尬的情况你今天拉下来的latest和三个月前拉下来的latest可能根本不是同一个版本。正经使用的时候一定要精确到具体的版本号。拉取镜像docker pull mysql:5.7拉完之后通过docker images确认镜像已经存在docker images看到REPOSITORY为mysql、TAG为5.7的记录镜像就准备好了。3.2 docker run之前必须理解的几个参数创建MySQL容器是这次部署的核心环节。先看完整命令然后我逐一拆解每个参数的含义和必要性docker run -d \ --name mysql57 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORDyourpassword \ -e TZAsia/Shanghai \ -v /data/mysql57/conf:/etc/mysql/conf.d \ -v /data/mysql57/data:/var/lib/mysql \ --character-set-serverutf8mb4 \ --collation-serverutf8mb4_unicode_ci \ mysql:5.7逐个说-d后台运行容器相当于把MySQL放到后台跑不会霸占你的终端。不加这个参数容器会以前台方式运行日志会直接刷在当前终端上想要做实验观察可以但实际部署肯定要用-d。--name mysql57给容器起一个名字。这个名字在后面所有docker命令中都可以用来指代这个容器比如docker logs mysql57、docker start mysql57。不加的话Docker会随机给个名字管理起来非常难受。-p 3306:3306端口映射格式是宿主机端口:容器端口。左边3306是宿主机上对外提供服务的端口右边3306是容器里MySQL监听的端口。这样宿主机外部通过访问CentOS服务器的3306端口就能连上容器里跑着的MySQL。右边这个3306一般不用改因为MySQL默认监听3306左边这个3306可以改成别的比如3307但如果改了之后所有连接都要用新端口。-e MYSQL_ROOT_PASSWORDyourpassword环境变量这是MySQL镜像特有的初始化机制。镜像第一次启动时会根据这个变量初始化root用户的密码。只有第一次启动时有效一旦数据目录初始化完成之后再改密码就得通过MySQL内部的SQL来改了。-e TZAsia/Shanghai设置容器时区。镜像默认时区是UTC比北京时间慢8个小时如果你不设置存进去的时间戳和日志时间都会差8小时排查问题的时候容易算迷糊。-v /data/mysql57/conf:/etc/mysql/conf.d配置目录挂载。宿主机上的/data/mysql57/conf目录会映射到容器里的/etc/mysql/conf.d目录你在这个目录放一个.cnf文件MySQL启动的时候就会自动读取它作为额外配置。这样改配置不用进容器直接改宿主机文件重启容器就行。-v /data/mysql57/data:/var/lib/mysql数据目录挂载整个部署里最关键的一行。容器里MySQL的数据文件存储位置是/var/lib/mysql通过挂载把它映射到宿主机上的/data/mysql57/data目录。这样一来MySQL所有的数据文件实际都写在宿主机的磁盘上即使容器被删掉、重建数据依然完好无损。--character-set-serverutf8mb4和--collation-serverutf8mb4_unicode_ci这两个参数是MySQL 5.7容器版本的专用做法。它们不是docker run本身支持的参数而是MySQL镜像里定义的启动参数最终会被翻译成MySQL的配置。作用是把数据库默认字符集设为utf8mb4排序规则设为utf8mb4_unicode_ci避免中文乱码问题。3.3 创建MySQL容器的完整命令与验证实际操作的时候我的宿主机目录规划是这样的先创建目录mkdir -p /data/mysql57/conf mkdir -p /data/mysql57/data然后执行完整的创建命令docker run -d \ --name mysql57 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORDAdmin123456 \ -e TZAsia/Shanghai \ -v /data/mysql57/conf:/etc/mysql/conf.d \ -v /data/mysql57/data:/var/lib/mysql \ --character-set-serverutf8mb4 \ --collation-serverutf8mb4_unicode_ci \ mysql:5.7创建完之后确认容器状态docker ps看到STATUS这列是Up就说明容器起来了。这里有一个非常常见的误区docker ps看的是容器进程状态不代表MySQL服务本身就绪了。容器刚启动时MySQL内部的初始化还需要一段时间可能要等十几秒到几十秒。此时如果马上尝试连接很可能会报Connection refused。判断MySQL真正就绪的办法是看日志docker logs mysql57日志末尾出现类似ready for connections的提示才是真正的启动完成。然后就可以用客户端连接验证可以宿主机上装个mysql客户端也可以用容器自带的命令行工具docker exec -it mysql57 mysql -uroot -p输入之前设置的密码看到MySQL的命令行提示符MySQL的容器化部署就算真正跑通了。4. 数据持久化设计为什么/var/lib/mysql必须挂到宿主机4.1 容器文件系统与数据丢失风险这是整个Docker部署MySQL里面最值得认真理解的一个设计点。容器运行的时候它内部有一个独立的文件系统所有写入都发生在这个文件系统里。这个文件系统的生命周期跟容器绑定在一起容器还在文件就在容器被删了这个文件系统也跟着销毁里面的一切都灰飞烟灭。举个具体的场景。你用docker run创建了一个MySQL容器往里存了数据跑了两个月。某一天你为了升级MySQL版本执行了docker rm把旧容器删掉然后用新镜像重新docker run了一个容器。这时候你会发现之前存的那些业务数据全没了就像没存在过一样。这不是Docker的设计缺陷而是容器本身轻量、临时、可随时重建这个理念的必然结果。但数据库这种东西核心价值全在数据里绝对不能容忍数据随着容器消失而消失。解决方案就是数据卷挂载把容器内部的/var/lib/mysql目录映射到宿主机上的一个真实目录。从MySQL的角度看它还是照常往/var/lib/mysql这个路径写文件但从实际存储看这些文件被Docker定向转发到了宿主机的/data/mysql57/data目录。容器删除后宿主机的目录照样在数据文件一个不少。4.2 数据卷挂载的推荐目录结构与权限问题宿主机上的数据目录位置有讲究。我推荐专门建一个目录统一管理不要随便放在某个用户目录下。比如用/data来放所有Docker容器的持久化数据每个容器一个子目录/data ├── mysql57 │ ├── conf │ └── data ├── redis6 │ └── data └── nginx └── html这样的目录结构一眼看过去就知道哪些数据对应哪个容器备份、迁移、排查都方便。权限问题是个容易踩坑的点。MySQL容器内的服务是以mysql这个系统用户身份运行的挂载进来的宿主机目录容器里的mysql用户需要有读写权限。如果你事先用了mkdir创建目录默认owner是root容器内的mysql用户就可能因为权限不足而无法初始化数据库表现就是容器反复重启日志里报权限相关的错误。解决方案是直接把目录owner改成跟容器内uid一致MySQL 5.7镜像里mysql用户的uid通常是999chown -R 999:999 /data/mysql57这个细节我第一次没注意结果容器启动失败查了半天才发现是权限问题。这里写完顺便给大家提个醒创建完目录后顺手改一下owner能省下后面排查的时间。4.3 验证持久化的简单实验持久化到底有没有生效可以做个很直接的实验。先用客户端连接MySQL建一个测试库和一张表CREATE DATABASE testdb; USE testdb; CREATE TABLE t1 (id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50)) DEFAULT CHARSETutf8mb4; INSERT INTO t1 (name) VALUES (persist_test);然后退出容器把这个容器直接删掉docker stop mysql57 docker rm mysql57再重新用相同的参数创建容器docker run -d \ --name mysql57 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORDAdmin123456 \ -e TZAsia/Shanghai \ -v /data/mysql57/conf:/etc/mysql/conf.d \ -v /data/mysql57/data:/var/lib/mysql \ --character-set-serverutf8mb4 \ --collation-serverutf8mb4_unicode_ci \ mysql:5.7等容器起来后连接数据库查一下之前建的表SHOW DATABASES; SELECT * FROM testdb.t1;如果testdb还在、数据也还在说明持久化配置生效了。这个实验我建议第一次做的时候认真跑一遍跑完之后你对容器、镜像、数据卷三者的关系会有一个非常直观的理解这种理解是光看文档体会不到的。5. 字符集、时区与远程访问的细节调整5.1 中文乱码问题的根因与预防容器部署MySQL后中文乱码是个高频问题而且症状很有迷惑性本地客户端连接显示正常但程序写入后再查出来就全是问号或者程序写入时直接报character set相关的错误。根因几乎都指向同一个地方MySQL 5.7的默认字符集是latin1不支持中文存储。我在创建容器时加的两个参数就是针对这个问题的。这里多说一句utf8mb4和utf8的区别。MySQL里的utf8最多只能存3个字节的字符而很多现在的特殊字符、emoji表情需要4个字节用utf8存会直接报错或者出现乱码。utf8mb4是utf8的超集能覆盖所有Unicode字符是当前最稳妥的选择。排序规则utf8mb4_unicode_ci基于通用Unicode排序算法在处理多语言数据时比utf8mb4_general_ci更准确对于需要存中文的应用来说更靠谱。容器已经创建了再去改字符集会比较麻烦所以最好在创建的时候就一次性配好。如果你部署的时候漏了这个参数事后的补救方案是修改宿主机上挂载的conf目录下的配置文件加一段[mysqld] character-set-serverutf8mb4 collation-serverutf8mb4_unicode_ci然后重启容器。但是注意这个方案只对新建的库、表生效已经建好的老库老表字符集不会自动变那些库需要另外处理。这也是为什么我一直强调创建容器的时候就要把字符集参数加上不要图省事。5.2 时区设置对日志和应用的影响时区问题看起来不太起眼但造成的后果让人很头大。容器默认时区是UTC比北京时间早8个小时的错觉实际上是慢了8个小时。如果应用往MySQL里写入带有时间的记录存进去的时间会比北京时间晚8小时查询出来跟业务实际时间对不上。涉及定时任务、对账、订单统计之类对时间敏感的业务这种偏差会引发一系列看起来逻辑不对的问题。我的做法是创建容器时直接通过环境变量指定时区-e TZAsia/Shanghai更保险的做法是同时在配置文件里也指定默认时区[mysqld] default-time-zone 08:00两者配合不管是通过环境变量还是配置文件MySQL的内部时区都是东八区时间存进去就对了。5.3 远程连接的安全策略容器部署完成后通常情况下宿主机外部的应用服务器或本机的数据库客户端都要通过网络来访问这个MySQL。第一道关卡是防火墙。CentOS 7默认使用firewalld默认情况下3306端口对外并不开放。在宿主机上开放端口firewall-cmd --zonepublic --add-port3306/tcp --permanent firewall-cmd --reload如果不放行就会出现从宿主机本机连接没问题但其他服务器连不上的情况。第二道关卡是MySQL的用户授权。默认情况下root用户只允许localhost访问。要给远程服务器授权需要单独创建一个用户或者修改root的host范围我的建议是创建独立账号别直接用root做远程登录CREATE USER appuser% IDENTIFIED BY app_pass_123; GRANT ALL PRIVILEGES ON testdb.* TO appuser%; FLUSH PRIVILEGES;这里有一个安全上的取舍。%表示允许从任意IP登录带来的问题是只要MySQL的3306端口暴露在网络上这个账号就会被不断尝试口令爆破。一个更稳妥的做法是指定具体的网段CREATE USER appuser192.168.1.% IDENTIFIED BY app_pass_123;这样一来只有192.168.1.x网段内的机器能连外面的人连账号都不存在安全性高不少。第三道关卡是容器本身的端口映射范围。如果你只需要让内网访问那映射的时候可以只绑定内网IP-p 192.168.1.100:3306:3306这样宿主机上的3306端口只对指定的IP地址提供服务不会暴露在所有网络接口上。这个细节在网络环境比较复杂的情况下特别有用。6. 运行期间的常用运维操作与排查思路6.1 日常命令速查容器跑起来只是第一步后面日常维护才是持续的事。我整理了这套模式下最高频的命令操作命令查看运行中的容器docker ps查看所有容器含已停止的docker ps -a查看某容器日志docker logs mysql57持续跟踪日志docker logs -f mysql57停止容器docker stop mysql57启动已停止的容器docker start mysql57重启容器docker restart mysql57进入容器内的shelldocker exec -it mysql57 bash进入容器内的MySQLdocker exec -it mysql57 mysql -uroot -p删除容器docker rm mysql57还有个命令容易被忽略docker top mysql57它可以列出容器内的进程信息当你怀疑MySQL内部某个操作卡住了用它看一下进程状态很有帮助。6.2 容器启动失败的典型排查链路部署过程中大概率会遇到容器起不来的情况遇到先别慌按这个链路排查基本都能定位到问题。第一步看容器当前状态docker ps -a如果状态是Exited(1)或者Restarting说明容器启动后崩了。第二步看日志docker logs mysql57日志里会直接给出MySQL进程的报错信息。大部分新手犯的错是跳过这一步直接去网上搜docker mysql 起不来然后各种瞎试纯属浪费时间。日志里的信息往往就是问题的答案。第三步根据日志里的错误类型对症下药。我把几个最常见的场景整理成表格日志里的报错信息实际原因解决办法Cant find file /var/lib/mysql/数据目录挂载错误或目录为空但权限不对检查-v映射的宿主机目录是否存在owner是否为999permission denied宿主机数据目录权限不足chown -R 999:999 /data/mysql57port already in use宿主机3306被占用检查端口占用换映射端口或释放端口Got fatal error 1236容器之前异常退出binlog和实际数据不一致属于异常情况需要根据上下文具体分析第四步修改完问题后别急着用docker run再建一个容器。正确的做法是先停止并删除旧容器再重新创建。为什么因为如果你直接又跑一个docker run且用了同一个--nameDocker会提示容器名称冲突Error response from daemon: Conflict. The container name /mysql57 is already in use正确顺序是docker stop mysql57 docker rm mysql57 docker run -d ...之前的完整参数还有一个肉眼容易忽略的细节创建容器时如果-v宿主机目录路径写错了或者目录没建好Docker会自动创建一个空目录作为挂载目标这个空目录里什么都没有MySQL初始化时发现数据目录为空会尝试初始化但如果目录权限不对同样会失败。所以创建容器前该建目录建目录该改owner改owner一次到位。6.3 日志查看与性能观察了解MySQL的运行情况不一定非要进容器执行SQLDocker本身提供的日志和统计功能就能做基本面判断。docker logs mysql57能看到MySQL标准输出里的所有日志错误日志、启动信息、慢查询日志如果配了也会输出。排查问题时习惯用这个命令带时间戳来看docker logs --since 30m mysql57只看最近30分钟的输出比直接拉几千行日志要清爽得多。容量方面docker stats命令可以查看所有运行中容器的CPU、内存、网络、磁盘IO实时数据docker stats这个命令输出的内容比较像Linux的top是排查容器很卡和内存占用高这类问题的第一手工具。如果你发现MySQL容器长期内存占用一直在涨不用急着彻查SQL优化先看看是不是连接数堆积、慢查询太多导致的。6.4 开机自启配置还有一个收尾工作别漏了。你通过systemctl enable docker配置了Docker服务开机自启但Docker服务起来后容器默认并不会自动跟着起来。如果服务器重启了MySQL容器会处于停止状态你的业务连接就会直接失败。解决方案是在创建容器时加一个参数--restart unless-stopped这个参数的意思是容器自动启动除非你手动停止了它。也就是说Docker服务启动时这个容器会跟着起来你手动docker stop之后它不会自己起来但如果你只是重启服务器Docker一启动它就会自动带起来。这个设置对数据库这种需要常驻的服务非常实用。加上这个参数的完整创建命令docker run -d \ --name mysql57 \ --restart unless-stopped \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORDAdmin123456 \ -e TZAsia/Shanghai \ -v /data/mysql57/conf:/etc/mysql/conf.d \ -v /data/mysql57/data:/var/lib/mysql \ --character-set-serverutf8mb4 \ --collation-serverutf8mb4_unicode_ci \ mysql:5.7如果容器已经创建了但当时没加这个参数也可以用命令修改docker update --restart unless-stopped mysql57docker update不能改docker run的大部分参数但--restart这类运行时策略是可以随时修改的。这个用法比较冷门知道的人不多但特别好使。7. 我自己在实际部署中踩过的坑与最终建议整个流程走下来印象最深的还是数据目录权限那个坑。当时创建好目录就急急忙忙跑docker run结果容器一直在Restarting状态docker logs一看是那个熟悉的permission denied。排查时最初的直觉是系统SELinux的问题查了半天也没得出个结论后来把目录owner改成999后一次就好了。从那之后我的习惯是先mkdir、再chown、最后docker run三步按顺序来一步都不省。还有一次是因为改了镜像版本从5.7升到8.0数据目录还是同一个结果8.0的容器起不来报错内容是数据目录的版本不兼容。这个场景提醒我两个问题一是MySQL从5.7升8.0这种大版本跨越不能直接复用旧数据目录需要先做数据迁移二是每个容器的重要数据目录命名尽量带上版本号比如/data/mysql57/和/data/mysql80/分开避免混淆。关于学习顺序我也想说点实在的。如果你是刚接触Docker不要一上来就去研究镜像构建、Docker Compose这些进阶话题。先把我写的这套流程完整跑一遍装Docker、拉镜像、跑一个容器、理解端口映射、理解数据卷、理解环境变量。这些核心概念弄明白了后面的东西都是水到渠成的。这次部署完成之后CentOS 7上的这套环境已经可以作为一个基础模板留着。下次再要部署别的服务比如Redis、Nginx思路是完全一样的拉镜像、定端口、规划数据目录、设好重启策略。Docker这种东西第一次用可能会觉得参数太多记不住但当你亲手把一套流程从零跑通那些参数就不再是死记硬背的东西了而是每条都有活生生的使用场景在背后撑着。