Docker Compose 生产环境配置完全指南
前言
Docker Compose 是定义和运行多容器 Docker 应用的重要工具。开发者通常在开发环境中使用 docker-compose.yml 快速启动服务,但将其直接迁移到生产环境会面临诸多挑战——安全、高可用、日志管理、备份恢复、性能调优等。
本文基于 Ubuntu 22.04 LTS + Docker Compose V2(docker compose 子命令,不再是独立二进制),从零搭建一个生产级的 Docker Compose 部署架构,涵盖安全加固、日志轮转、健康检查、备份策略、CI/CD 集成和故障排查。
一、生产环境与开发环境的差异
| 维度 | 开发环境 | 生产环境 |
| 重启策略 | 无或 always | unless-stopped + healthcheck |
| 日志 | 默认 json-file 无限制 | 日志轮转 + 集中式日志(Loki/ELK) |
| 网络 | 默认 bridge | 自定义 overlay/manual 网络 + 网络策略 |
| 持久化 | 匿名卷或 bind mount | 命名卷 + 定时备份 |
| 密码 | 硬编码在 yml | .env 文件或 Docker Secrets |
| 资源限制 | 无限制 | 明确 CPU/Memory reservations 和 limits |
| 监控 | 无 | 资源监控 + 容器探活告警 |
二、安装最新版 Docker 与 Compose V2
首先确保系统已安装 Docker Engine 24+(推荐 27.x)和 Compose V2:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| sudo apt remove docker docker-engine docker.io containerd runc
sudo apt update sudo apt install -y ca-certificates curl gnupg lsb-release
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
sudo usermod -aG docker $USER
docker --version docker compose version
|
注意:生产环境不建议使用 sudo 运行 Docker,务必通过 docker 组管理权限。同时需严格审查哪些用户属于 docker 组——docker 组的成员拥有相当于 root 的权限。
三、基础安全加固
3.1 Docker Daemon 配置
创建 /etc/docker/daemon.json:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| { "log-driver": "json-file", "log-opts": { "max-size": "10m", "max-file": "3" }, "live-restore": true, "userland-proxy": false, "iptables": true, "storage-driver": "overlay2", "exec-opts": ["native.cgroupdriver=systemd"], "default-ulimits": { "nofile": { "Name": "nofile", "Hard": 65535, "Soft": 65535 } } }
|
关键参数说明:
live-restore: true:Docker 守护进程重启时不停止运行中的容器。生产环境必开。
userland-proxy: false:禁用用户态代理,减少攻击面,配合 iptables: true 使用。
default-ulimits:为所有容器设置默认文件描述符限制。
重启 Docker 使配置生效:
1 2
| sudo systemctl daemon-reload sudo systemctl restart docker
|
3.2 限制 Docker Socket 访问
Docker Socket(/var/run/docker.sock)映射到容器内是极其危险的操作。如果确实需要(如 Traefik、Portainer),应严格限制:
- 使用只读模式挂载
docker.sock
- 在容器内以非 root 用户运行
- 使用 Docker Socket Proxy(如
tecnativa/docker-socket-proxy)提供安全访问层
1 2 3 4 5 6 7 8 9 10 11 12
| services: docker-proxy: image: tecnativa/docker-socket-proxy:latest volumes: - /var/run/docker.sock:/var/run/docker.sock:ro environment: - CONTAINERS=1 - NETWORKS=1 - SERVICES=1 - TASKS=1 restart: unless-stopped
|
四、生产级 compose.yml 模板
以下是一个包含 Nginx + PHP-FPM + MariaDB + Redis 的完整生产级 compose.yml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162
| version: "3.9"
services: nginx: image: nginx:1.27-alpine container_name: app-nginx restart: unless-stopped ports: - "80:80" - "443:443" volumes: - ./nginx/conf.d:/etc/nginx/conf.d:ro - ./app:/var/www/html:ro - ./nginx/ssl:/etc/nginx/ssl:ro - static-data:/var/www/static depends_on: - php networks: - frontend healthcheck: test: ["CMD", "wget", "-qO-", "http://localhost:80/health"] interval: 30s timeout: 10s retries: 3 start_period: 10s deploy: resources: limits: cpus: "0.5" memory: "256M" reservations: cpus: "0.25" memory: "128M" logging: driver: "json-file" options: max-size: "10m" max-file: "3"
php: image: php:8.3-fpm-alpine container_name: app-php restart: unless-stopped volumes: - ./app:/var/www/html - php-socket:/var/run/php environment: - APP_ENV=production - DB_HOST=mariadb - REDIS_HOST=redis env_file: - .env.production depends_on: mariadb: condition: service_healthy redis: condition: service_healthy networks: - frontend - backend healthcheck: test: ["CMD", "php-fpm", "-t"] interval: 30s timeout: 5s retries: 3 start_period: 15s deploy: resources: limits: cpus: "1.0" memory: "512M" reservations: cpus: "0.5" memory: "256M"
mariadb: image: mariadb:11.4 container_name: app-mariadb restart: unless-stopped volumes: - mariadb-data:/var/lib/mysql - ./db/init:/docker-entrypoint-initdb.d:ro environment: MARIADB_ROOT_PASSWORD_FILE: /run/secrets/db_root_password env_file: - .env.production networks: - backend healthcheck: test: ["CMD", "healthcheck.sh", "--su=mysql", "--connect", "--innodb_initialized"] interval: 15s timeout: 10s retries: 5 start_period: 60s deploy: resources: limits: cpus: "2.0" memory: "2G" reservations: cpus: "1.0" memory: "1G" secrets: - db_root_password - db_password
redis: image: redis:7.4-alpine container_name: app-redis restart: unless-stopped command: > redis-server --requirepass ${REDIS_PASSWORD} --appendonly yes --appendfsync everysec --save 900 1 --save 300 10 --save 60 10000 volumes: - redis-data:/data networks: - backend healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 15s timeout: 5s retries: 3 start_period: 10s deploy: resources: limits: cpus: "0.5" memory: "256M"
volumes: mariadb-data: driver: local redis-data: driver: local static-data: driver: local php-socket: driver: local
networks: frontend: driver: bridge ipam: config: - subnet: "172.20.0.0/24" backend: driver: bridge internal: true ipam: config: - subnet: "172.20.1.0/24"
secrets: db_root_password: file: ./secrets/db_root_password.txt db_password: file: ./secrets/db_password.txt
|
五、关键配置详解
5.1 健康检查(Healthcheck)
每个服务都应配置 healthcheck,让 Docker 能自动检测服务状态,配合 depends_on.condition: service_healthy 确保依赖服务启动后再启动当前服务。
健康检查参数速查:
| 参数 | 说明 | 推荐值 |
| test | 健康检查命令(需返回 0 表示健康) | 根据服务类型定制 |
| interval | 检查间隔 | 15-30s |
| timeout | 单次检查超时 | 5-10s |
| retries | 连续失败次数 | 3-5 |
| start_period | 启动后的宽限期 | 根据服务启动时间而定 |
5.2 资源限制
deploy.resources 设置容器的 CPU 和内存上限,防止某个容器耗尽主机资源导致雪崩。
limits:硬上限,超过则 OOM kill
reservations:软预留,调度时保证至少分配这些资源
5.3 网络隔离
使用 两个网络 实现分层隔离:
frontend:Nginx 等对外暴露的服务在此网络
backend:内部服务(DB、Redis)使用 internal: true,完全隔离外部访问
只有 php 服务同时加入 frontend 和 backend 网络,作为应用层的数据通道。
5.4 Docker Secrets
敏感信息(数据库密码、API 密钥等)不应写死在 compose.yml 或环境变量中。使用 Docker Secrets:
- 创建
./secrets/db_password.txt 文件(一行明文)
- 在 compose 中通过
secrets: 声明
- 在 service 中通过
secrets: 引用
- 密码读取方式:
MARIADB_ROOT_PASSWORD_FILE: /run/secrets/db_root_password
六、日志管理
6.1 容器日志轮转
必须在 Docker Daemon 或每个服务级别设置日志轮转,防止日志撑爆磁盘:
1 2 3 4 5
| logging: driver: "json-file" options: max-size: "10m" max-file: "3"
|
6.2 集中式日志方案
推荐使用 Loki + Promtail(轻量)或 ELK(功能丰富):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| services: loki: image: grafana/loki:3.0 volumes: - ./loki-config.yml:/etc/loki/local-config.yml:ro - loki-data:/loki ports: - "3100:3100" command: -config.file=/etc/loki/local-config.yml restart: unless-stopped
promtail: image: grafana/promtail:3.0 volumes: - /var/lib/docker/containers:/var/lib/docker/containers:ro - ./promtail-config.yml:/etc/promtail/config.yml:ro command: -config.file=/etc/promtail/config.yml restart: unless-stopped
|
6.3 日志清理脚本
即便配置了轮转,长期运行的服务器仍可能出现日志残留。建议配合定时任务清理:
1 2 3 4 5
| #!/bin/bash
find /var/lib/docker/containers -name "*-json.log" -mtime +3 -delete journalctl --vacuum-time=7d
|
添加到 crontab:
1
| 0 3 * * * /usr/local/bin/clean-docker-logs.sh
|
七、数据备份与恢复
7.1 数据库自动备份
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| #!/bin/bash
BACKUP_DIR=/data/backups/mariadb TIMESTAMP=$(date +%Y%m%d_%H%M%S) RETENTION_DAYS=30 DB_PASSWORD=$(cat /opt/docker-app/secrets/db_root_password.txt)
mkdir -p $BACKUP_DIR
docker exec app-mariadb mysqldump \ --all-databases \ --single-transaction \ --routines \ --triggers \ --events \ -u root -p"$DB_PASSWORD" \ | gzip > $BACKUP_DIR/full_backup_$TIMESTAMP.sql.gz
find $BACKUP_DIR -name "*.sql.gz" -mtime +$RETENTION_DAYS -delete
rsync -avz --delete $BACKUP_DIR/ backup@remote-server:/data/backups/mariadb/
|
添加到 crontab:
1
| 0 2 * * * /usr/local/bin/backup-mariadb.sh
|
7.2 容器卷备份
1 2 3 4 5 6
| #!/bin/bash
docker run --rm \ -v mariadb-data:/data \ -v /data/backups/volumes:/backup \ alpine tar czf /backup/mariadb-data_$(date +%Y%m%d).tar.gz -C /data .
|
7.3 恢复流程
1 2 3 4 5 6 7 8
| gunzip < full_backup_20260612_020000.sql.gz | docker exec -i app-mariadb mysql -u root -p"$PASSWORD"
docker run --rm \ -v mariadb-data:/data \ -v /data/backups/volumes:/backup \ alpine tar xzf /backup/mariadb-data_20260612.tar.gz -C /data
|
八、部署与 CI/CD 集成
8.1 项目目录结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| /opt/docker-app/ ├── compose.yml ├── .env.production # 环境变量(不进版本库) ├── secrets/ # Docker Secrets(不进版本库) │ ├── db_root_password.txt │ └── db_password.txt ├── nginx/ │ ├── conf.d/ │ └── ssl/ ├── app/ # 应用代码 ├── db/ │ └── init/ # 初始化 SQL ├── scripts/ │ ├── backup.sh │ └── deploy.sh └── monitoring/ ├── loki-config.yml └── promtail-config.yml
|
8.2 零停机部署脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| #!/bin/bash
set -e
cd /opt/docker-app
echo "[1/4] 拉取最新代码..." git pull origin main
echo "[2/4] 构建新镜像..." docker compose build --pull
echo "[3/4] 滚动更新服务..."
docker compose up -d --no-deps --scale nginx=2 nginx docker compose up -d --no-deps php
echo "[4/4] 等待健康检查..." sleep 15
docker image prune -f
echo "部署完成!"
|
8.3 GitHub Actions 自动部署
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| name: Deploy to Production
on: push: branches: [main]
jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4
- name: Copy files via SSH uses: appleboy/scp-action@v0.1.7 with: host: ${{ secrets.DEPLOY_HOST }} username: ${{ secrets.DEPLOY_USER }} key: ${{ secrets.DEPLOY_KEY }} source: "." target: "/opt/docker-app"
- name: Execute deploy script uses: appleboy/ssh-action@v1.0.3 with: host: ${{ secrets.DEPLOY_HOST }} username: ${{ secrets.DEPLOY_USER }} key: ${{ secrets.DEPLOY_KEY }} script: cd /opt/docker-app && bash scripts/deploy.sh
|
九、监控与告警
9.1 本地资源监控
1 2 3 4 5 6 7 8
| docker stats --no-stream
docker compose logs --tail=100 -f nginx
df -h | grep -E "overlay|/dev/sda"
|
9.2 cAdvisor + Prometheus 集成
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| services: cadvisor: image: gcr.io/cadvisor/cadvisor:latest container_name: cadvisor restart: unless-stopped volumes: - /:/rootfs:ro - /var/run:/var/run:ro - /sys:/sys:ro - /var/lib/docker:/var/lib/docker:ro - /dev/disk/:/dev/disk:ro ports: - "8080:8080" privileged: true networks: - monitoring
node_exporter: image: prom/node-exporter:latest container_name: node-exporter restart: unless-stopped volumes: - /proc:/host/proc:ro - /sys:/host/sys:ro - /:/rootfs:ro command: - '--path.procfs=/host/proc' - '--path.sysfs=/host/sys' - '--path.rootfs=/rootfs' ports: - "9100:9100" networks: - monitoring
networks: monitoring: driver: bridge
|
十、常见问题排查
| 问题 | 原因 | 解决方案 |
| 容器不断重启 | 健康检查失败或配置错误 | docker compose logs [service] 查看具体错误 |
| 端口被占用 | 其他服务占用了映射端口 | ss -tlnp | grep :80 查看占用进程 |
| 磁盘空间满 | 日志或镜像占用 | docker system prune -af 清理;docker compose logs --tail=0 清空日志 |
| 数据库连不上 | 网络配置或认证问题 | 确认网络是否正确;检查 secrets 文件是否一致 |
| 容器内时区不对 | 未设置 TZ 环境变量 | 添加 environment: TZ: Asia/Shanghai |
| PHP 写文件报错 | 权限不一致 | 确认 UID/GID 映射;使用 user: "1000:1000" |
| Docker 守护进程无法启动 | daemon.json 语法错误 | dockerd --validate 检查配置 |
| swap 导致 OOM | 内存限制 + swap 交互 | 在 daemon.json 中禁用 swap:设置 "swappiness": 0 |
十一、安全最佳实践清单
- 最小权限原则:容器内使用非 root 用户运行进程(
RUN adduser -D appuser && USER appuser)
- 只读文件系统:不需要写权限的目录使用
:ro 挂载
- 镜像签名验证:使用 Docker Content Trust(
export DOCKER_CONTENT_TRUST=1)
- 定期更新基础镜像:
docker compose build --pull && docker compose up -d
- 安全扫描:集成 Trivy 或 Docker Scout 扫描镜像漏洞
- 网络策略:后端服务使用
internal: true 网络
- 密钥管理:Docker Secrets 替代明文环境变量
- 审计日志:启用 Docker Daemon 的 auditd 规则
- 资源配额:所有容器设置 CPU/Memory limits
- 备份恢复演练:定期测试备份数据能否正常恢复
总结
Docker Compose 并非仅适用于开发环境,通过合理的配置和加固,完全可以在生产环境中安全、稳定地运行。本文从安装、安全加固、生产级配置模板、日志管理、数据备份、CI/CD 集成到监控告警,覆盖了完整的生产化流程。
关键在于记住几个核心原则:安全隔离、资源限制、日志管理、健康检查、定期备份。遵循这些原则,Docker Compose 可以成为中小规模生产环境的高效部署方案。
对于更大规模的集群场景,可考虑升级到 Docker Swarm 或 Kubernetes,但 Docker Compose 的配置思维和最佳实践在这些平台同样适用。
本文由AI辅助生成,内容仅供参考