三个月后烂掉的 compose,通常烂在同一个地方
刚装好的时候都能跑。三个月后再看,问题几乎总是这几类:镜像还停在 latest,某次自动拉取之后起不来了;数据库端口映射到了公网,日志里全是爆破记录;数据放在容器里,docker compose down 一敲全没了;日志文件涨到几十个 GB,把盘写满。
compose 本身不难,难的是把它当成一份要长期维护的契约,而不是一次性的启动脚本。下面六条,是我觉得差异最大的部分。
一、锁版本,别让 latest 替你决定升级时间
# 不推荐
image: louislam/uptime-kuma:latest
# 推荐:锁到具体版本,升级是一个动作
image: louislam/uptime-kuma:2.5.7
版本号是回滚的锚点。真要做到位,可以锁 digest(@sha256:...),连 tag 被重新推送都不怕。代价是每次升级要手动改,但这本来就是该由你决定的事。
二、网络分段:只有反向代理配 published 端口
很多人图省事,每个服务都写 ports: "8080:8080",最后机器上开了十几个口子。正确做法是让 Caddy 或 Traefik 独占 80/443,其他服务只在内部网络里被访问:
networks:
edge: # 能出网,给需要访问互联网的服务
internal:
internal: true # 完全隔离,只做容器间通信
services:
caddy:
networks: [edge, internal]
dozzle:
networks: [internal] # 不需要出网,也不需要 published 端口
数据库尤其要这样。Postgres、Redis 永远不该有 ports,需要本地连就用 docker compose exec 或者 SSH 隧道。
三、健康检查决定启动顺序,不是 depends_on 单独决定的
services:
db:
image: postgres:17-alpine
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d app"]
interval: 10s
timeout: 5s
retries: 5
app:
depends_on:
db:
condition: service_healthy
只写 depends_on: [db],Compose 只保证「容器起来了」,不保证「数据库能接受连接」。少了 condition: service_healthy,应用在开机时随机报连接失败,你就会开始怀疑人生。
四、日志和资源都要有上限
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
deploy:
resources:
limits:
memory: 1g
默认的 json-file 日志没有上限,一个话痨容器几周就能写满磁盘。内存限制则能防止某个服务泄漏把整台机器拖死——尤其在你还跑了别的服务的时候。
五、数据放卷里,备份交给专职容器
命名卷比 bind mount 更容易迁移和备份。备份不必自己写 cron 脚本,offen/docker-volume-backup 这类工具就是干这个的:挂上要备份的卷,配好 cron 和保留天数,它会打包、加密、传到 S3 或本地目录。
backup:
image: offen/docker-volume-backup:v2
restart: always
env_file: ./backup.env
volumes:
- kuma_data:/backup/kuma:ro
- /var/run/docker.sock:/var/run/docker.sock:ro
给需要一致性的服务加上 docker-volume-backup.stop-during-backup=true 标签,备份时会先停容器,备完再拉起来。数据库这类服务,别指望「运行中直接拷文件」能恢复。
六、密钥走 env_file,不进 git
env_file:
- ./.env
# chmod 600 .env
.env 里放密码、API key、S3 凭据,目录权限 600,提交前确认 .gitignore 里有它。compose 文件本身可以公开,密钥不行——这条底线被突破一次,后面所有安全措施都白搭。
把它们拼起来:一份能长期跑的参考栈
下面这份是最小可用版本:Caddy 负责 TLS 和入口,Uptime Kuma 做拨测,Dozzle 看日志,backup 负责卷备份。除了 Caddy,没有任何服务对外开端口。
name: edge
services:
caddy:
image: caddy:2-alpine
restart: unless-stopped
ports: ["80:80", "443:443", "443:443/udp"]
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
networks: [edge, internal]
logging:
driver: json-file
options: { max-size: "10m", max-file: "3" }
uptime-kuma:
image: louislam/uptime-kuma:2.5.7
restart: unless-stopped
volumes: [kuma_data:/app/data]
networks: [edge]
labels: ["docker-volume-backup.stop-during-backup=true"]
dozzle:
image: amir20/dozzle:v11.3.4
restart: unless-stopped
volumes: ["/var/run/docker.sock:/var/run/docker.sock:ro"]
networks: [internal]
backup:
image: offen/docker-volume-backup:v2
restart: always
env_file: ./backup.env
volumes:
- kuma_data:/backup/kuma:ro
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
edge:
internal:
internal: true
volumes:
caddy_data:
caddy_config:
kuma_data:
配套的 Caddyfile 用子域名,避免折腾子路径:
status.example.com {
encode zstd gzip
reverse_proxy uptime-kuma:3001
}
logs.example.com {
basic_auth {
admin $2a$14$替换成caddy_hash_password的输出
}
reverse_proxy dozzle:8080
}
密码哈希用 caddy hash-password --plaintext '你的密码' 生成。Dozzle 挂了 Docker socket,等于把宿主机 root 交出去,必须挡在认证后面——这也是它只放内网的原因。
升级和回滚,要写成步骤
- 升级前看 release notes,确认有没有数据库迁移;有迁移的先做卷快照。
docker compose pull && docker compose up -d --remove-orphans,然后docker compose ps看状态。- 回滚就是改回上一个 tag 再
up -d。但注意:数据库迁移通常是单向的,回滚镜像不等于回滚数据,所以快照才是最后一道保险。 - Watchtower 这类自动更新工具,只适合无状态、丢了也无所谓的服务。带数据库的,别交给它。
我的判断
compose 的水平,不体现在「能不能一次启动成功」,而体现在「换一台机器,能不能在半小时内一模一样地重建出来,并且数据还在」。
把这六条做到,你得到的不只是一份好看的 YAML,而是一个可以放心交给半年后的自己的系统:版本可控、入口唯一、数据可恢复、出事有日志。剩下的功能需求,都是往这个骨架里加服务而已。










暂无评论内容