教程·阅读约 5 分钟·
Docker Compose 生产环境部署完整指南:从开发到上线的每一步

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 的最佳实践,没有之一。它让你在构建阶段可以使用完整的编译工具链,但最终的运行镜像只包含必要的文件。

code
# ===== 构建阶段 =====
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"]

关键点解释:

  1. 两个 FROM 指令:第一阶段用完整镜像编译,第二阶段用精简镜像只复制产物。最终镜像大小可以缩小数倍。
  2. RUN npm ci --only=productionnpm cinpm install 更快、更确定(严格遵循 lock 文件)。--only=production 只装生产依赖。
  3. 非 root 用户USER appuser 确保容器以非特权用户运行,这是基本的安全加固。很多安全扫描工具会对此做检查。
  4. HEALTHCHECK:Docker 会定期执行这个检查。如果连续失败,Docker 会将容器标记为不健康,配合 restart 策略可以自动重启。

更安全的替代方案:distroless 镜像

如果对安全性要求更高,可以用 Google 的 distroless 镜像。它连 shell 都没有——攻击者即使进了容器也无事可做。

code
# 构建阶段同上
# ...
 
# 运行阶段使用 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 不能用 wgetcurl,需要用 CMD-SHELL 的方式调用内置的 node 脚本。或者更简单——不要用 distroless,用 alpine 镜像加非 root 用户就够了。

第二步:编写 docker-compose.yml

这是核心。以下是一个完整的生产级配置:

code
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.1127.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: 10mmax-file: 3 保证每个容器最多保留 30MB 日志。

appendonly 模式(Redis):启用 AOF 持久化,确保 Redis 重启后数据不丢失。

第三步:Nginx 反向代理配置

code
# nginx/nginx.conf
events {
    worker_connections 1024;
}
 
http {
    include /etc/nginx/conf.d/*.conf;
}
code
# 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 里。正确做法:

code
# .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

密码生成推荐:

code
# 生成 32 位随机密码
openssl rand -base64 32

第五步:部署到服务器

code
# 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

第六步:更新部署流程

code
# 应用更新
git pull
docker compose build app
docker compose up -d --no-deps app

--no-deps 参数很关键:它只重建 app 服务,不重启依赖的 postgres 和 redis。这样数据库不会因为应用更新而短暂不可用。

如果想进一步减少停机时间,可以用零停机部署模式:

code
# 使用 blue-green 方式
docker compose up -d --scale app=2 --no-recreate postgres redis nginx
# 逐个滚动重启
docker compose restart app

安全注意事项清单

  1. 不要以 root 运行容器——Dockerfile 里用 USER 指令
  2. 端口不要暴露到 0.0.0.0——绑定到 127.0.0.1 然后通过 Nginx 代理
  3. 不要暴露 Docker socket——/var/run/docker.sock 的挂载等于给了容器 root 权限
  4. 使用只读文件系统——read_only: true(如果应用支持)
  5. 敏感信息用环境变量——不要硬编码在代码或配置里
  6. 限制资源使用——给每个服务设置 CPU/Memory 限制
  7. 日志轮转——不设限制的话,日志可能撑爆磁盘
  8. 网络隔离——只把需要通信的服务放到同一个网络里

什么时候 Docker Compose 不够用?

Docker Compose 不是万能的。当出现以下情况时,说明你需要考虑 K8s 或 Nomad 了:

  • 有多台服务器需要管理
  • 需要自动扩缩容
  • 服务数量超过 15-20 个
  • 需要灰度发布、A/B 测试等高级部署策略
  • 团队规模大,需要多环境管理

但对于一台 VPS、三五个人、十来个服务——Docker Compose 完全够用,而且比 K8s 简单至少一个数量级。

参考

分享到
微博Twitter

© 2026 四月 · CC BY-NC-SA 4.0

原文链接:https://aprilzz.com/tutorials/docker-compose-production-guide