用Docker Compose搞定多容器应用的那些事
我的 docker-compose.yml 写法,亲测靠谱
我一般写 Docker Compose 文件不会一上来就堆 services,而是先想清楚几个问题:这个项目要不要本地开发联调?要不要持久化数据?环境变量怎么管?网络怎么配?想明白了再动手,不然改来改去特别烦。
这是我现在用得最多的一种结构:
version: '3.8'
services:
app:
build:
context: .
dockerfile: Dockerfile
ports:
- "3000:3000"
volumes:
- .:/app
- /app/node_modules
environment:
- NODE_ENV=development
env_file:
- .env
depends_on:
- db
stdin_open: true
tty: true
db:
image: postgres:14
ports:
- "5432:5432"
environment:
POSTGRES_DB: myapp_dev
POSTGRES_USER: devuser
POSTGRES_PASSWORD: devpass
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U devuser -d myapp_dev"]
interval: 10s
timeout: 5s
retries: 5
volumes:
pgdata:
这里有几个点我说说为啥这么写:
- 使用 volume 排除 node_modules:你可能看到
- /app/node_modules这一行有点奇怪,这是为了防止宿主机的 node_modules 覆盖容器里的,避免依赖执行异常。我之前没加这个,结果 npm install 装的东西全被清了,折腾了半天才发现是挂载覆盖的问题。 - healthcheck 加了才靠谱:
depends_on默认只等容器启动,不等服务真正 ready。PostgreSQL 启动要几秒,Node 服务一上来就连肯定失败。加上 healthcheck 之后,Compose 才会真正等数据库能连了再起 app。 - env_file + environment 混着用:敏感配置放
.env文件(记得 gitignore),通用的比如NODE_ENV直接写进去。别图省事全塞 environment 里,后期换环境容易炸。
这几种错误写法,别再踩坑了
下面这些是我和同事都犯过的错,有些现在看挺离谱,但当时真就这么干了……
1. 不加 healthcheck,瞎写 depends_on
services:
web:
depends_on:
- db
db:
image: mysql:8.0
这种写法等于没写。web 是等 db 容器起来了才启动,但 MySQL 可能还在初始化,根本连不上。结果就是 Node 服务报 connect ECONNREFUSED,重试几次就崩了。我以前以为 depends_on 是“等它准备好”,后来才知道它只是“等它跑起来”。
正确做法要么加 healthcheck,要么在应用层加重试逻辑(比如用 backoff 库连数据库)。
2. 把所有东西写死在 yaml 里
environment:
DATABASE_URL: postgres://devuser:devpass@db:5432/myapp_dev
REDIS_URL: redis://redis:6379
API_KEY: abc123xyz
千万别把密码、密钥这些东西直接写进 compose 文件!尤其是一不小心推到 GitHub 上,等于送黑客大礼包。API_KEY 这种应该走 env_file 或 secret 管理。
再说,开发、测试、生产环境 URL 肯定不一样,你难道每个环境都改一遍 yaml?太傻了。用 ${} 占位符才是正道:
environment:
DATABASE_URL: ${DATABASE_URL:-postgres://devuser:devpass@db:5432/myapp_dev}
这样本地没设就用默认值,CI 或服务器上设置了就用外部值,灵活又安全。
3. 忘记命名 volume,数据一删就没
volumes:
- ./data:/var/lib/mysql
这种叫“匿名挂载”,看着方便,实际巨坑。一旦你 docker-compose down -v,或者机器重启后路径变了,数据就没了。而且不好迁移。
我有个项目用户上传的图片全存在本地卷,一开始用的是上面这种写法,结果某次清理容器时手滑删了,恢复不了,产品经理差点把我开了……
现在我都显式定义 named volume:
volumes:
pgdata:
driver: local
这样 Docker 会统一管理,docker volume ls 能看到,备份迁移也方便。
实际项目中的坑
环境隔离做得不到位
我见过最乱的项目是:dev、staging、prod 全用一个 docker-compose.yml,靠手动改端口和镜像 tag 来区分。结果有一次上线,把 staging 的 DB 配置复制到了 prod 启动脚本里,连错了库,写坏数据。
现在我的做法是:
docker-compose.yml放通用配置docker-compose.override.yml放本地开发覆盖(比如挂载代码、开热重载)docker-compose.prod.yml放生产配置(用 built 镜像、关调试、加 restart)
启动的时候:
# 本地开发
docker-compose up
# 生产部署
docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d
这样各环境隔离清晰,不容易搞混。
build 和 image 混着用,CI 构建慢
有段时间我们每次部署都在服务器上 build,因为 compose 文件里写了 build。结果每次都要拉代码、装依赖、编译,五六分钟起不来服务,发布体验极差。
后来改成:CI 里用 Docker Buildx 打包镜像,打上 tag 推到 registry;prod compose 文件只写 image,不写 build。
关键点是:build 只该在开发环境用。生产必须用预构建镜像,否则不可控也不可复现。
忘了设 restart 策略
线上服务器重启一下,服务全挂,还得手动上去 docker-compose up,这就离谱。一定要加:
restart: unless-stopped
这样除非你明确 stop,否则容器崩溃或机器重启都会自动拉起来。别偷懒。
还有些小建议
- 别用 latest 标签。
image: nginx:latest看着省事,哪天 nginx 更新 break change,你就哭了。固定版本更稳。 - 给容器起个好名字,方便排查。用 container_name 显式指定,比如
container_name: app-db,不然默认是 project_service_num,难读。 - 日志别不管。加个 logging 配置,避免某个服务疯狂打日志把磁盘撑爆:
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
- http 服务挂反向代理的话,别把 Nginx 也塞进主 compose 文件。单独拆出来做 reverse proxy 更灵活,多个项目可以共用。
- 如果要用 secrets 或 configs(比如 TLS 证书),别硬编码。虽然大多数小项目用不到,但提前了解下机制没坏处。
最后一点:别指望 docker-compose 解决一切
docker-compose 在本地开发和小型部署里很好用,但上了规模就得考虑 Kubernetes 或 Nomad 了。别在 prod 死磕 compose 做集群、滚动更新、服务发现——不是它的活。
另外,compose 文件写得太复杂也不是好事。超过 50 行我就考虑拆成模块,或者用 Makefile 封装常用命令,比如:
up:
docker-compose up -d
logs:
docker-compose logs -f
down:
docker-compose down --remove-orphans
prod-up:
docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d
这样团队新人也能快速上手,不用记一堆参数。
以上是我总结的最佳实践
这套写法从踩坑、重构、再踩坑过来,现在算是稳定了。当然也不是完美方案,比如多架构构建(arm/vs amd)还得额外处理,但对大多数中小型项目够用了。
如果你有更好的组织方式,或者更优雅的环境管理策略,欢迎评论区交流。咱们都是搬砖的,互相抄作业才能早点下班。

暂无评论