运维知识
悠悠
2026年6月26日

Docker Compose 用了这么久,这些骚操作你可能一个都没用过

Compose 都进化到 V2 了,很多人还在用最基础的功能,那些真正能提效的特性根本没碰过。今天就把我在生产环境里实际用到的一些 Compose 进阶配置掏出来,都是踩过坑之后总结的。

启动顺序这个坑,你一定踩过

最经典的场景:Web 应用依赖 MySQL,Compose 文件里写了 depends_on: db,以为万事大吉了。结果一跑 docker compose up,应用容器启动了,数据库容器也启动了——但 MySQL 还在初始化,应用连不上数据库直接报错退出。

很多人的解决方案是在应用启动脚本里加 sleep 15 或者写个 wait-for-it.sh 脚本去循环检测端口。能用,但丑。

Compose 其实早就给了优雅解:healthcheck + depends_on 条件等待。

services:
  db:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
      MYSQL_DATABASE: myapp
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 5s
      timeout: 3s
      retries: 10
      start_period: 30s
    volumes:
      - db_data:/var/lib/mysql

  web:
    build: .
    ports:
      - "8080:8080"
    depends_on:
      db:
        condition: service_healthy
    environment:
      DB_HOST: db
      DB_PORT: 3306

关键就在 depends_on 里面的 condition: service_healthy。有了这个,Compose 会一直等到 db 的 healthcheck 通过了才去启动 web 容器。不用 sleep,不用额外脚本,干干净净。

start_period: 30s 这个参数容易被忽略——它的意思是容器启动后的前30秒内,healthcheck 失败不算数,不会把容器标记为 unhealthy。MySQL 这种启动慢的服务,这个参数一定要给够,不然容器还在初始化就被判定不健康了。

除了 MySQL,Redis 的 healthcheck 可以这么写:

  redis:
    image: redis:7-alpine
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 3s
      timeout: 2s
      retries: 5

PostgreSQL 的:

  postgres:
    image: postgres:16
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 3s
      retries: 10
      start_period: 20s

养成习惯,每个基础设施服务都配上 healthcheck,后面不管是本地开发还是 CI/CD 流水线里跑集成测试,都能省不少事。

Profiles:一份文件管多套环境

以前我管一个项目,本地开发要跑 app + db + redis,测试环境还要多一个 Selenium 做 E2E 测试,生产环境又要挂上监控的 Prometheus + Grafana。三套环境三份 compose 文件,改一个配置要同步好几个地方,烦得要死。

后来发现 profiles 这个功能,一份文件全搞定:

services:
  app:
    build: .
    ports:
      - "8080:8080"
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 3s
      retries: 10

  redis:
    image: redis:7-alpine
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 3s
      retries: 5

  # 只在测试时启动
  selenium:
    image: selenium/standalone-chrome:latest
    profiles:
      - testing
    ports:
      - "4444:4444"

  # 只在需要监控时启动
  prometheus:
    image: prom/prometheus:latest
    profiles:
      - monitoring
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
    ports:
      - "9090:9090"

  grafana:
    image: grafana/grafana:latest
    profiles:
      - monitoring
    ports:
      - "3000:3000"
    depends_on:
      - prometheus

用的时候:

# 本地开发,只启动核心服务
docker compose up

# 跑集成测试,带上 selenium
docker compose --profile testing up

# 需要看监控面板
docker compose --profile monitoring up

# 全都要
docker compose --profile testing --profile monitoring up

没有指定 profile 的服务默认启动,标记了 profile 的服务只有在显式激活时才会起来。这样一份文件就覆盖了所有场景,维护成本直接砍掉三分之二。

Watch 模式:告别无尽的 rebuild

做本地开发最痛苦的是什么?改了一行代码,要重新 docker compose build,等镜像构建完再 up,一来一回半分钟就没了。如果你的 Dockerfile 写得不好没有利用缓存,那更是每次都要重新安装依赖,一次 build 两三分钟。

Compose V2 的 watch 功能直接终结了这个痛苦。它能监控你本地文件的变化,自动同步到容器里或者触发重建:

services:
  web:
    build: .
    ports:
      - "3000:3000"
    develop:
      watch:
        # 源代码变了,直接同步进容器(热加载)
        - action: sync
          path: ./src
          target: /app/src

        # package.json 变了,重新构建镜像
        - action: rebuild
          path: ./package.json

        # 配置文件变了,重启容器就行
        - action: sync+restart
          path: ./config
          target: /app/config

然后跑:

docker compose watch

这样一来,你改 src/ 下面的代码,文件直接同步进容器,配合应用自己的热加载(比如 nodemon、Spring DevTools)立刻生效;改了 package.json 新增依赖,Compose 自动触发 rebuild;改了配置文件,容器自动重启加载新配置。

三种 action 的区别搞清楚就行:

  • sync:只同步文件,不重启容器。适合有热加载能力的应用。
  • sync+restart:同步文件后重启容器。适合需要重启才能加载配置的场景。
  • rebuild:重新构建镜像并替换容器。适合依赖变更、Dockerfile 本身变了的情况。

我现在本地开发项目全用这个,基本告别了手动 build 的操作。

Secrets:别再把密码明文写在 yml 里了

看过太多项目把数据库密码、API Key 直接写在 docker-compose.yml 或者 .env 文件里,然后整个文件提交到 Git 仓库。虽然 .env 一般会 gitignore,但 compose 文件本身经常有人直接把值硬编码进去。

Compose 支持 secrets 管理,虽然不像 Kubernetes Secrets 或者 HashiCorp Vault 那么重量级,但对于小团队和开发环境来说足够了:

services:
  db:
    image: postgres:16
    secrets:
      - db_password
    environment:
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password

  app:
    build: .
    secrets:
      - db_password
      - api_key
    environment:
      DB_PASSWORD_FILE: /run/secrets/db_password
      API_KEY_FILE: /run/secrets/api_key

secrets:
  db_password:
    file: ./secrets/db_password.txt
  api_key:
    environment: "MY_API_KEY"

secrets 会以文件的形式挂载到容器的 /run/secrets/ 目录下,而不是通过环境变量暴露。很多官方镜像(PostgreSQL、MySQL、MariaDB)都支持 *_FILE 后缀的环境变量,会自动从文件读取值。

当然你的应用代码也需要支持从文件读取密钥,这个改动其实很小,伪代码大概就是:

import os

def get_secret(name):
    # 优先从 secrets 文件读
    secret_file = f"/run/secrets/{name}"
    if os.path.exists(secret_file):
        with open(secret_file) as f:
            return f.read().strip()
    # 降级到环境变量
    return os.environ.get(name.upper())

这样密码文件不进 Git,compose 文件里没有敏感信息,干净多了。

网络隔离:别让所有容器都能互相访问

默认情况下 Compose 会创建一个网络,把所有服务都扔进去,每个服务都能访问其他所有服务。对于简单项目无所谓,但如果你的项目稍微复杂一点,比如有前端、后端、数据库三层,让前端容器能直接连数据库这件事本身就不合理。

手动划分网络其实很简单:

services:
  frontend:
    build: ./frontend
    ports:
      - "80:80"
    networks:
      - frontend_net

  backend:
    build: ./backend
    ports:
      - "8080:8080"
    networks:
      - frontend_net
      - backend_net

  db:
    image: postgres:16
    networks:
      - backend_net
    volumes:
      - db_data:/var/lib/postgresql/data

networks:
  frontend_net:
  backend_net:

volumes:
  db_data:

这样 frontend 只能访问 backend,访问不了 db;backend 能同时访问 frontend 网络和 db 所在的 backend 网络。最小权限原则,减少攻击面。

多文件组合和 override

还有一个很实用的特性:Compose 支持多文件叠加。你可以有一个基础的 docker-compose.yml,然后针对不同环境写 override 文件:

# 文件结构
docker-compose.yml           # 基础配置
docker-compose.override.yml  # 本地开发覆盖(自动加载)
docker-compose.prod.yml      # 生产环境覆盖

docker-compose.override.yml 这个文件名是约定俗成的,Compose 会自动加载它来覆盖主文件的配置,不用你手动指定。

# docker-compose.yml(基础)
services:
  app:
    image: myapp:latest
    environment:
      LOG_LEVEL: info

# docker-compose.override.yml(开发环境自动覆盖)
services:
  app:
    build: .
    volumes:
      - ./src:/app/src
    environment:
      LOG_LEVEL: debug
      DEBUG: "true"
    ports:
      - "8080:8080"
      - "5005:5005"  # debug 端口

本地开发直接 docker compose up 就会合并两个文件;部署生产时用:

docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

这个模式的好处是基础配置只写一份,各环境的差异通过 override 文件表达,不用维护多份完整的 compose 文件。

资源限制:别让一个容器把服务器吃死

线上跑 Compose 的话,一定要给容器加资源限制。不加限制的后果我见过——一个 Java 应用内存泄漏,把整台机器 32G 内存吃光,连 SSH 都登不上去,最后只能强制重启。

services:
  app:
    image: myapp:latest
    deploy:
      resources:
        limits:
          cpus: '2.0'
          memory: 2G
        reservations:
          cpus: '0.5'
          memory: 512M
    restart: unless-stopped

limits 是硬限制,超了就 OOM Kill;reservations 是预留资源,Docker 调度时会保证这些资源可用。

另外 restart: unless-stopped 也别忘了加,容器崩了能自动拉起来。和 restart: always 的区别是,如果你手动 docker compose stop 停掉了容器,unless-stopped 不会在 Docker 重启后自动拉起来,而 always 会。

一个实际项目的完整示例

最后给一个我自己在跑的项目配置,算是把前面讲的东西串起来。一个典型的 Web 应用 + API + PostgreSQL + Redis + Nginx 的架构:

services:
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
      - ./nginx/ssl:/etc/nginx/ssl:ro
    networks:
      - frontend
    depends_on:
      api:
        condition: service_healthy
    restart: unless-stopped
    deploy:
      resources:
        limits:
          memory: 256M

  api:
    build:
      context: .
      dockerfile: Dockerfile
    secrets:
      - db_password
      - redis_password
    environment:
      DB_HOST: postgres
      DB_PORT: 5432
      DB_NAME: myapp
      DB_USER: appuser
      DB_PASSWORD_FILE: /run/secrets/db_password
      REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379/0
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 10s
      timeout: 5s
      retries: 3
      start_period: 40s
    networks:
      - frontend
      - backend
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    restart: unless-stopped
    deploy:
      resources:
        limits:
          cpus: '2.0'
          memory: 2G
        reservations:
          memory: 512M
    develop:
      watch:
        - action: sync
          path: ./src
          target: /app/src
        - action: rebuild
          path: ./requirements.txt

  postgres:
    image: postgres:16-alpine
    secrets:
      - db_password
    environment:
      POSTGRES_DB: myapp
      POSTGRES_USER: appuser
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U appuser -d myapp"]
      interval: 5s
      timeout: 3s
      retries: 10
      start_period: 20s
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql:ro
    networks:
      - backend
    restart: unless-stopped
    deploy:
      resources:
        limits:
          memory: 1G

  redis:
    image: redis:7-alpine
    command: redis-server --requirepass ${REDIS_PASSWORD}
    healthcheck:
      test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
      interval: 3s
      timeout: 2s
      retries: 5
    volumes:
      - redis_data:/data
    networks:
      - backend
    restart: unless-stopped
    deploy:
      resources:
        limits:
          memory: 512M

  # 监控组件,需要时才启动
  prometheus:
    image: prom/prometheus:latest
    profiles:
      - monitoring
    volumes:
      - ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml:ro
    networks:
      - backend
    ports:
      - "9090:9090"

  grafana:
    image: grafana/grafana:latest
    profiles:
      - monitoring
    volumes:
      - grafana_data:/var/lib/grafana
    networks:
      - frontend
    ports:
      - "3000:3000"
    depends_on:
      - prometheus

networks:
  frontend:
  backend:

volumes:
  postgres_data:
  redis_data:
  grafana_data:

secrets:
  db_password:
    file: ./secrets/db_password.txt
  redis_password:
    file: ./secrets/redis_password.txt

这份配置覆盖了:健康检查和启动顺序、网络隔离、Secrets 管理、资源限制、Profiles 按需启动监控、Watch 模式开发热加载、自动重启策略。基本上是一个生产可用的模板了。


Docker Compose 这个东西吧,入门门槛低,但上限比大多数人想象的高。很多人用了好几年还停留在最基础的阶段,其实多花半小时翻翻官方文档的 How-to 部分,能发现不少直接提升工作效率的东西。

不是所有场景都需要上 K8s 的。对于中小团队、开发测试环境、甚至一些流量不大的生产服务,一份写得好的 Compose 文件配合 CI/CD 就已经很够用了。关键是要把它用好、用全,别只会 image + ports 三板斧。

希望今天这些配置对你有用。如果你也有什么 Compose 的骚操作,欢迎留言交流。觉得有收获的话,转发给还在 sleep 10 的同事看看吧。


公众号:耕云躬行录

个人博客:躬行笔记

文章目录

博主介绍

热爱技术的云计算运维工程师,Python全栈工程师,分享开发经验与生活感悟。
欢迎关注我的微信公众号@运维躬行录,领取海量学习资料

微信二维码