Docker Compose实战:Nginx+MySQL+Redis多容器编排部署与运维

发布时间:2026/10/5 15:46:56
Docker Compose实战:Nginx+MySQL+Redis多容器编排部署与运维 很多后端项目在正式环境上的部署形态早已不是一个进程走天下而是 Nginx 扛流量入口、MySQL 存核心数据、Redis 做缓存/会话/队列的多服务组合。Docker Compose 的价值在这一刻就非常具体了它不需要你写一堆 docker run 拼命令也不需要一个一个容器手动建网络、手动设置依赖顺序一份 docker-compose.yml 就能把整套服务的拓扑、配置、网络、启动顺序全部声明清楚然后 docker compose up -d 一条命令拉起整个世界。这篇文章从零开始基于 Nginx MySQL Redis 这个最典型、最高频的服务栈完整走一遍多容器应用的设计、编排、部署和日常运维。适合已经掌握 Docker 基本操作、想进阶到多个容器协同工作阶段的开发者和运维新手也适合准备把本地开发环境改造成 Compose 管理、或者需要快速搭建一套内部测试环境的朋友。我会把每段配置的为什么这么做一并拆开讲而不是只给你一份能跑就行的 YAML。1. 为什么这套服务栈搭配 Docker Compose而不是 shell 脚本或 docker run1.1 docker run 的一次性思维解决不了服务间的依赖关系早年间我部署这套服务栈干的是最笨的活装好 MySQL 后手动启动再装 Redis再配 Nginx然后写一份 systemd 服务脚本让它们开机自启服务之间的通信靠固定 IP 和端口。这套流程最大的问题在于——容器世界的临时性和平台迁移的不可控。你在一台机器上调好的 docker run 参数换到另一台机器上可能要改 IP 段、改挂载路径、改时区设置。而且docker run 本身就是一次性思维它适合临时跑一个容器做验证但不适合描述一整套微服务架构。相比之下Docker Compose 把三个服务的配置集中存放一份文件写完拉到任何一台装有 Docker Compose 的机器上就能跑。这种声明式的管理方式本质上和基础设施即代码Iac的理念一脉相承。服务和服务的依赖关系depends_on、网络关系和资源限制都变成文件里可以审阅、可以版本控制的文本。1.2 单机多容器场景下Compose 的复杂度控制能力有人可能要说Kubernetes 不也是干这个的吗没错K8s 确实编排能力更强、生态更完整但我们要诚实面对一个事实对于单机、多容器十几个以内的中小型项目部署一套 K8s 的维护成本远高于收益。你需要管理 etcd、控制平面、工作节点还需要掌握 Pod、Service、Ingress 等一整套抽象概念。而 Docker Compose 的模型足够简单——服务service、网络network、卷volume、容器container四者关系一目了然学习曲线平缓而且资源开销小。正因如此Compose 非常适合这套 Nginx MySQL Redis 服务栈三个服务Nginx 需要暴露 80/443 端口给外部MySQL 需要持久化数据目录Redis 需要挂载配置且按需开启持久化。把它们放在同一个自定义网络中不需要暴露 MySQL 和 Redis 的端口给宿主机安全性也更好。1.3 服务编排在 Compose 语境下的真实含义很多文章一谈到编排就要扯到服务发现、负载均衡、自动重建这类复杂度偏高的概念。在 Compose 的世界里编排落到实操上其实是这四件具体的事定义服务每个容器基于什么镜像、什么启动命令、什么环境变量。连接服务通过项目内的自定义网络让容器之间通过服务名互相访问例如 Nginx 里代理的目标主机可以直接写成http://backend:8080而不是http://172.17.0.3:8080。管理生命周期start、stop、restart 批量操作而不是一个个 docker ps 找容器 ID。处理配置变更改完 YAML 后一条 docker compose up -d只有配置有变动的服务会被重建其他服务保持不动尽量不影响运行中的业务。搞清楚这四点后面看配置文件就不会云里雾里。2. 环境准备Docker Compose 的安装方式与版本选择2.1 确认 Docker 与 Compose 插件的关系先提醒一个容易踩的坑从 Docker 20.10 之后的版本开始Docker Compose 有了两个存在形式。一个是旧的docker-compose独立二进制使用 Python 写的 v1 版本一个是新的docker composeDocker CLI 插件用 Go 写的 v2 版本注意中间没有横杠。我强烈建议使用docker composev2 插件版因为它的安装更干净更新跟随 Docker 版本走语法兼容性也更好。国内很多老教程还在教pip install docker-compose那个 v1 版本已经停止维护很久了真没必要再用。可以通过docker compose version验证插件是否可用。如果命令提示不存在两个办法一是通过发行版包管理器安装。比如在 Debian/Ubuntu 上较新版本的系统用apt install docker-compose-plugin老版本系统可能需要先更新 docker 仓库索引CentOS/RHEL 用dnf install docker-compose-plugin。这样安装的是docker compose插件。二是下载二进制手动安装到/usr/local/lib/docker/cli-plugins/目录并赋予可执行权限。这个方法适合 Docker 版本较旧或者系统发行版较老、仓库里没有 compose 插件包的情况。2.2 镜像的版本选择策略以及为什么要锁版本基础环境准备好之后第一个要面对的问题就是三个服务的镜像选哪个 tag我看到很多新手直接nginx:latest、mysql:latest、redis:latest这种写法开发阶段没问题一旦上了生产环境就埋雷。最大的问题是不可复现今天 pull 下来的 MySQL 可能是 8.0.36三个月之后在另一台机器上 pull 就变成了 8.0.44小版本之间的默认配置、兼容性差异可能让你排查得头疼。我个人的习惯是在 compose 文件里锁死次要版本比如 mysql:8.0、redis:7.2、nginx:1.26。这样既不会频繁被大版本更新打断节奏又能拿到该大版本内的安全更新。对于更加严格的生产环境甚至可以精确到mysql:8.0.36、redis:7.2.4这样的小版本升级前先在测试环境验证。另外如果项目本来就在内网环境或者需要离线交付提前docker pull镜像再导出为 tar 包也是常见操作锁版本在这种情况下就更有必要——避免导出和运行时之间镜像版本已经不同的尴尬。2.3 目录结构规划一个项目一个文件夹别让配置散落Compose 的目录设计直接关系到可维护性。我不建议把三个服务的配置全部堆在项目根目录下更不建议把 nginx 配置、mysql 初始化脚本、redis 配置全部扔进同一个文件平铺。比较成熟的目录结构是这样myapp-stack/ ├── docker-compose.yml ├── .env # 环境变量存放端口、密码、版本号等可变参数 ├── nginx/ │ ├── conf.d/ │ │ ├── default.conf # 站点配置 │ │ └── ssl/ │ │ ├── example.com.crt │ │ └── example.com.key ├── mysql/ │ ├── conf.d/ # 自定义 MySQL 配置片段 │ │ └── my-custom.cnf │ └── init/ │ └── init.sql # 首次初始化脚本仅首次启动时执行 └── redis/ └── redis.conf这样一来配置是跟着项目走的——你在项目目录里敲docker compose down之后把整个文件夹打包拷到另一台机器上一个 up 就能恢复整套环境。这也侧面降低了环境不一致导致的部署事故。3. docker-compose.yml 的核心设计逐段拆解3.1 顶层结构version、services、networks、volumes 的职责划分一份标准 Compose 文件的顶层元素严格来说就四个name项目名、services服务定义、networks自定义网络、volumes命名卷。新版 Compose 已经不再强制要求写version: 3.8这类字段你写了它也只是兼容性提示不会有什么实际影响。我习惯直接省略 version避免和真实版本号混淆。项目名是一个容易被忽略的细节。默认情况下Compose 用项目目录名即文件夹名作为项目名而项目名会作为容器名前缀和网络名前缀。也就是说如果你在myapp目录里跑起来容器名就类似myapp-nginx-1网络就类似myapp_default。这带来一个实际好处三个不同的 Compose 项目可以放在同一台机器上它们各自网络隔离、服务名互不冲突只要端口不同即可并行运行。3.2 编排 Nginx端口映射、配置挂载与依赖关系Nginx 是这套栈里和宿主机交互最频繁的一个容器因为外部流量要通过它进来。核心配置大概是这样的services: nginx: image: nginx:1.26 container_name: myapp-nginx restart: always ports: - 80:80 - 443:443 volumes: - ./nginx/conf.d:/etc/nginx/conf.d:ro - ./nginx/ssl:/etc/nginx/ssl:ro - ./www:/usr/share/nginx/html:ro depends_on: - backend networks: - app-network这里有三个点要说透第一ports右侧的映射格式是宿主机端口:容器端口。因为 MySQL 和 Redis 不直接暴露给外部它们在networks里的 IP 其实由 Docker 内部网络自动分配。Nginx 对外只暴露 80 和 443业务端口不要随意开这是最小暴露面原则。第二restart: always配合容器异常退出的场景很关键。我见过有人写restart: unless-stopped这两者的差异在于unless-stopped在手动停止后不会自动拉起而always在任何情况下都会尝试拉起除非手动 stop 后明确 down 移除容器。对于反向代理这类必须 7×24 在线的组件always几乎是最优选。第三Nginx 目录内的配置我用:ro只读挂载避免容器运行时改写宿主机文件这也是一个值得养成的习惯——配置挂载只有读的需求没有写的需求只读挂载能防止误操作。至于这里的depends_on依赖我把它放在 Nginx 上而不是反过来实际上是一个比较考究的细节Nginx 依赖于 backend 服务假设你还有一个应用服务容器这里刻意预留了扩展位意味着它会在 backend 启动完成之后再启动。如果目前没有 backend这条可以先删掉但保留这个结构会提醒你——反向代理的依赖永远指向业务服务而不是指向 MySQL/Redis。3.3 编排 MySQL数据卷、初始化脚本和时区配置MySQL 是整个栈中最娇贵的一个服务因为它的数据落盘、字符集、时区、初始化密码等一旦设错后期改起来非常痛。这是我的推荐配置mysql: image: mysql:8.0 container_name: myapp-mysql restart: always environment: MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} MYSQL_DATABASE: ${MYSQL_DATABASE:-appdb} MYSQL_USER: ${MYSQL_USER:-appuser} MYSQL_PASSWORD: ${MYSQL_PASSWORD:-apppass} command: - --character-set-serverutf8mb4 - --collation-serverutf8mb4_unicode_ci - --default-time-zone8:00 volumes: - mysql_data:/var/lib/mysql - ./mysql/conf.d:/etc/mysql/conf.d:ro - ./mysql/init:/docker-entrypoint-initdb.d:ro networks: - app-network先解释环境变量MYSQL_ROOT_PASSWORD必须设置且不要用默认值我习惯把这类敏感信息抽到.env文件里再通过${VAR}引用而.env文件本身加入.gitignore。MYSQL_DATABASE会在首次启动时自动创建MYSQL_USER和MYSQL_PASSWORD用于创建一个具备该库权限的普通账号业务代码用这个账号连接而不是用 root这是最基础的权限分离。再说command这段。很多 MySQL 部署后中文乱码、时间格式不对根源就在字符集和时区。我在 command 里强制指定utf8mb4和utf8mb4_unicode_ci排序规则避免 MySQL 8.0 默认的utf8mb4_0900_ai_ci与自己本地环境的排序规则预期不一致。时区写8:00是硬编码中国时区如果你部署在海外服务器或者有多时区需求可以改为00:00或者通过环境变量动态指定。最后是数据卷的分配逻辑。mysql_data:/var/lib/mysql用的是命名卷Docker 会在宿主机上找一个专门的目录存放数据你不需要关心它在哪docker volume命令可以查看和管理。之所以不用 bind mount 把宿主机目录直接挂进/var/lib/mysql是因为 MySQL 对目录的属主、权限很敏感bind mount 很容易因为属主不一致导致启动失败。命名卷则是由 Docker 自发管理权限踩坑概率显著低。3.4 编排 Redis内存数据库的持久化与密码保护Redis 的配置相对轻量但有几个关键点不能漏redis: image: redis:7.2 container_name: myapp-redis restart: always command: [redis-server, /usr/local/etc/redis/redis.conf] volumes: - ./redis/redis.conf:/usr/local/etc/redis/redis.conf:ro - redis_data:/data networks: - app-networkcommand明确指定以配置文件方式启动而不是默认的无配置启动。默认启动的 Redis 是不开启持久化的一旦容器删除缓存数据全部丢失——对缓存场景也许无所谓但如果 Redis 被你用来做分布式锁或临时状态存储这数据丢起来就麻烦了。commands里对应的配置文件我通常写这几项requirepass yourstrongpassword appendonly yes appendfsync everysec maxmemory 512mb maxmemory-policy allkeys-lrurequirepass设置访问密码避免 Redis 默认无认证裸奔。appendonly yes开启 AOF 持久化appendfsync everysec数据安全性介于每秒写盘和性能之间比较均衡。缓存模式下maxmemory与淘汰策略配合保证 Redis 不会吃满服务器内存导致整机卡死。redis_data:/data这个命名卷对应 Redis 的持久化目录和 MySQL 一样用它而不是 bind mount理由相同。3.5 网络模型为什么推荐内部网络而非默认 bridge三个服务默认在同一个 Compose 项目中Compose 会自动创建一个默认网络所有服务都会加入。但我仍然显式声明了一个自定义网络app-network并且让所有服务都加入它。显式声明的价值有两个一是语义清晰。后续新增服务时如果不需要和这栈里的 MySQL 通信就不加这个网络实现了逻辑上的分组隔离。二是方便扩展。比如将来你需要多网卡场景Nginx 同时连接外部网络和内部业务网络显式网络声明给了你自由调整拓扑的空间。还要强调一点MySQL 和 Redis 的容器在 compose 项目里通过服务名互相访问Nginx 如果配置里写proxy_pass http://backend:8080Compose 内置的 DNS 会自动解析backend这个名字为对应容器的内网 IP。这个能力是内置的不需要额外配 hosts —— 这也是很多从物理机部署迁移过来的人最先感到爽的地方。4. 环境变量、配置文件的注入方式和首次初始化4.1 .env 文件把密码、版本、端口从 YAML 中剥离很多人会把密码直接写死在 docker-compose.yml 里我强烈反对。因为这个文件常常被收进 Git 仓库一次误提交密码就泄露到所有协作者手里。正确做法是用.env文件存变量然后在 YAML 里用${VAR}引用。Compose 在启动时会自动读取项目目录下名为.env的文件。例如.envMYSQL_ROOT_PASSWORDChangeMe_Prod_2024 MYSQL_DATABASEappdb MYSQL_USERappuser MYSQL_PASSWORDAnotherStrongPass REDIS_PASSWORDRedisPass_2024推荐给变量合理设置默认值比如MYSQL_DATABASE${MYSQL_DATABASE:-appdb}这样即使.env文件缺失服务也能用一个安全保守的默认值启动而不是直接报错。Compose 的变量替换支持很多默认值语法灵活运用能有效降低我明明配置了但起来报错的概率。4.2 MySQL 首次启动初始化脚本机制一个比想象中简单的设计Docker 官方 MySQL 镜像有一个非常实用的机制在容器第一次启动时会自动执行/docker-entrypoint-initdb.d/目录下的 .sh、.sql、.sql.gz 文件。因此你可以在这个目录挂载一个init.sql里面写建表语句、初始数据、索引等。注意前提条件只在数据卷为空时执行。也就是说如果mysql_data卷已经存在且里面有数据容器启动不再执行脚本。如果你想修改初始化脚本得先清空旧卷通常docker compose down -v但因为down -v会连数据一起删务必先备份。脚本执行顺序按照文件名排序如果你有多个初始化文件用01_init_schema.sql、02_seed_data.sql这样的前缀名控制顺序。举一个初始化的例子CREATE TABLE IF NOT EXISTS users ( id BIGINT AUTO_INCREMENT PRIMARY KEY, email VARCHAR(255) NOT NULL UNIQUE, nickname VARCHAR(64) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci;4.3 时区、字符集和 MySQL 默认的坑MySQL 8.0 默认时区是 UTC 而不是本地时区这会导致日志时间、NOW()的结果和业务预期差 8 个小时。在command里写--default-time-zone8:00解决这个问题。我更推荐用环境变量TZAsia/Shanghai来设置容器时区这样不仅 MySQL其他服务也能共享同一时区语义。字符集方面MySQL 5.7 时代大家习惯在 my.cnf 里写[mysqld] character-set-serverutf8mb4在 Compose 的command字段里直接写带--前缀的参数即可它会覆盖默认配置。但如果你同时在挂载目录里放了自定义.cnf文件注意参数冲突问题——同一配置项出现多次后读取的覆盖先读取的而命令行参数优先级最高最容易遮住文件配置。5. 启动、验证、日常操作命令速查5.1 首次启动docker compose up -d 之后的黄金三连查配置全部就绪后在项目目录执行docker compose up -d这个命令首次执行时会自动拉取镜像创建网络、卷并按依赖顺序启动服务。看到容器起来之后我习惯立刻做三项检查看三容器的状态docker compose ps输出里每个服务的 STATUS 列应该是Up而且后面的时间表明它们持续运行了一段时间而不是几分钟就重启一次。如果看到Restarting说明容器内服务启动失败在反复重启此时需要查日志。看日志确认服务真正就绪docker compose logs --tail100 mysql docker compose logs --tail50 nginx docker compose logs --tail50 redis注意 MySQL 的就绪标志是日志里出现ready for connections.而不是容器起来了这个表象。容器起来只代表进程被启动MySQL 内部还需要初始化数据目录、启动回滚流程这个耗时可能高达几秒到几十秒。附带上-f实时跟读日志能更直观地看到启动过程。验证服务间通信docker compose exec nginx ping mysql不建议直接 ping因为很多精简镜像里没有 ping 命令。更通用的是借助docker compose exec进入容器跑一段工具命令。例如验证 Nginx 到 MySQL 的连通性可以进入 Nginx 容器后用curl请求自己的配置页面再在 MySQL 容器里执行mysql -u appuser -p登录。更省事的是直接测试网络端口docker compose exec redis redis-cli -a yourpassword INFO server docker compose exec mysql mysql -uappuser -papppass -e SELECT 1;5.2 常用运维命令清单这里整理一份高频命令表都是我日常用到烂熟的命令作用备注docker compose up -d创建并后台启动所有服务配置变更后的重建也用它docker compose ps查看服务状态比 docker ps 多了项目分组docker compose logs -f [service]跟踪日志加服务名可以只看单个服务docker compose exec [service] bash进入容器执行命令交互式调试首选docker compose restart [service]重启单个服务不重建网络和卷docker compose stop停止所有服务保留容器和网络可以再 startdocker compose down移除容器和网络默认不删除卷docker compose down -v移除容器、网络和卷危险命令会删数据docker compose config校验并展开最终的配置变量替换后打印 YAML特别提醒docker compose down -v里的-v会把所有命名卷一并删除如果你没有做数据备份这个命令基本等于格式化了整个数据库。日常操作中我几乎不用它除非确信当前环境是一次性的、数据无所谓。5.3 日常扩容或修改配置的正确姿势改配置不可避免。我建议遵循这个标准流程修改 YAML 文件或挂载的配置文件先docker compose config检查语法变量是否能正确替换再docker compose up -dCompose 会比较期望状态和当前状态自动重建有变化的服务未变化的不动这个流程比先 down 再 up安全得多因为你不会把没改动的服务也重建一遍。像 Nginx 改配置文件这种场景还有一个更轻量的方式docker compose exec nginx nginx -s reloadNginx 支持热加载配置不需要重启容器。这也是 Compose 场景中提升可用性的关键技巧——生产环境下一次无感知 reload 永远比重启容器更受欢迎。6. 实际操作中的经典踩坑实录6.1 MySQL 容器启动失败的重启循环这个坑我在迁移旧项目时踩过很多次。现象是docker compose ps显示 MySQL 的状态始终是Restarting看日志一看是各种初始化报错。常见根因有三个第一数据目录权限不对。如果你在/var/lib/mysql上做过 bind mount宿主机的权限很可能不是 MySQL 容器内mysql用户所期望的镜像内的用户 UID 是 999。解决方法放弃 bind mount改用命名卷如果非要用目录给目录设置属主为999:999并注意 SELinux 上下文。第二自定义 cnf 文件里的配置项写错。比如你在[mysqld]段里写了port3307但 Compose 里容器端口仍然是 3306启动后客户端连接全部失败。这类问题用docker compose config是查不出来的因为它只是渲染了配置不会校验 MySQL 语义。应对方法是用docker compose exec mysql mysql --help查看 MySQL 启动实际读取的参数。第三初始化密码与已有数据冲突。如果你先启动过一版 MySQL数据卷里已经有 user 记录后来修改了MYSQL_ROOT_PASSWORD环境变量这个新密码不会自动生效。MySQL 初始化密码只在第一次数据卷创建时写入之后改环境变量只是看个寂寞。这也侧面验证了锁版本 卷持久化策略的必要性——你可以在 yaml 里随意改密码变量但真正影响数据的是卷里的历史状态。6.2 Nginx 替换 SSL 证书不生效的问题这个场景相当高频你替换了 Nginx 挂载目录下的证书文件也nginx -s reload了但浏览器访问时返回的仍然旧证书。问题根源基本都在挂载方式上——如果你用 bind mount 挂载整个/etc/nginx/ssl目录宿主机上替换文件后容器内看到的的确是新文件reload 按理说应该生效。但如果你的挂载粒度是文件级比如volumes: - ./nginx/ssl/example.com.crt:/etc/nginx/ssl/example.com.crt:ro - ./nginx/ssl/example.com.key:/etc/nginx/ssl/example.com.key:ro这时候替换宿主机文件、再执行docker compose restart nginx你会发现容器里的文件依然是旧内容。别急着怀疑 Docker先看你是不是用vim直接编辑的挂载文件。很多常见文本编辑器为了安全会先写临时文件再 rename 覆盖原文件而这个 rename 操作在 bind mount 场景下会把文件更换而非内容修改暴露给容器导致容器内挂载的文件句柄仍然指向旧 inode。处理方式很简单替换证书后不是 reload 而是 restart 容器让容器重新挂载文件。生产环境操作 SSL 证书时先在测试环境验证新证书能正常加载再在线上 commit 更新这样能避免用户在错误证书页面上留下差评。6.3 MySQL SSL 连接报错本地测试和生产环境的差异热词里有一条 mysql ssl连接错误实际项目中我也遇到过。场景通常分两类一类是应用通过 SSL 连接 MySQL 时报SSL connection error: unknown error number。排查思路先确认 MySQL 容器是否启用了 require_secure_transport 选项。如果你在自定义 cnf 里写了require_secure_transportON那么任何非 SSL 连接都会被拒绝。但容器里默认没有配置 CA 证书、服务端证书启用这个参数之后反而会让普通客户端连接不上。解决方案要么为 MySQL 配置正式的 SSL 证书要么在测试环境先关闭这个参数确保应用能正常连通再谈加密传输。另一类是本地工具连接远程 MySQL 时报 SSL 相关错误比如mysql命令提示证书链不完整。这种情况多半是你用 SSL 连接时没指定--ssl-modeDISABLED/--ssl-modePREFERRED而 MySQL 服务端的自签名证书不完全适配客户端的 CA 校验。单机调试阶段建议显式关闭 SSL 或使用PREFERRED模式不要让加密握手变成排错障碍。6.4 Redis 分布式锁、主从复用一套密码的注意事项很多人会用 Redis 做分布式锁比如 Redisson 或手写 SET NX EX。如果 Redis 开启了requirepass而你的客户端配置里没写密码锁写入会直接报NOAUTH Authentication required然后锁流程静默失败最终导致并发控制形同虚设。因此Redis 的密码配置要和业务代码里的连接字符串保持同步我建议把密码统一放在配置中心或环境变量里而不是散落在十几台服务器的 Secret 文件里。如果你进一步做 Redis 主从热词里也有 docker 安装 redis 主从注意主从都挂同一个 compose 项目网络下时从节点复制是通过配置里的replicaof指向主节点名称实现的。例如redis-slave: image: redis:7.2 command: [redis-server, /usr/local/etc/redis/redis-slave.conf] depends_on: - redis配置文件里写着replicaof redis 6379这里的redis会被 Compose 内置 DNS 解析为主节点容器。但要注意主节点开启了requirepass从节点也必须配置masterauth否则复制握手会因为认证失败而不断重试日志里会刷MASTER aborted replication业务上看起来缓存数据就是时有时无。主从复制在容器世界里并不是开箱即得的这些坑只有踩过才会记录在案。6.5 Windows / macOS 本地环境的路径兼容问题热词里有 macos 安装 redis、麒麟v10 x86——64 安装docker 26和docker compose、本地虚机等多环境部署需求。这说明不少人在本地开发机上先跑这套栈。本地 Docker DesktopmacOS/Windows和 Linux 环境之间最容易栽跟头的是挂载路径写法 。macOS 和 Windows 的 Docker Desktop 在 bind mount 时路径要写成绝对路径或者在 Compose 里使用相对路径。相对路径从 compose 文件所在目录计算这点没问题但千万别把 Windows 的盘符路径直接写进 YAML。另外本地和虚拟机之间部署同一套 Compose 时如果宿主机端口有冲突比如本机 3306 被原生 MySQL 占用了在${MYSQL_PORT:-3306}里换一个端口即可。真正要小心的是docker compose命令找不到 —— 旧版本 Docker Desktop 需要手动开启 Compose 插件部分 Linux 发行版需要额外安装。这条坑我用一句顺口溜记住先看 docker info再看 compose version任何时候先确认 CLI 插件是否就位再谈排错。7. 数据备份、恢复与日常监控7.1 MySQL 数据备份最朴素的方案容器的备份思路和物理机不太一样。物理机上你可以在系统层面直接复制数据目录但容器环境里我建议还是用 mysqldump 或 MySQL 8 的mysqlbackup走逻辑备份因为直接复制底层数据文件很容易因为缓存没有刷盘导致数据不一致。最简单的备份命令就是利用docker compose exec在容器内执行docker compose exec mysql sh -c exec mysqldump --all-databases -u root -p$MYSQL_ROOT_PASSWORD backup_$(date %F).sql然后定期把这份 SQL 文件同步到异地。恢复方式也很标准cat backup_2024-06-01.sql | docker compose exec -T mysql mysql -u root -p注意-T参数它禁止分配伪终端即 Disable pseudo-tty allocation管道操作时避免乱码。备份前要克服一个心理障碍不要复制卷目录。直接cp -r /var/lib/docker/volumes/...这种操作大概率会因为数据目录处于活跃读写状态而得到一个不一定能恢复的备份。逻辑备份虽然全量导出慢一点但恢复可靠度最高。7.2 Redis 的持久化文件备份策略Redis 在容器里开启 AOF 后它会有一个.aof或者.appendonlydir目录存放持久化文件。备份方式推荐用 Redis 自带的BGSAVE配合命名卷的快照docker compose exec redis redis-cli -a yourpass BGSAVE执行后Redis 会 fork 子进程生成 RDB 快照到/data/dump.rdb然后我们把这个文件从容器里拷贝出来docker cp myapp-redis:/data/dump.rdb ./redis-backup-$(date %F).rdb如果在同一台宿主机上备份几个容器docker cp是最简单直接的方式。如果还要考虑异地备份那就走脚本定期docker cp rsync。7.3 日志轮转与磁盘占用控制容器日志默认由 Docker 驱动接管如果不加限制一个长时间不重启的容器可能把日志文件撑到几个 GB。生产环境建议在 Compose 文件里为每个服务配置日志驱动services: nginx: ... logging: driver: json-file options: max-size: 10m max-file: 3这一段在很多模板里被人忽略但我觉得它应该成为服务定义的标配。max-size: 10m表示单份日志文件上限 10MBmax-file: 3表示最多保留 3 份总量封顶 30MB。这样的好处是既让现场有一定的日志回溯空间又不至于因为日志把磁盘塞满而拖垮整个宿主机。对于 MySQL 这种日志量较大的服务还可以考虑把 error log 和 slow query log 显式配置到文件挂载目录配合logrotate宿主机定时清理。Docker 自身的日志驱动压不压缩其实无所谓主机磁盘管理才是关键。7.4 日常健康检查容器活着不代表服务健康docker compose ps显示 Up 只是容器级别的状态不代表服务能正常响应业务请求。更可靠的做法是在 Compose 里定义healthcheck让容器内部定期做自检并把健康状态反射到docker compose ps上。比如 MySQL 的健康检查可以写成mysql: ... healthcheck: test: [CMD, mysqladmin, ping, -h, localhost, -u, root, -p${MYSQL_ROOT_PASSWORD}] interval: 30s timeout: 5s retries: 3 start_period: 60s这样docker compose ps会显示 MySQL 是否 healthy甚至可以配合depends_on的condition: service_healthy让依赖它的服务一定等它真正就绪后再启动。这套机制解决了depends_on 只等容器启动、不等服务可用的经典痛点。Redis 的健康检查更简单redis: ... healthcheck: test: [CMD, redis-cli, ping] interval: 10s timeout: 3s retries: 3实际运维时我会在 Compose 文件里为每个核心服务配好 healthcheck然后让监控脚本每隔几分钟轮询docker compose ps的健康状态列出现 unhealthy 就报警。网络层面如果服务互相调用的间隔很长单靠 Compose 内 DNS 无法感知对方是否健康配合 healthcheck 至少能在早期发现服务假死。7.5 更新的正确节奏滚动升级和回滚思路这套服务栈的滚动升级和单容器不同。例如 MySQL 要升级小版本不建议直接改 image tag 然后up -d而应该先做全量备份修改 image tag例如mysql:8.0.36变成mysql:8.0.40docker compose pull mysql提前拉取新镜像docker compose up -d mysql观察日志和业务联通性如果异常立刻docker compose up -d改回旧 tag 执行回滚Nginx 和 Redis 的更新类似但 Redis 由于内存数据是瞬时的更新前先BGSAVE一下提升回滚后的数据一致性。整体上容器世界给了你一个很大的便利镜像在就是可以随时回到旧版本前提是你没有删除旧镜像。我建议更新前先docker tag备份当前镜像 tag 到:backup-YYYYMMDD最大程度降低意外。8. 如何把这个栈扩展到真实业务项目8.1 增加应用服务如 Java/Node/Python的接入方法这套 Nginx MySQL Redis 最终一定是为业务服务的所以 Nginx 如何反代到业务容器 是扩展的第一步。比如你有一个 Spring Boot 容器它监听 8080 端口在 Nginx 的站点配置里可以这样写server { listen 80; server_name your-domain.com; location / { proxy_pass http://spring-app:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } }spring-app是 Compose 里另一个服务的名字。只要它加入同一个app-networkNginx 就能通过服务名解析并反向代理。这个服务名即域名的机制是 Compose 编排最优雅的地方——你不再需要为每个容器固定 IPDNS 自动更新新增节点不用改网络拓扑只需要在 YAML 中加一个服务名。8.2 MySQL 连接池和 Redis 连接串的配置经验应用容器和 MySQL 连接时重点关心连接池配置。比如 Java 的 HikariCP、Node 的 mysql2 连接池初始连接数、最大连接数、空闲超时时间这些参数要和容器资源限制匹配。在 Compose 里我通常会给 MySQL 设置资源上限例如mysql: ... deploy: resources: limits: memory: 2g cpus: 1.5这个限制会通过容器运行时告诉 MySQL让它不要把内存吃穿。连接池配置里最大连接数和max_connections系统变量要匹配否则 MySQL 报 Too many connections 只是时间问题。Redis 的连接串则统一走密码 服务名例如redis://:yourpassredis:6379/0。注意把项目环境的 Redis 密码放到 .env避免在代码仓库里硬编码。8.3 给 Compose 加一层配置环境隔离我见过很多团队只有一个 docker-compose.yml 文件在本地、开发、生产之间靠改文件内容切换。这不太利于稳定。更推荐的做法是使用 Compose 的 override 文件机制docker compose up -d # 读取 docker-compose.yml docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d主文件放公共配置覆盖文件放某环境特有的配置比如端口映射、日志级别、资源限制。环境变量层面也可以用--env-file指定例如docker compose --env-file .env.prod up -d实践下来这套主文件 环境覆盖 环境变量文件的组合能让同一份 Compose 配置安全地跑在多个环境而不用复制粘贴多份 YAML 导致同步不及时的问题。9. 我最近在真实项目里的一次完整落地复盘9.1 背景与目标上个月我把一个原来跑在裸机上的老项目迁到了 Docker Compose 环境。三个老服务分别是 Nginx 1.18、MySQL 5.7.44、Redis 5.0业务是几个高并发的 API 服务。迁移前最担心的就是平滑度既要保持原来的行为又要利用容器化带来的版本一致性、快速部署和回滚能力。9.2 迁移步骤拆解第一步我没有直接改生产而是在一台闲置测试机上搭了整套 Compose 环境把线上数据通过 mysqldump 导入跑了一周的业务仿真脚本对比性能和日志。第二步针对 MySQL 5.7 和 8.0 的差异做了一轮兼容测试。因为原库用了utf8mb4但排序规则是 5.7 的老版本迁移到 8.0 后需要对每个表执行ALTER TABLE ... CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci或者调整建表语句。这个过程最容易漏掉的是存储过程和函数——它们对排序规则和 SQL_MODE 的兼容性更为敏感。第三步把 Nginx 的反代配置从旧机器上平移到 Compose 挂载目录并确认proxy_pass的目标从 IP 改成了服务名。第四步Redis 数据没有做迁移因为它是纯缓存性质掉几秒用户感知不强。先挂上一个空的 Redis 容器等业务流量把热点键重新缓存起来即可。9.3 迁移过程遇到的坑和应对迁移过程果然不是一帆风顺。MySQL 升级到 8.0 后原来 5.7 的连接工具比如老版本 MySQL 客户端因为认证插件改成caching_sha2_password而连不上。解决办法有两个在 MySQL 用户里显式改为mysql_native_password或者强制所有客户端升级。生产环境我建议优先升级客户端因为mysql_native_password在 MySQL 8.0 虽然仍有兼容性支持但未来会有被移除的风险。Nginx 从 1.18 升到 1.26 之后原来配置里的一些 deprecated 语法新版本也能识别但要注意如果你用了ssl_protocols的旧写法新版本可能直接警告甚至拒绝启动。这个阶段docker compose config只校验结构不校验 nginx 语义所以我用了一个笨办法在容器里执行nginx -t检测配置语法再 reload。9.4 迁移后的效果上线后运行了两周最大的感受不是性能提升其实容器化本身不会带来魔法般的性能飞跃而是运维心智负担的下降一台机器上几套 Compose 项目的版本一目了然发布新版本只要改 image tag 然后 up -d回滚只要改回旧 tag 再 up -d整个过程不超过两分钟。相比之下以前手动改配置文件 重载服务 验证的心智负担高得多。10. 从这套服务栈延伸出去的下一步方向10.1 引入 Traefik / Caddy 作为七层网关一旦服务数量变多比如加了前端、后端、管理台三个 Nginx site在 Compose 里继续堆 Nginx 配置就渐渐不灵活了。此时可以考虑将 Nginx 替换为 Traefik 或 Caddy它们和 Docker 集成更强可以动态发现 Compose 服务并自动路由省去手动改 Nginx conf 再 reload 的流程。不过我要强调不要一上来就为了新潮而换网关。Nginx 对于刚上手这套栈的团队是最稳妥的选择而且网上资料多、踩坑经验充足。10.2 从单机 Compose 到 Swarm / K8s 的迁移路径当业务流量大到单机撑不住时自然要考虑横向扩展。这时候 Docker Compose 的优势——单机配置简单——会反过来成为它扩展单机的短板因为你需要考虑跨主机的服务发现、存储共享、负载均衡等问题。往 Swarm 迁移Compose 文件大部分能复用只是要把deploy下加replicas: 3补充容器调度和状态管理往 K8s 迁移则要重写为 Deployment / Service / Ingress 清单迁移成本显著上升。我的建议是如果业务还处于一天几千请求的状态专心用 Compose 就够了别提前背上 K8s 的复杂运维债。等真正需要水平扩展的时候再逐步引入管编排平台这样每一步都有明确的收益和验证点。10.3 用 Compose 跑本地开发环境的独特优势这套服务栈不止对生产有价值对本地开发也是神器。以前新同事入职后要在自己电脑上装 MySQL、Redis、Nginx屡屡遇到版本不一致、权限配置复杂等问题。现在只要仓库里带一份 docker-compose.yml 和.env.example新同事把.env.example复制成.env一条docker compose up -d整套依赖服务全部就位连数据库表结构都通过 init 脚本自动建好。要清理环境直接docker compose down宿主机不会残留一堆卸载不干净的本地服务。这个过程省下的 onboarding 时间比我写任何技术文档都高效。10.4 最后的实操建议根据我个人的经验如果你正在学习 Docker Compose 多容器编排建议不要去抄一大段复杂的模板而是先从这个最小栈开始亲手把 docker-compose.yml 写一遍再逐步调整。学习路径上以下几点优先级最高先吃透 volumes 和 networks 这两个概念它们决定了数据安全和网络隔离再学会看日志、用 exec 进容器调试这比背命令重要得多然后考虑把 MySQL 的初始化脚本、Redis 的持久化应用起来让数据真正活在容器化环境里最后再研究健康检查、资源限制、日志轮转这些都是生产环境运维的核心我早期犯过最蠢的错误就是把所有配置堆在一个 YAML 文件里然后每次改配置都小心翼翼担心把别的东西搞坏。现在我会刻意把配置拆分成 compose 文件 环境配置文件 各服务的子配置目录整体结构清晰排错也快得多。希望这份基于 Nginx MySQL Redis 服务栈的 Compose 实践能让你在动手部署的时候少走一些弯路做出的环境更接近可以直接上生产的标准。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询