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: 5PostgreSQL 的:
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-stoppedlimits 是硬限制,超了就 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 的同事看看吧。
公众号:耕云躬行录
个人博客:躬行笔记