☰
Docker容器化实战:从镜像原理到MySQL与Redis部署编排
2026/10/29 3:58:23 网站建设 项目流程

1. 先搞明白Docker到底解决了什么问题

1.1 从“在我机器上是好的”说起——环境一致性的本质

接触容器化这些年,Docker是我最常用也最想推荐先学的工具。以前部署一套环境,要在服务器上手动装依赖、调配置、处理版本冲突,折腾半小时是常事,换了机器还得重来一遍。用Docker之后,一条命令拉起服务,交付的是整个运行环境,而不是一堆安装步骤。

很多人第一次听到Docker,会把它理解成一个轻量虚拟机。这个类比方向对,但不全对。虚拟机虚拟的是整台硬件,里面要装完整操作系统,启动要几十秒甚至几分钟,占用几个GB磁盘很常见。Docker虚拟的是操作系统内核之上的运行空间,容器共享宿主机内核,只打包你自己的应用和它需要的库、配置,启动以毫秒计,镜像压缩后往往只有几十到几百MB。打个比方:虚拟机是整套出租房,含全部家电家具;容器是搬家公司精打包后的箱子,里面只装你需要的东西,到了新场地拆开就能用。

这套机制解决了分布式开发和交付里最让人头疼的环境不一致问题。你本地跑通的代码,上了服务器就起不来,多半是系统库版本、依赖路径、环境变量有差异。镜像把应用和运行时环境固化成不可变对象,镜像在哪里,环境就还原到哪里。这种可复现性,比任何部署文档都可靠。

1.2 镜像、容器、仓库:三个核心概念一次说清

刚接触Docker时,镜像(Image)、容器(Container)、仓库(Repository)这三个概念最容易绕晕。我当时的理解方式是:镜像就是安装包,容器就是安装包运行起来之后的进程实例,仓库就是存放安装包的软件源。

拿MySQL举例。docker pull mysql:8.0是从仓库把镜像下载到本地,类似于下载了一个安装包;docker run基于镜像启动一个容器,类似于执行安装并启动服务。你可以在同一台机器上,基于同一个镜像启动多个容器,互相隔离。改容器里的东西不影响镜像,就像你修改某个程序运行时内存里的数据,不会改变磁盘上的安装包一样。要保存修改,可以用docker commit生成一个新镜像(但我几乎不用这个,可复现性差,正确做法是用Dockerfile构建)。

关联操作随手列一下,后面实战会反复用到:

  • docker pull 镜像名:标签:拉取镜像到本地
  • docker images:查看本地镜像列表
  • docker run 参数 镜像名:创建并启动容器
  • docker ps -a:查看所有容器(含已停止的)
  • docker exec -it 容器名 bash:进入容器内部
  • docker logs 容器名:查看容器日志
  • docker rm 容器名/docker rmi 镜像名:删除容器/镜像

这三个概念的关系,是理解后面所有操作的地基。地基稳了,再去看Dockerfile、Compose、Kubernetes都有顺藤摸瓜的感觉。

2. 安装Docker与解决镜像下载慢的焦虑

2.1 Docker Engine和Docker Desktop怎么选

安装Docker之前,先分清两个东西:在Linux服务器上跑的是Docker Engine——纯命令行工具集,只有守护进程和CLI,没有图形界面;在macOS或Windows上用的Docker Desktop——一个带GUI的桌面应用,内部其实也跑着一个Linux虚拟机来承载容器。

选哪个取决于场景。生产环境、云服务器、Linux开发机,一律装Docker Engine,轻量、稳定、资源开销小。日常开发在Mac或Windows上,装Docker Desktop方便很多,因为可以在写代码的同时直接跑容器,不用额外维护远程机器。国内用户装Docker Desktop时注意:安装包体积大,下载前确认网络稳定;使用WSL2后端的Windows用户,建议用WSL2发行版自带的Docker体验更顺滑。

Linux上安装Docker Engine最省事的方式是使用官方脚本:

curl -fsSL https://get.docker.com | sh

执行完成后,把当前用户加入docker组,避免每次敲命令都用sudo:

sudo usermod -aG docker $USER newgrp docker

验证是否成功:

docker version docker run hello-world

看到Hello from Docker!的输出,说明引擎已经正常工作了。如果这条命令卡住不动,多半是拉取镜像慢,接着往下看加速配置。

2.2 镜像下载慢?先把加速器配好

镜像下载慢,是刚上手时体感最强的问题。原因不复杂:默认的Docker Hub仓库部署在海外,跨国传输带宽有限,拉一个大镜像动辄几十分钟,甚至卡到超时。这跟访问某些海外网站速度慢是同一类问题,解决方案也相似——把镜像请求打到离自己更近的仓库节点,也就是配置镜像加速器。

Docker的守护进程支持通过/etc/docker/daemon.json文件配置镜像源。用一个数组列出可用的镜像仓库地址,Docker会优先从这些地址拉取镜像。

sudo mkdir -p /etc/docker sudo tee /etc/docker/daemon.json <<-'EOF' { "registry-mirrors": [ "https://docker.m.daocloud.io", "https://hub-mirror.c.163.com" ] } EOF sudo systemctl daemon-reload sudo systemctl restart docker

配置完成后,用下面这条命令确认镜像加速器已经生效:

docker info | grep -A 2 "Registry Mirrors"

如果输出里有你配置的地址,说明Docker已经在使用加速器拉取镜像了。实测下来,配置加速器之后,拉取MySQL、Redis这种两三百MB的镜像,基本可以在几分钟内完成,体验质的提升。

提示:镜像加速器地址偶尔会失效或变慢,建议多配两到三个,Docker会按顺序尝试。如果某个时间点拉取还是很慢,换个地址再试一次。

2.3 镜像仓库的选择与加速方案

除了加速器,镜像仓库本身也有很多选择。Docker Hub是官方默认仓库,镜像最全,社区最活跃,但服务器在国外。国内使用较多的是阿里云容器镜像服务个人版,注册后在控制台可以拿到专属加速地址,格式类似https://xxx.mirror.aliyuncs.com,把它填入daemon.json即可。

还有一个思路:对于像mysql、redis、nginx这类基础镜像,国内很多云厂商都已经做了同步缓存,用它们的加速地址下载速度往往比直接从官方地址快不少。这里我不具体推荐某一家,因为不同地区和不同时段的表现差异很大,你可以自己实测对比。

特殊情况下,有些组织会把镜像发布在GitHub容器仓库(ghcr.io)或Red Hat的Quay.io上,这类仓库地址不能直接当作加速器用。拉这些镜像慢的时候,我一般会找替代方案:要么从Docker Hub上找同作者的官方镜像,要么在有代理的跳板机上手动拉取后导出为tar包,再导入到目标服务器。后一种方法就是docker save和docker load的用法,后面在离线场景下也经常用到。

3. 实战:Docker安装MySQL 8.0并使用

3.1 拉取镜像与首次启动

动手装MySQL 8.0之前,先说一个关键决策:官方镜像的tag怎么选。mysql:latest是当前最新稳定版,mysql:8.0会一直跟随8.0版本的小版本更新,mysql:8.0.x是锁定具体小版本。生产环境我强烈建议锁定到具体小版本,比如mysql:8.0.33,避免某天重启容器时因为latest变化导致不兼容。为了演示,这里用mysql:8.0即可。

拉取镜像并启动:

docker pull mysql:8.0 docker run -d \ --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=123456 \ -e TZ=Asia/Shanghai \ mysql:8.0

逐个参数拆解:

  • -d:后台运行容器
  • --name mysql8:给容器命名,方便后续管理
  • -p 3306:3306:把主机的3306端口映射到容器的3306端口,外部连接MySQL时连宿主机的3306即可
  • -e MYSQL_ROOT_PASSWORD=123456:设置root用户密码。这个环境变量只在首次创建数据目录时生效,容器初始化完成后就废弃了
  • -e TZ=Asia/Shanghai:设置容器时区,不设的话默认是UTC,时间比北京时间慢8个小时
  • mysql:8.0:指定镜像和标签

启动之后,检查一下容器状态:

docker ps docker logs mysql8

首次启动要做数据库初始化,日志里滚动刷新一段时间。看到ready for connections字样,说明MySQL已经就绪。

3.2 数据持久化与配置文件挂载

上面那条命令能跑通,但有一个隐藏的坑:容器一旦被删除(docker rm或重建),里面的数据全部丢失。MySQL的数据默认写在容器内部的/var/lib/mysql,容器没了,数据跟着没。正确做法是把数据目录挂载到宿主机上,用-v参数指定。

mkdir -p /data/mysql8/conf mkdir -p /data/mysql8/data docker run -d \ --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=123456 \ -e TZ=Asia/Shanghai \ -v /data/mysql8/data:/var/lib/mysql \ -v /data/mysql8/conf/my.cnf:/etc/mysql/conf.d/my.cnf \ mysql:8.0

这里做了两个挂载:数据目录挂载到/data/mysql8/data,配置文件挂载到/etc/mysql/conf.d/my.cnf。后者是MySQL官方镜像预留的自定义配置目录,会自动加载里面的所有.cnf文件。

我遇到过一个实际案例:某次给MySQL容器加配置时,直接替换了镜像内默认的my.cnf,结果启动失败,因为官方镜像的默认配置里引用了一些额外路径,被覆盖后就找不到了。所以官方镜像预留的conf.d目录,才是挂载自定义配置的正确位置。

生产环境建议在my.cnf里至少加上这几项:

[mysqld] character-set-server=utf8mb4 collation-server=utf8mb4_unicode_ci default-time-zone=+08:00 max_connections=500 slow_query_log=1 slow_query_log_file=/var/log/mysql/slow.log long_query_time=2

修改配置后重启容器:

docker restart mysql8

注意:某些参数(比如lower_case_table_names关于表名大小写敏感的设置)必须在首次初始化数据目录时生效,改配置之后重启容器是没用的。这种参数要放在第一次docker run之前就准备好配置文件,或者干脆用docker run的命令行参数传进去。这块是易错点,踩坑的人很多。

3.3 连接MySQL并验证

容器启动就绪后,先到容器里验证账号能登录:

docker exec -it mysql8 mysql -uroot -p123456

登录成功后执行:

SELECT VERSION(); SHOW VARIABLES LIKE 'character_set_server';

确认版本是8.0.x,字符集是utf8mb4,说明初始化正常。

宿主机上用客户端连接时,确保MySQL容器里已经创建了可远程访问的账号。MySQL 8.0默认的root账号只允许从localhost连接,从宿主机直接连会报权限错误。可以进入容器执行授权:

CREATE USER 'app'@'%' IDENTIFIED BY 'App@123456'; GRANT ALL PRIVILEGES ON *.* TO 'app'@'%'; FLUSH PRIVILEGES;

Navicat或DBeaver连接时填宿主机IP和映射端口3306即可。遇到Public Key Retrieval is not allowed错误,在连接参数里加上allowPublicKeyRetrieval=true就行——这是MySQL 8.0缓存SHA2密码插件导致的经典报错,不是配置问题。

还有一个容易被忽略的点:端口映射。如果宿主机3306已经被其他MySQL占用了,容器会启动失败。这种情况改用其他宿主端口,比如-p 3307:3306,外部连接用3307端口。我建议在服务器上干脆给每个容器分配独立端口段,方便追踪,防止冲突。

4. 实战:Docker部署Redis主从架构

4.1 主从结构怎么设计

Redis主从复制的作用是给读请求做水平扩展,以及给数据做个实时热备。MySQL可以一主多从,Redis同样可以。这次部署一主一从,结构是:主节点负责写,从节点同步主节点的数据并负责读流量,主节点挂掉时从节点可以手动提升为新主。虽然现在企业里普遍用哨兵或Cluster做高可用,但理解手工主从是理解哨兵机制的起点,所以先把这个搭明白。

Docker里跑主从,第一步是建一个自定义网络,让两个容器可以通过容器名互相访问,而不是依赖随时变化的IP地址:

docker network create redis-net

4.2 主节点配置

Redis官方镜像默认没有配置文件,启动时会用内置默认配置。主从复制要求绑定可访问的地址,所以得自己准备配置文件。

创建主节点配置/data/redis/master/redis.conf:

bind 0.0.0.0 port 6379 appendonly yes appendfilename "appendonly.aof" requirepass 123456
  • bind 0.0.0.0:允许任意地址连接,容器里必须这么设,否则容器外访问不到
  • appendonly yes:开启AOF持久化,防止重启丢数据
  • requirepass 123456:设置主节点密码,从节点同步时也要用这个密码认证

启动主节点:

docker run -d \ --name redis-master \ --network redis-net \ -p 6379:6379 \ -v /data/redis/master/redis.conf:/etc/redis/redis.conf \ redis:7.0 \ redis-server /etc/redis/redis.conf

这里注意,镜像后面跟的参数覆盖了Dockerfile里的默认启动命令,指定使用我们挂载进去的配置文件启动。

4.3 从节点配置与验证

从节点配置里最关键的一行是replicaof,指定主节点的容器名和端口。Docker自定义网络自带DNS解析,容器名可以当作主机名用,这是比写IP稳得多的方案——主节点重建导致IP变化时,从节点还能自动找到它。

创建从节点配置/data/redis/slave/redis.conf:

bind 0.0.0.0 port 6379 appendonly yes appendfilename "appendonly.aof" replicaof redis-master 6379 masterauth 123456 requirepass 123456

masterauth是从节点向主节点做同步认证时用的密码,必须和主节点的requirepass保持一致。如果主节点没设密码,这一行可以不要。

启动从节点:

docker run -d \ --name redis-slave \ --network redis-net \ -p 6380:6379 \ -v /data/redis/slave/redis.conf:/etc/redis/redis.conf \ redis:7.0 \ redis-server /etc/redis/redis.conf

宿主机的6380端口映射到从节点的6379,这样从外部看,主从分别是宿主机IP:6379和宿主机IP:6380。

进入从节点容器验证主从状态:

docker exec -it redis-slave redis-cli -a 123456 127.0.0.1:6379> info replication

输出里应该看到role:slave,并且master_link_status:up,slave_repl_offset不断增长。再往主节点写一条数据,从节点立刻能查到,说明复制链路已经打通。

提示:Redis 5.0之前配置从节点的指令是slaveof,5.0之后改成了replicaof。虽然老命令仍然兼容,但新配置一律用replicaof更规范。

这套主从结构里,最值得记住的教训是:容器之间通信要借助自定义网络,而不是依赖-p端口映射。自定义网络里的容器可以通过容器名互相访问,网络隔离也更安全;端口映射只是给外部访问用的入口。很多人在Docker里搭集群网络不通,问题就出在没建自定义网络、写死了IP地址。

5. 用Docker Compose从单容器走向整站编排

5.1 Compose是什么,什么时候用

手动docker run部署单个容器还好,一旦服务多了,问题就来了:十几个容器要按依赖顺序启动、要共享网络配置、要统一管理环境变量,光靠命令完全记不住,每重建一次都要翻历史找当初的参数。Docker Compose就是来解决这个问题的——它用一个YAML文件描述整套服务,一条命令完成创建、启动、停止。

Compose适用的场景很清晰:单机上的多容器编排。如果你只是调试一个MySQL容器,手动docker run完全够用;一旦涉及“MySQL + Redis + 后端应用 + Nginx”这种组合,直接上Compose。单机多服务的编排是它的舒适区,Kubernetes负责的跨多台机器的编排则是另一个量级的话题,不该用Compose硬扛。

5.2 用compose.yaml编排MySQL和Redis

Compose默认的配置文件名是compose.yaml或compose.yml,官方在2023年后的新文档里建议用compose.yaml,因为和docker-compose命令的旧缩写区分开来。写一个包含MySQL和Redis的编排文件:

services: mysql: image: mysql:8.0 container_name: app-mysql restart: always ports: - "3306:3306" environment: MYSQL_ROOT_PASSWORD: "123456" TZ: "Asia/Shanghai" volumes: - ./mysql/data:/var/lib/mysql - ./mysql/conf/my.cnf:/etc/mysql/conf.d/my.cnf networks: - app-net redis: image: redis:7.0 container_name: app-redis restart: always ports: - "6379:6379" command: redis-server /etc/redis/redis.conf volumes: - ./redis/data:/data - ./redis/conf/redis.conf:/etc/redis/redis.conf networks: - app-net networks: app-net: driver: bridge

这个文件里几个要点:

  • restart: always:容器意外退出时自动重启,服务器重启后容器也会跟着恢复,这在生产环境里几乎是标配
  • volumes使用相对路径./:Compose会基于文件所在目录解析路径,所以整个项目目录可以像代码一样提交到仓库,换台机器拉下来直接up,环境秒级复现
  • networks定义一个桥接网络,两个服务都在这个网络里,互相用服务名(mysql、redis)访问

在配置文件同级目录执行:

docker compose up -d

Compose会依次拉取镜像、创建网络、启动容器。查看状态和日志:

docker compose ps docker compose logs -f mysql

停止和清理:

docker compose down # 停止并删除容器,数据卷默认保留 docker compose down -v # 连同数据卷一起删除,慎用,数据会丢

我在项目里习惯把整套服务的目录整理成这样的结构:

app/ ├── compose.yaml ├── mysql/ │ ├── data/ │ └── conf/my.cnf └── redis/ ├── data/ └── conf/redis.conf

这样一套目录就是一个完整项目,代码、配置、数据入口都聚在一起,团队成员拉起环境只需要装好Docker、clone仓库、执行docker compose up -d,三分钟搞定。

5.3 Compose进阶:编排带主从的Redis

前面手动部署的Redis主从,用Compose编排会更清晰。在redis-master和redis-slave两个服务的配置里,replicaof可以直接写服务名redis-master,Compose网络天然支持容器名解析:

services: redis-master: image: redis:7.0 container_name: redis-master restart: always ports: - "6379:6379" command: redis-server /etc/redis/redis.conf volumes: - ./redis/master/redis.conf:/etc/redis/redis.conf networks: - redis-net redis-slave: image: redis:7.0 container_name: redis-slave restart: always ports: - "6380:6379" command: redis-server /etc/redis/redis.conf volumes: - ./redis/slave/redis.conf:/etc/redis/redis.conf networks: - redis-net networks: redis-net: driver: bridge

把主从两个容器放到Compose里管理后,最大的改变是启动顺序不用管了。Docker负责网络创建,两个服务之间只要在运行时能解析到对方就行。Compose在启动时会自动处理服务依赖(用depends_on可进一步控制顺序),整体运维成本比手动跑命令低一个量级。

6. 实操中常见的坑与排查技巧实录

6.1 端口冲突:容器起不来,日志却没报错

场景:docker run执行完,容器状态一直Exited。查docker logs发现MySQL报bind: Address already in use,但宿主机上看不到对应进程——因为占用端口的是另一个容器,而不是普通进程。

排查思路:先用docker ps -a找所有容器,再用docker inspect 容器名 | grep -i port看端口映射,最后判断是不是容器之间端口冲突。如果宿主机本身有进程占端口,用lsof -i:3306能看到。解决方案优先改新容器的宿主端口,而不是去动旧服务。

6.2 容器内时间不对,日志全是UTC

现象:容器日志时间比本地时间慢8小时。原因是基础镜像默认使用UTC时区,容器内没有同步宿主机的timezone。

解决方式:运行容器时加环境变量-e TZ=Asia/Shanghai,或者在Compose文件里给每个服务加environment: TZ: Asia/Shanghai。对某些不读TZ环境变量的镜像,可以在Dockerfile里设置时区,或者挂载宿主机的/etc/localtime。最简单稳定的方案还是环境变量,一劳永逸。

6.3 Docker日志把磁盘塞满

容器默认会把标准输出和标准错误写入json日志文件。长时间不清理,某个日志量大的容器可能占据几个GB。查磁盘使用情况:

du -sh /var/lib/docker/containers/*/*.log

应对方案有两个层面。一是运行时限制日志大小,在/etc/docker/daemon.json里配置:

{ "log-driver": "json-file", "log-opts": { "max-size": "10m", "max-file": "3" } }

配置后重启Docker,新容器的日志单个文件最大10MB,最多保留3个,老容器不受影响。二是在应用层面把日志输出到文件再挂载出来,通过宿主机日志轮转工具统一管理。生产环境建议两者配合。

6.4 容器退出码怎么看

docker ps -a里最后一列是退出码,这个数字很有用:

  • 0:正常退出
  • 1:程序运行出错,可能是配置错误、依赖缺失
  • 137:被强制杀掉,通常是OOM(内存不足)或手动docker kill
  • 139:段错误,可能是镜像兼容性问题

排查顺序:先docker logs看应用日志,再docker inspect看容器的状态和资源限制,最后结合退出码定位方向。很多情况下看到退出码就能猜个大概,137就去看内存限制,1就去看配置文件。

6.5 一句话避坑清单

  • 镜像tag锁版本,别用latest上生产
  • 数据目录务必挂载到宿主机
  • 跨容器通信用自定义网络,不要依赖IP
  • 改MySQL大小写敏感、字符集等参数要在首次初始化前配置
  • Redis主从同步里masterauth和主节点requirepass必须一致
  • Compose里down -v会删数据卷,执行前想清楚

这些坑基本都是我踩过一遍、在线上吃过亏才记住的。写在这里,希望能帮你省掉几晚上的排查时间。

我在实际项目中养成的习惯是:所有服务优先考虑容器化,但容器化不等于把配置都塞进镜像里,而是数据卷挂载、配置外置、日志系统统一管理。这套方法论一开始可能觉得麻烦,用顺手之后就会发现,环境交付从步骤文档变成了一个命令,变更回滚从手工操作变成了换镜像版本重跑,整体的效率和稳定性完全不一样。

接下来你可以在自己的项目里做个实验:把一个传统部署的MySQL迁移到Docker,再把Redis主从用Compose编排起来,跑一段时间对比看运维体验。容器化真正带来的改变,不是省了安装依赖的时间,而是让环境本身变成了可重复构建、可版本管理的资产。这个收益,用起来才能体会到。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询