本节摘要:生产环境部署考量是一组决定"Compose 能不能上、上了怎么不出事"的决策。核心是两件事:第一,认清 Compose 的定位边界——它是单机编排工具,适合小型应用与单节点部署;第二,把开发模板升级成生产配置——固定镜像版本、设置时区、配置重启策略、加资源限制与健康检查。本节给出完整的生产版 compose 示例、就绪检查清单,并讨论配置漂移与部署自动化。
我们得先泼一盆冷水:Compose 最初的设计目标是简化单机或小型集群的应用部署,它不是万能的编排系统。在开发环境里 docker compose up -d 一行命令拉起五个服务很痛快,但把同样的操作搬到生产,要面对的是完全不同的追问——挂了怎么办、流量涨了怎么办、发布怎么不中断。
Compose 的局限是明摆着的。它主要针对单机环境,虽然可以配合 Docker Swarm 做集群部署,但 Swarm 的功能相对简单,缺乏高级的调度与管理能力。它本身不具备自动伸缩功能,副本数要手动调整;滚动更新能力有限,docker compose up -d 虽然能更新服务,但过程相对粗糙,容易造成服务中断;资源管理能力也不足,无法对容器资源使用做细粒度控制。
那什么时候仍然值得用 Compose?我们给出三条判断标准。第一,小型应用:规模小、复杂度不高,Compose 能把部署和管理成本压到最低。第二,单节点部署:业务只需要跑在一台机器上,没有跨主机需求。第三,过渡阶段:把 Compose 当作从开发环境走向生产环境的桥梁,等规模上来再逐步迁移到更专业的编排工具。换句话说,Compose 生产化的前提是"单机能扛住",判断依据不是情怀,而是流量、可用性 SLA 和团队运维能力。
确定可以用 Compose 之后,就要把第三章的模板逐项升级。下面是我们认为生产环境必须补上的七件事,每件都对应一段配置。
固定镜像版本。 开发环境用 nginx:latest 无所谓,生产环境这是事故源。镜像仓库里的 latest 标签会漂移,今天拉的和上周拉的可能差好几个版本,线上行为无法复现。更稳妥的做法是固定到具体版本号,比如 nginx:1.25.3,甚至固定到镜像摘要 digest。
设置时区。 容器默认 UTC 时区,日志时间戳和业务时间会差八个小时,排查问题时先对不上表。通过环境变量 TZ: Asia/Shanghai 统一设置,日志、定时任务、数据库会话的时区就都一致了。
重启策略。 容器异常退出后要不要自动拉起,由 restart 决定。开发环境默认 no 就行,生产环境我们习惯用 unless-stopped——它会在容器退出后自动重启,但明确 stop 的容器不会被拉起来,比 always 更符合运维直觉。
资源限制。 不设限制的容器会无限占用 CPU 和内存,把同机的其他服务拖垮。详见 4.3,但配置基线上至少要有 CPU 与内存的 limits。
健康检查。 让 Docker 能感知容器是否真的活着,而不是只看进程在不在。定义 healthcheck 后,配合依赖关系可以实现"数据库就绪再启动应用"。
日志轮转。 默认 json-file 驱动会把日志无限写进宿主磁盘,必须配置 max-size 与 max-file,详见 4.4。
数据持久化与环境变量注入。 数据挂 named volume,敏感参数一律走环境变量或 secrets(详见 4.2),绝不硬编码进文件。
下面是一张生产配置基线的速查表:
| 配置项 | 推荐写法 | 防的是什么 |
|---|---|---|
| 镜像 tag | nginx:1.25.3 而非 latest | 镜像漂移、不可复现 |
| 时区 | environment 中设置 TZ | 日志与业务时间错位 |
| 重启策略 | restart: unless-stopped | 异常退出后服务不恢复 |
| CPU 限额 | deploy.resources.limits.cpus | 单容器占满整机 CPU |
| 内存限额 | deploy.resources.limits.memory | OOM 拖垮同机服务 |
| 健康检查 | healthcheck 定义存活探针 | 假活、依赖未就绪 |
| 日志轮转 | max-size 与 max-file | 日志撑爆磁盘 |
| 数据卷 | named volume 持久化 | 容器重建丢数据 |
把上面的基线合起来,就是一份可以直接抄的生产版 compose。示例是一个 Web 服务加一个 PostgreSQL 数据库,所有配置项都有实际含义:
version: "3.9" services: web: image: nginx:1.25.3 restart: unless-stopped environment: TZ: Asia/Shanghai deploy: resources: limits: cpus: "0.5" memory: 512M logging: driver: json-file options: max-size: "10m" max-file: "3" healthcheck: test: ["CMD", "curl", "-f", "http://localhost"] interval: 30s timeout: 10s retries: 3 networks: - my-network db: image: postgres:13.4 restart: unless-stopped environment: TZ: Asia/Shanghai POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} volumes: - db_data:/var/lib/postgresql/data networks: - my-network networks: my-network: driver: bridge volumes: db_data:
逐行解读关键配置。image: nginx:1.25.3 固定了镜像版本,拉取行为可复现;restart: unless-stopped 让崩溃的容器自动恢复;cpus: "0.5" 限制 Web 最多用半个 CPU 核,memory: 512M 封顶内存,防止它失控;logging 段把单个日志文件限制在 10MB 以内并只保留 3 个文件;healthcheck 每 30 秒用 curl 探一次本机,连续 3 次失败就判定不健康,配合 interval: 30s、timeout: 10s、retries: 3 三个参数,Docker 会在容器假死时触发重启逻辑。数据库密码通过 ${POSTGRES_PASSWORD} 从环境变量注入,实际值放在 .env 文件里,compose 文件本身不含任何敏感信息。db_data 是 named volume,容器重建后数据还在。
这里有个细节值得说:新版 Docker Compose 已经不强制要求 version 字段,但我们保留 version: "3.9" 是为了和 Swarm 的 stack 文件兼容——如果哪天要用 docker stack deploy,这个字段还在,转换成本为零。
生产环境最隐蔽的敌人是配置漂移:每台机器、每次发布之间的配置悄悄出现差异,最终在某个凌晨以诡异故障的形式爆发。漂移通常来自三个源头。
第一是镜像漂移,即上一节说的 latest 标签问题。第二是参数漂移:有人在服务器上手改了一个容器的环境变量,没有同步回配置文件,下次重建容器配置就"回到解放前"。第三是环境差异:开发、测试、生产各有一套 .env,字段对不上,行为自然对不上。
我们的对策有两条。第一条,把 compose 文件、.env 模板(注意是模板,真实密钥走 secrets)纳入版本控制,任何改动都要经过提交记录,杜绝"手改服务器";第二条,用覆盖文件机制管理环境差异——基础文件 docker-compose.yml 存放所有环境共通的定义,开发覆盖文件 docker-compose.override.yml 在本地自动生效,生产环境用显式组合 docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d 加载生产覆盖。这样"一份基础定义加按环境覆盖"的结构,本身就是对漂移的结构性防御,4.6 节会展开讲。
配置稳定之后,部署过程也要自动化。手动部署容易出错、效率低下,靠人肉 SSH 上去敲命令,早晚要出一次"少打了一个参数"的事故。自动化有两条线:持续集成与持续部署(CI/CD),用 Jenkins、GitLab CI、GitHub Actions 这类工具把构建、测试、部署串成流水线,代码一提交就自动走到生产;基础设施即代码(IaC),用 Terraform、Ansible 管理服务器与中间件,让"机器长什么样"也进入版本控制。两条线在本章 4.6 有详细方案,这里先记住结论:凡是重复两次以上的部署动作,都应该变成脚本或流水线。
高可用方面要诚实:Compose 本身不提供高可用。它没有故障转移、没有跨主机调度。单机场景下我们能做的是"单机内的高可用"——部署多个容器副本,用 Nginx 或 HAProxy 做负载均衡,把流量分发给多个副本,一个容器挂了其他副本继续服务;再配合健康检查,让 Docker 尽早发现故障容器。但这套方案的边界是宿主机本身,机器宕了就是宕了。跨节点的故障转移,属于 4.7 节编排选型的范畴。
生产环境的部署决策可以画成一张流程图,帮团队快速对齐方案:
把前五节的内容收拢成一份踩坑清单,这几条是我们上线时反复撞过的。
端口与资源冲突。 多个项目共用一个 Docker 主机时,忘了端口已被占用是常态。docker compose up 报 bind 失败时先 docker ps 看谁占了端口;更省心的做法是给每个项目划独立的端口段,写进项目文档,新服务上线前先查一遍分配表。
.env 缺失。 生产机上没有 .env 文件,compose 里的 ${POSTGRES_PASSWORD} 会被解析成空字符串,数据库用空密码启动。上线流程里必须包含"校验 .env 存在且字段齐全"这一步,部署脚本里加检查,空值直接 fail,别让服务带着空配置悄悄跑起来。
depends_on 不等健康检查。 depends_on 默认只等容器启动,不等服务就绪。数据库容器起来了但还在初始化,应用已经连上来,连接失败就退出。解法是给依赖服务配 healthcheck,并让应用容器带重启策略——先启动失败,等数据库就绪后自动拉起,这是最简单也最可靠的就绪编排。
时区不统一。 部分容器设了 TZ,部分没设,日志时间差八小时,排查跨容器问题时先对不上时间线。把"所有服务都带 TZ"写进模板检查项,比事后逐个补漏省事得多。
手改服务器。 上线后图省事直接在服务器上改配置、改文件,改完不回流版本库。两周后这台机器就成了"独一无二"的机器,重建等于重新配置。规则只有一条:服务器上的任何变更都要能通过重新部署复现,否则就写进版本库再执行。
⚠️ 不要在生产上用 latest 标签。我们见过不止一次:某天镜像仓库更新了 latest,线上容器重建后行为大变,回滚时却找不到"之前那个镜像"到底长什么样。固定版本号是最便宜的保险。
💡 健康检查的探针要探业务本身。curl 一下首页比检查进程存活靠谱得多,数据库没就绪时进程照样活着,但业务已经不可用了——healthcheck 就是为这种"假活"准备的。
下一节我们进入安全加固——基线只是"配置正确",密钥和权限才是生产环境真正的攻防战场。