
Docker Compose 生产环境部署完整指南:从开发到上线的每一步
从 Dockerfile 编写到 Compose 编排,从多阶段构建到健康检查,从日志管理到安全加固——一份面向开发者的 Docker Compose 生产部署实战教程
原创。一份完整的使用 Docker Compose 将应用部署到生产环境的教程:从 Dockerfile 编写到 Compose 编排,从多阶段构建到健康检查,从日志管理到安全配置。读完这篇文章,你就能把自己的应用用 Docker Compose 安全地部署到生产服务器上。
为什么选择 Docker Compose?
如果你在运维一台 VPS,服务数量不多(3-10 个),不想上 Kubernetes 那一套复杂的东西,Docker Compose 就是最合适的选择。
很多人觉得 Docker Compose 只是"开发环境用的",生产环境得用 K8s。这个看法正在过时。2026 年的 Docker Compose 已经有相当完善的生产级能力:
- 重启策略:
restart: always让容器在崩溃后自动恢复 - 健康检查:内置的健康检测机制
- 资源限制:CPU、内存的硬限制和软限制
- 卷管理:持久化数据的安全处理
- 网络隔离:自定义网络,服务间通信可控
- 日志轮转:防止日志撑爆磁盘
对于大多数中小型项目,Docker Compose 的运维复杂度远低于 K8s,足够了。
第一步:编写生产级 Dockerfile
很多人写 Dockerfile 就是简单从基础镜像拉个环境、复制代码、跑起来。生产环境不能这样。
多阶段构建
多阶段构建是 Dockerfile 的最佳实践,没有之一。它让你在构建阶段可以使用完整的编译工具链,但最终的运行镜像只包含必要的文件。
# ===== 构建阶段 =====
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
# ===== 运行阶段 =====
FROM node:20-alpine AS runner
# 安全:使用非 root 用户
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 appuser
WORKDIR /app
# 只复制构建产物和运行时依赖
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
USER appuser
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
CMD ["node", "dist/server.js"]关键点解释:
- 两个 FROM 指令:第一阶段用完整镜像编译,第二阶段用精简镜像只复制产物。最终镜像大小可以缩小数倍。
- RUN npm ci --only=production:
npm ci比npm install更快、更确定(严格遵循 lock 文件)。--only=production只装生产依赖。 - 非 root 用户:
USER appuser确保容器以非特权用户运行,这是基本的安全加固。很多安全扫描工具会对此做检查。 - HEALTHCHECK:Docker 会定期执行这个检查。如果连续失败,Docker 会将容器标记为不健康,配合
restart策略可以自动重启。
更安全的替代方案:distroless 镜像
如果对安全性要求更高,可以用 Google 的 distroless 镜像。它连 shell 都没有——攻击者即使进了容器也无事可做。
# 构建阶段同上
# ...
# 运行阶段使用 distroless
FROM gcr.io/distroless/nodejs20-debian12 AS runner
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
EXPOSE 3000
CMD ["dist/server.js"]注意 distroless 镜像没有 shell,所以 HEALTHCHECK 不能用 wget 或 curl,需要用 CMD-SHELL 的方式调用内置的 node 脚本。或者更简单——不要用 distroless,用 alpine 镜像加非 root 用户就够了。
第二步:编写 docker-compose.yml
这是核心。以下是一个完整的生产级配置:
version: "3.8"
services:
app:
build:
context: .
dockerfile: Dockerfile
image: myapp:latest
restart: unless-stopped
ports:
- "127.0.0.1:3000:3000"
env_file:
- .env.production
environment:
- NODE_ENV=production
volumes:
- app_data:/app/data
- /etc/localtime:/etc/localtime:ro
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
deploy:
resources:
limits:
cpus: "1.0"
memory: 512M
reservations:
cpus: "0.5"
memory: 256M
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
networks:
- app_network
postgres:
image: postgres:16-alpine
restart: unless-stopped
environment:
- POSTGRES_DB=myapp
- POSTGRES_USER=myapp
- POSTGRES_PASSWORD=${DB_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U myapp -d myapp"]
interval: 10s
timeout: 5s
retries: 5
deploy:
resources:
limits:
memory: 1G
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
networks:
- app_network
redis:
image: redis:7-alpine
restart: unless-stopped
command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "--raw", "incr", "ping"]
interval: 10s
timeout: 5s
retries: 5
deploy:
resources:
limits:
memory: 256M
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
networks:
- app_network
nginx:
image: nginx:alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./nginx/conf.d:/etc/nginx/conf.d:ro
- ./certbot/www:/var/www/certbot:ro
- ./certbot/conf:/etc/letsencrypt:ro
depends_on:
- app
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
networks:
- app_network
volumes:
postgres_data:
redis_data:
app_data:
networks:
app_network:
driver: bridge配置要点解释
端口绑定到 127.0.0.1:127.0.0.1:3000:3000 确保应用只在本机可访问。对外暴露通过 nginx 处理。这样做了一层额外的安全隔离。
depends_on 的 condition 模式:condition: service_healthy 确保 Postgres 和 Redis 完全就绪后应用才启动。默认的 depends_on 只保证容器启动,不保证服务可用。
资源限制:deploy.resources.limits 是做资源隔离的关键。如果某个应用因为 bug 导致内存泄漏,Limit 可以防止它拖垮整台机器。在生产环境里,不做资源限制的 Docker Compose 是把自己往火坑里推。
日志管理:logging 配置限制每个容器的日志文件大小。默认情况下 Docker 不做日志轮转,一个满负载的应用一天就能产生几十 GB 的日志。max-size: 10m 和 max-file: 3 保证每个容器最多保留 30MB 日志。
appendonly 模式(Redis):启用 AOF 持久化,确保 Redis 重启后数据不丢失。
第三步:Nginx 反向代理配置
# nginx/nginx.conf
events {
worker_connections 1024;
}
http {
include /etc/nginx/conf.d/*.conf;
}# nginx/conf.d/default.conf
upstream app {
server app:3000;
}
server {
listen 80;
server_name yourdomain.com;
# Let's Encrypt 验证
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl;
server_name yourdomain.com;
ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
# 现代 SSL 配置
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
# 安全头部
add_header Strict-Transport-Security "max-age=63072000" always;
add_header X-Content-Type-Options nosniff;
add_header X-Frame-Options DENY;
add_header X-XSS-Protection "1; mode=block";
location / {
proxy_pass http://app;
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;
# WebSocket 支持
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}关键点:Nginx 作为反向代理,应用服务监听 app:3000(通过 Docker Compose 网络解析到实际容器)。所有外部请求先经过 Nginx,再转发到 APP。这意味着你的应用不需要处理 SSL 证书——全交给 Nginx。
第四步:环境变量管理
不要把数据库密码、API Key、JWT Secret 写在 docker-compose.yml 里。正确做法:
# .env.production(不要提交到 Git)
DB_PASSWORD=your_secure_password_here
REDIS_PASSWORD=another_secure_password_here
JWT_SECRET=generate-a-random-64-char-string
API_KEY=your_api_key.env.production 文件通过 env_file 引用,不会出现在版本控制中。记得加到 .gitignore。
密码生成推荐:
# 生成 32 位随机密码
openssl rand -base64 32第五步:部署到服务器
# 1. 登录服务器
ssh user@your-server
# 2. 安装 Docker 和 Docker Compose
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER
# 重新登录使组生效
# 3. 创建项目目录
mkdir -p /opt/myapp
cd /opt/myapp
# 4. 上传文件(本机执行)
scp docker-compose.yml .env.production user@your-server:/opt/myapp/
scp -r nginx/ user@your-server:/opt/myapp/
# 5. 启动
docker compose pull
docker compose up -d
# 6. 查看状态
docker compose ps
docker compose logs -f
# 7. 配置 SSL(使用 certbot)
docker compose exec nginx certbot --nginx -d yourdomain.com第六步:更新部署流程
# 应用更新
git pull
docker compose build app
docker compose up -d --no-deps app--no-deps 参数很关键:它只重建 app 服务,不重启依赖的 postgres 和 redis。这样数据库不会因为应用更新而短暂不可用。
如果想进一步减少停机时间,可以用零停机部署模式:
# 使用 blue-green 方式
docker compose up -d --scale app=2 --no-recreate postgres redis nginx
# 逐个滚动重启
docker compose restart app安全注意事项清单
- 不要以 root 运行容器——Dockerfile 里用
USER指令 - 端口不要暴露到 0.0.0.0——绑定到
127.0.0.1然后通过 Nginx 代理 - 不要暴露 Docker socket——
/var/run/docker.sock的挂载等于给了容器 root 权限 - 使用只读文件系统——
read_only: true(如果应用支持) - 敏感信息用环境变量——不要硬编码在代码或配置里
- 限制资源使用——给每个服务设置 CPU/Memory 限制
- 日志轮转——不设限制的话,日志可能撑爆磁盘
- 网络隔离——只把需要通信的服务放到同一个网络里
什么时候 Docker Compose 不够用?
Docker Compose 不是万能的。当出现以下情况时,说明你需要考虑 K8s 或 Nomad 了:
- 有多台服务器需要管理
- 需要自动扩缩容
- 服务数量超过 15-20 个
- 需要灰度发布、A/B 测试等高级部署策略
- 团队规模大,需要多环境管理
但对于一台 VPS、三五个人、十来个服务——Docker Compose 完全够用,而且比 K8s 简单至少一个数量级。
参考
- Docker 官方文档:Compose file reference
- Docker 官方文档:Best practices for writing Dockerfiles
- Nginx 官方文档:ngx_http_proxy_module
© 2026 四月 · CC BY-NC-SA 4.0
原文链接:https://aprilzz.com/tutorials/docker-compose-production-guide