个人项目和小团队的常见误判:容器化之后觉得"不用 K8s 就不专业"。事实是单机 docker-compose 能稳定支撑相当大的规模——本博客所在的那台 ECS 上跑着二十多个服务,全是 compose 管理,稳定运行一年以上。关键是纪律。

骨架:一份能活下去的 compose

services:
  app:
    image: myapp:1.4.2          # 用具体 tag,不用 latest
    restart: unless-stopped     # 崩溃自愈的底线
    networks: [frontend, backend]
    healthcheck:
      test: ["CMD", "curl", "-sf", "http://localhost:3000/healthz"]
      interval: 30s
      timeout: 3s
      retries: 3
    logging:
      driver: json-file
      options: { max-size: "10m", max-file: "3" }  # 不设就会吃满磁盘

  db:
    image: postgres:16
    restart: unless-stopped
    networks: [backend]         # 数据库不进 frontend 网络
    volumes: [pgdata:/var/lib/postgresql/data]

networks:
  frontend: {}
  backend: { internal: true }   # 后端网不开出站,少一层暴露面

volumes:
  pgdata: {}

四个要点:固定 tag、restart 策略、日志轮转、网络隔离。最后一条最常被忽略——internal: true 让数据库所在网络无法主动出站,应用被攻破也难横向外联。

环境变量与密钥

.env 进 .gitignore,仓库里放 .env.example。更敏感的(生产密钥)直接放宿主机 systemd 的 EnvironmentFile,不进项目目录。密钥泄露的第一大来源就是 compose 文件被提交进仓库。

升级与回滚

docker compose pull app && docker compose up -d app   # 升级
docker tag myapp:1.4.1 myapp:rollback && docker compose up -d app  # 回滚(保留旧镜像)

单机回滚的底气来自:镜像 tag 固定 + 数据都在 volume 里 + 配置在环境变量里。三者齐备,回滚就是 30 秒的事。

备份:compose 之外的定时任务

数据不在容器里,在 volume 里——备份 volume 就是备份全部状态:

# 每日凌晨:压缩卷 + 上传对象存储 + 保留 7 份
docker run --rm -v pgdata:/data -v /backup:/backup alpine \
  tar czf /backup/pgdata-$(date +%F).tar.gz -C /data .

没做备份的 compose 集群等于写在沙滩上。restore 演练至少做一次——没验证过恢复的备份不算备份。

什么时候才需要 K8s

出现以下信号再考虑:多机调度需求(单机资源到顶)、服务需要按优先级抢占、团队有了专职运维。在那之前,compose + 一台好机器 + 这份纪律,就是单人运维的最优解:所有状态一目了然,一个 YAML 讲清全部架构。