Docker Compose 很适合单机部署:应用、数据库、缓存和反向代理可以用一组配置描述清楚。但开发环境里的 compose.yaml 往往不能直接拿去生产。生产环境最重要的不是“能不能 up -d”,而是升级、日志、数据和恢复路径是否清楚。
本文用两个 Compose 文件演示一种简单的分层方法:基础文件描述服务本身,生产文件只覆盖上线所需的差异。
基础文件和生产覆盖文件分开
基础 compose.yaml 可以保持适合本地开发:
services:
app:
build: .
expose:
- "8000"
environment:
APP_ENV: development
volumes:
- .:/app
db:
image: postgres:16
environment:
POSTGRES_DB: app
POSTGRES_USER: app
POSTGRES_PASSWORD: change-me-in-development
volumes:
- db-data:/var/lib/postgresql/data
volumes:
db-data:
生产文件 compose.production.yaml 只写生产差异:
services:
app:
build: .
env_file:
- .env.production
environment:
APP_ENV: production
volumes: []
restart: unless-stopped
read_only: true
tmpfs:
- /tmp
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:8000/health"]
interval: 30s
timeout: 5s
retries: 3
db:
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d app"]
interval: 10s
timeout: 5s
retries: 5
read_only: true 不是所有应用都能直接使用。如果程序需要写缓存、上传文件或临时文件,就把这些位置单独挂载卷,不要为了省事把整个容器文件系统重新开放写入。
合并后的配置一定要先检查:
docker compose -f compose.yaml -f compose.production.yaml config
这个步骤可以提前发现缩进、变量和合并结果的问题,也能确认生产环境到底使用了哪些端口、卷和环境变量。
只把真正需要的端口暴露给宿主机
应用和数据库通常不应该直接暴露到公网。若只有 Nginx 或 Caddy 需要接受公网流量,应用可以只使用 Compose 网络:
services:
app:
expose:
- "8000"
proxy:
image: nginx:stable
ports:
- "80:80"
- "443:443"
depends_on:
app:
condition: service_healthy
expose 只提供给同一 Compose 网络中的服务,ports 才会映射到宿主机。数据库如果没有远程管理需求,甚至不需要 ports。
如果应用确实需要被宿主机上的反向代理访问,可以只绑定本机:
ports:
- "127.0.0.1:8000:8000"
这样公网仍然只能通过反向代理访问。
重启策略不要和另一个进程管理器打架
对普通 Compose 服务,restart: unless-stopped 通常比手动写一个 systemd 无限重启脚本更直接。Docker 官方也提醒,不要同时把 Docker 重启策略和宿主机进程管理器叠加使用,否则两套机制可能互相拉起、互相停止。
重启策略解决的是“容器退出或 Docker 重启后怎么办”,不是应用健康本身。健康检查解决的是“容器还活着,但服务已经不能正常响应”的可观察性问题。两者用途不同,不要把一个当成另一个。
日志要有上限
开发环境里看 docker compose logs -f 很方便,生产环境如果不限制日志,磁盘迟早会被写满。可以给服务配置日志上限:
services:
app:
logging:
driver: local
options:
max-size: "10m"
max-file: "5"
这不是完整的日志归档方案,但至少能避免单个容器无限吃掉磁盘。应用本身也应该降低无意义的 debug 日志,并为错误记录请求 ID、时间和服务名,方便把一次请求串起来。
一套相对稳妥的发布流程
# 1. 检查合并后的配置
docker compose -f compose.yaml -f compose.production.yaml config
# 2. 构建新镜像
docker compose -f compose.yaml -f compose.production.yaml build app
# 3. 只重建应用,不主动动数据库
docker compose -f compose.yaml -f compose.production.yaml up -d --no-deps app
# 4. 查看状态和最近日志
docker compose -f compose.yaml -f compose.production.yaml ps
docker compose -f compose.yaml -f compose.production.yaml logs --tail=100 app
如果应用有数据库迁移,先明确迁移是否向后兼容,再执行迁移。不要把“启动容器”和“删除数据卷”写进同一条随手复制的命令里。docker compose down 默认不会删除卷,但带上 -v 就会删除项目卷,这个选项必须单独确认。
备份不是把 Compose 文件复制一份
Compose 文件只描述如何启动服务,不能替代数据备份。至少要分别考虑:
- 数据库的逻辑备份,例如 PostgreSQL 的
pg_dump。 - 上传文件或媒体目录。
.env.production、反向代理配置和 TLS 证书的安全备份。- 镜像版本、Compose 文件和迁移脚本。
数据库备份完成后,最好在临时目录或另一台机器上做一次恢复演练。一个“备份命令成功”的文件,不等于已经证明可以恢复业务。
小服务器的最后检查
[ ] 生产环境没有把源码目录绑定进容器
[ ] 数据库没有无必要的公网端口
[ ] 环境变量没有提交到 Git
[ ] 容器日志有大小上限
[ ] 应用有健康检查
[ ] 镜像版本不是永远漂移的 latest
[ ] 只更新需要更新的服务
[ ] 数据库和上传文件有独立备份
[ ] 至少做过一次恢复测试
Compose 的价值不是让命令变短,而是让部署状态更容易被别人读懂、更容易在另一台机器上复现。把配置、数据、日志和发布动作分开,单机部署也可以很有秩序。