Shhh 3.2.4 Docker Compose 部署指南

目标:用 Docker Compose 编排生产可用的 Shhh 3.2.4(PostgreSQL 16 + 自动迁移 + Gunicorn 三服务依赖链),完成创建/读取/一次性销毁验证,并沉淀迁移缺环境变量、漏迁移、网段冲突三个实测排障案例。

ShhhShhh2026/10/27

Shhh 3.2.4 Docker Compose 部署指南

目标:用 Docker Compose 部署一套生产可用的 Shhh 3.2.4(PostgreSQL 16 + 自动数据库迁移 + Gunicorn 应用三服务编排),完成创建/读取/一次性销毁全链路验证,并沉淀迁移缺环境变量、漏迁移、网段冲突三个实测排障案例。源码裸机部署路线见姊妹篇 《Shhh 3.2.4 Ubuntu 22.04 源码部署指南》。全部命令与回显均为 2026-10-26/27 实测记录。

一、环境信息

项目值
Shhh 镜像smallwat3r/shhh:latest(官方仅发布 latest 单标签,推送于 2025-10-23,即 3.2.4 tag 发布次日,实测 digest sha256:9450e11ea1df...f3655e2,amd64,约 274MB)
数据库镜像postgres:16-alpine
Docker / Compose 版本Docker 29.9.2 / Docker Compose v5.6.1
宿主端口8128(映射到应用监听的 8081)
Compose 项目名shhh-3.2.4-stack(文件内显式声明)
实测日期2026-10-26 ~ 2026-10-27

⚠️ 注意:官方镜像只有 latest 一个标签,无法通过标签锁定版本。部署时务必记录镜像 digest(docker image inspect smallwat3r/shhh:latest --format '{{index .RepoDigests 0}}'),升级前对比 digest 判断是否真的有新版本。

二、编排思路:为什么是三个服务

Shhh 官方镜像内不自动建表,数据库结构必须靠 flask db upgrade(Alembic 迁移)显式创建。因此一套可靠的编排需要三个服务形成依赖链:

db(postgres:16-alpine,healthcheck pg_isready)
  ↓ condition: service_healthy
migrate(跑一次 flask db upgrade 后退出)
  ↓ condition: service_completed_successfully
web(gunicorn 对外服务)

关键机制:

  • db 带 healthcheck:pg_isready 就绪前不启动后续服务,避免迁移在数据库未监听时连接失败。
  • migrate 是一次性任务:每次 docker compose up 都会重跑,Alembic 幂等——已迁移到最新则直接跳过;升级版本时它自动完成增量建表。
  • web 等待迁移成功:service_completed_successfully 保证 web 启动时表一定存在,杜绝「应用先起、表还没建」的竞态。

三、compose 文件全文

保存为 /opt/shhh-stack/docker-compose.yml:

name: shhh-3.2.4-stack

services:
  db:
    image: postgres:16-alpine
    container_name: shhh-3-2-4-db
    environment:
      POSTGRES_USER: shhh
      POSTGRES_PASSWORD: q1w2e3r4
      POSTGRES_DB: shhh
      PGDATA: /data/postgres
    volumes:
      - ./pgdata:/data/postgres
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U shhh -d shhh"]
      interval: 5s
      timeout: 3s
      retries: 10
    networks:
      - shhh-net
    restart: unless-stopped

  migrate:
    image: smallwat3r/shhh:latest
    command: flask db upgrade
    environment:
      FLASK_ENV: production
      FLASK_APP: wsgi.py
      DB_HOST: db
      DB_PORT: "5432"
      DB_USER: shhh
      DB_PASSWORD: q1w2e3r4
      DB_NAME: shhh
      DB_ENGINE: postgresql+psycopg2
    depends_on:
      db:
        condition: service_healthy
    networks:
      - shhh-net

  web:
    image: smallwat3r/shhh:latest
    container_name: shhh-3-2-4-web
    ports:
      - "8128:8081"
    environment:
      FLASK_ENV: production
      FLASK_APP: wsgi.py
      DB_HOST: db
      DB_PORT: "5432"
      DB_USER: shhh
      DB_PASSWORD: q1w2e3r4
      DB_NAME: shhh
      DB_ENGINE: postgresql+psycopg2
    depends_on:
      db:
        condition: service_healthy
      migrate:
        condition: service_completed_successfully
    networks:
      - shhh-net
    restart: unless-stopped

networks:
  shhh-net:
    driver: bridge
    ipam:
      config:
        - subnet: 10.89.58.0/24

要点说明:

  • name: shhh-3.2.4-stack 必须显式声明:Compose 默认用目录名当项目名,多人/多组件共用宿主机时,同名项目执行 docker compose down 会误删彼此的容器。显式命名后项目的容器、网络都带 shhh-324-stack 前缀(实测 migrate 容器名为 shhh-324-stack-migrate-1)。
  • 网络子网显式指定:默认 bridge 池可能与其他项目冲突,显式 subnet 一劳永逸(排障案例见 8.3)。
  • PGDATA: /data/postgres:把数据目录指到挂载点的子目录,避免 alpine 版 PostgreSQL 在挂载根目录的 lost+found 冲突问题。
  • migrate 不配 restart:它是跑完即退的一次性任务,Exited (0) 就是它的正常终态。
  • 生产环境请把示例口令 q1w2e3r4 换成强口令,并考虑用 env_file 或 secrets 管理敏感值。

四、启动

cd /opt/shhh-stack
docker compose up -d

实测回显(可清晰看到 db → migrate → web 的依赖链执行顺序):

 Container shhh-324-stack-migrate-1 Starting
 Container shhh-3-2-4-db Waiting
 Container shhh-324-stack-migrate-1 Waiting
 Container shhh-3-2-4-db Healthy
 Container shhh-324-stack-migrate-1 Exited
 Container shhh-3-2-4-web Starting
 Container shhh-3-2-4-web Started

状态检查(实测):

docker compose ps -a
NAME                       IMAGE                    COMMAND                  SERVICE   CREATED          STATUS                     PORTS
shhh-3-2-4-db              postgres:16-alpine       "docker-entrypoint.s…"   db        12 seconds ago   Up 12 seconds (healthy)    5432/tcp
shhh-3-2-4-web             smallwat3r/shhh:latest   "gunicorn -b :8081 -…"   web       12 seconds ago   Up 6 seconds               0.0.0.0:8128->8081/tcp, [::]:8128->8081/tcp
shhh-324-stack-migrate-1   smallwat3r/shhh:latest   "flask db upgrade"       migrate   12 seconds ago   Exited (0) 6 seconds ago

db 为 Up (healthy)、migrate 为 Exited (0)、web 为 Up 即正常。web 启动日志(实测,节选):

[2026-10-26 14:43:44 +0000] [1] [INFO] Starting gunicorn 23.0.0
[2026-10-26 14:43:44 +0000] [1] [INFO] Listening at: http://0.0.0.0:8081 (1)
[2026-10-26 14:43:44 +0000] [1] [INFO] Using worker: sync
[2026-10-26 14:43:44 +0000] [8] [INFO] Booting worker with pid: 8
[2026-10-26 14:43:44 +0000] [9] [INFO] Booting worker with pid: 9
[2026-10-26 14:43:44 +0000] [10] [INFO] Booting worker with pid: 10
[Mon, 26 Oct 2026 14:43:44] [sev 20] [INFO] [apscheduler.scheduler]> Scheduler started
[Mon, 26 Oct 2026 14:43:44] [sev 20] [INFO] [apscheduler.scheduler]> Added job "delete_expired_records" to job store "default"

gunicorn 3 worker 就绪、APScheduler 的过期清理任务(每 60 秒一轮)已注册,即可对外服务。

五、功能验证

口令强度规则:至少 8 字符、含 1 个数字、1 个大写字母(示例用 Q1w2e3r4)。API 参数全集见姊妹篇 《Shhh 3.2.4 API 使用详解》。

1. 创建一条秘密:

curl -sS -w '\nHTTP_STATUS:%{http_code}\n' -X POST http://127.0.0.1:8128/api/secret \
  -H 'Content-Type: application/json' \
  -d '{"passphrase":"Q1w2e3r4","secret":"compose-deploy-check-4n7pQx","expire":"3d","tries":5}'
{"response":{"details":"Secret successfully created.","expires_on":"October 30, 2026 at 00:16 UTC","external_id":"Y_IB12WWjoJooUnQDoaz","link":"http://127.0.0.1:8128/secret/Y_IB12WWjoJooUnQDoaz","status":"created"}}

HTTP_STATUS:201

2. 正确口令读取一次:

curl -sS -w '\nHTTP_STATUS:%{http_code}\n' \
  'http://127.0.0.1:8128/api/secret?external_id=Y_IB12WWjoJooUnQDoaz&passphrase=Q1w2e3r4'
{"response":{"msg":"compose-deploy-check-4n7pQx","status":"success"}}

HTTP_STATUS:200

3. 二次读取(一次性语义生效):

{"response":{"msg":"Sorry, we can't find a secret, it has expired, been deleted or has already been read.","status":"expired"}}

HTTP_STATUS:404

浏览器访问 http://<宿主机IP>:8128 即创建页面。

六、数据持久化与生命周期

  • 唯一状态在 PostgreSQL:秘密密文、过期时间、剩余尝试次数全部在 ./pgdata 挂载目录中(实测目录内含 PG_VERSION、base、pg_hba.conf 等标准 PostgreSQL 数据文件)。实测重建 web 容器后已有秘密仍可正常读取——应用层无状态,随时可重建。
  • 备份:对 db 服务执行 docker compose exec db pg_dump -U shhh shhh > shhh-$(date +%F).sql。
  • 销毁:docker compose down 只删容器与网络,./pgdata 目录保留;确认要彻底清除数据时再手动删除该目录。

七、日常运维

操作命令
查看应用日志docker compose logs -f web
查看迁移日志docker compose logs migrate
重启应用docker compose restart web
整栈停止 / 启动docker compose down / docker compose up -d
升级版本docker compose pull && docker compose up -d(migrate 服务自动跑增量迁移)

升级流程说明:官方镜像仅 latest 标签,docker compose pull 拉到新 digest 后 up -d 会重建 web 与 migrate,迁移由编排自动完成——这正是第二节三服务设计的回报:升级永远是一条命令的事。

八、排障实录

8.1 migrate 服务报 RuntimeError: env=None specified in FLASK_ENV is not supported

现象:migrate 服务 Exited (1),docker compose logs migrate 显示:

RuntimeError: env=None specified in FLASK_ENV is not supported

原因与修复:Shhh 的配置加载器强依赖 FLASK_ENV,migrate 服务虽然定义了全部 DB_* 变量,但漏传 FLASK_ENV: production 时一样起不来。给 migrate 服务补上该变量(本文第三节文件已含)后 Exited (0)。

8.2 跳过迁移,创建秘密报 An unexpected error has occurred

现象:直接启动 web(无 migrate 服务或迁移未跑)时,API 调用一律返回:

{"response":{"details":"An unexpected error has occurred","status":"error"}}

定位:docker logs 出现 CRITICAL 级日志:

Could not query required table 'secret'

修复:执行迁移(本编排中即让 migrate 服务跑一次:docker compose up migrate 或整栈重拉)。表结构由 Alembic 管理,应用永不自动建表——任何部署方式下这一步都不可省略。

8.3 启动报 Pool overlaps with other one on this address space

现象:docker compose up -d 报错:

invalid pool request: Pool overlaps with other one on this address space

原因:compose 文件声明的 10.89.58.0/24 子网已被其他 Docker 网络占用。用 docker network ls + docker network inspect <name> 找到占用者,或给本栈换一个空闲子网(改 compose 文件里的 subnet 后重新 up)。多套自托管服务共用一台宿主机时,建议统一规划各栈子网。

九、参考链接