Compose 写对了,运维就轻一半:一份能长期跑的自建栈模板

三个月后烂掉的 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,而是一个可以放心交给半年后的自己的系统:版本可控、入口唯一、数据可恢复、出事有日志。剩下的功能需求,都是往这个骨架里加服务而已。

延伸阅读

© 版权声明
THE END
喜欢就支持一下吧
点赞8 分享
评论 抢沙发
头像/SUENCA
欢迎您留下宝贵的见解!
提交
头像/SUENCA

昵称

取消
昵称表情代码图片快捷回复

    暂无评论内容