Shhh 3.2.4 Docker Compose 部署指南
目标:用 Docker Compose 编排生产可用的 Shhh 3.2.4(PostgreSQL 16 + 自动迁移 + Gunicorn 三服务依赖链),完成创建/读取/一次性销毁验证,并沉淀迁移缺环境变量、漏迁移、网段冲突三个实测排障案例。
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)。多套自托管服务共用一台宿主机时,建议统一规划各栈子网。