用 Docker Compose 把一个单实例小站跑起来

给一个笔记站点做部署,最后落到单机的三个容器上,PostgreSQL、Node 应用、Caddy 反向代理。编排文件不长,但有几个决定是踩过之后才定下来的,这里按顺序说。

启动顺序要显式声明

应用启动前数据库必须真的可连接,不只是容器进程存在。depends_on 里要用 healthcheck 条件。

services:
  db:
    image: postgres:17-alpine
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U \"$$POSTGRES_USER\" -d \"$$POSTGRES_DB\""]
      interval: 3s
      timeout: 3s
      retries: 30

  app:
    depends_on:
      db:
        condition: service_healthy

Postgres 进程起来到能接受连接之间有一段空窗,只看容器状态的应用会在迁移这一步报连接拒绝。healthcheck 用 pg_isready 探测,应用容器的入口脚本按固定顺序走,先等数据库可连接,再跑迁移,同步文章目录,最后启动 Node 服务。任何一步失败就退出,靠 restart: unless-stopped 重试没有意义,错误会立刻暴露。

密钥不写进 compose 文件

compose 文件里所有敏感值都是 ${VAR:?VAR is required} 的形式,缺一个就启动失败。真实密钥放在外部 env file 里,用 --env-file 注入。

docker compose --env-file deploy/env/.env.runtime \
  -f docker-compose.yml -f deploy/compose.gateway.yml up -d

这样编排文件可以放心提交进仓库,env file 永远不进版本库。? 修饰符比默认值写法更安全,拼错变量名会直接报错,而不是悄悄用空字符串连上一个不存在的库。

应用不直接暴露端口

应用容器只用 expose: 4321,在内部网络里给 Caddy 用,宿主机上只开 80/443 给 Caddy。数据库容器连 expose 都没有,外部网络完全够不到它。想连数据库排查问题,用 docker compose exec db psql 进容器,不开端口。

数据库数据放在命名卷里。

volumes:
  db-data:
  caddy-data:
  caddy-config:

Caddy 的证书缓存在 caddy-data 卷里,重建容器不用重新申请证书,也避免频繁申请触发 CA 的限流。

验证

部署完成的标志是健康检查接口返回正常,容器状态为 Up 还不够。

curl -fsS https://你的域名/api/healthz

这个接口会真的连一次数据库再返回,能同时确认应用和数据库都活着。