Browse Source

docker deploy

main
Rain Mark 4 weeks ago
parent
commit
554253e48d
  1. 22
      .dockerignore
  2. 39
      Dockerfile
  3. 8
      README.md
  4. 13
      deploy/caddy/Caddyfile
  5. 6
      deploy/env/hw.env.example
  6. 16
      deploy/systemd/hw.service
  7. 34
      docker/entrypoint.sh
  8. 4
      docs/development-setup.md
  9. 211
      docs/production-deployment.md
  10. 175
      docs/production-ops.md
  11. 3
      docs/requirements.md
  12. 4
      docs/technical-design.md
  13. 8
      docs/todo-progress.md
  14. 81
      scripts/deploy-container.sh
  15. 55
      scripts/deploy-systemd.sh
  16. 101
      scripts/install-linux-container.sh
  17. 55
      scripts/install-linux-systemd.sh
  18. 72
      scripts/verify-container-deployment.sh
  19. 34
      scripts/verify-deployment.sh

22
.dockerignore

@ -0,0 +1,22 @@
.git
.gitignore
.sisyphus/
.venv/
.pytest_cache/
__pycache__/
*.py[cod]
node_modules/
instance/
app/static/css/
playwright-report/
test-results/
ui-audit-home-dark.png
ui-audit-home-default.png
ui-audit-home-sidebar-collapsed.png
ui-polish-desktop-collapsed.png
ui-polish-desktop-default.png
ui-polish-mobile-final-check.png
ui-polish-mobile-hidden-current.png
tests/
docs/
README.md

39
Dockerfile

@ -0,0 +1,39 @@
FROM node:20-bookworm-slim AS assets
WORKDIR /opt/hw
COPY package.json package-lock.json ./
RUN npm ci
COPY app/static/src ./app/static/src
COPY app/templates ./app/templates
RUN mkdir -p ./app/static/css && npm run build:css
FROM python:3.13-slim-bookworm
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1
WORKDIR /opt/hw
RUN groupadd --system --gid 10001 hw \
&& useradd --system --uid 10001 --gid 10001 --create-home --home-dir /opt/hw hw
COPY requirements.txt ./requirements.txt
RUN pip install -r requirements.txt
COPY . .
COPY --from=assets /opt/hw/app/static/css/main.css /opt/hw/app/static/css/main.css
RUN chmod +x /opt/hw/docker/entrypoint.sh \
&& mkdir -p /opt/hw/instance/csv_previews \
&& chown -R hw:hw /opt/hw
USER hw
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 CMD python -c "import json, urllib.request; payload = json.load(urllib.request.urlopen('http://127.0.0.1:8000/health', timeout=3)); raise SystemExit(0 if payload.get('status') == 'ok' else 1)"
ENTRYPOINT ["/opt/hw/docker/entrypoint.sh"]

8
README.md

@ -30,10 +30,10 @@
- 压力测试数据生成命令 `flask seed-stress`(默认 1000 户、平均每户 4 人、`STRESS-` 前缀)
- Python + Playwright 端到端测试(管理员搜索编辑、长辈录入页保存、管理端新增户、家庭成员 CRUD、账号管理提交链路、审计日志筛选与详情、CSV 导入导出闭环、认证与权限边界)
- 当前最新一次本地验证结果:
- `.venv/bin/python -m pytest tests``102 passed`
- `.venv/bin/python -m pytest tests``107 passed`
- `.venv/bin/python -m pytest tests/e2e -q` → 请以本地最近一次执行结果为准(需先安装 Playwright Chromium)
- `bash -n scripts/generate-secrets.sh scripts/install-linux-systemd.sh scripts/deploy-systemd.sh scripts/verify-deployment.sh` → 通过
- 已补齐 Linux 单机生产部署交付:`ProductionConfig`、Gunicorn、systemd 服务模板、Caddy 反向代理模板、环境变量示例、部署/校验脚本、生产部署与运维文档
- `bash -n scripts/generate-secrets.sh scripts/install-linux-container.sh scripts/deploy-container.sh scripts/verify-container-deployment.sh` → 通过
- 已补齐 Linux 单机生产容器化部署交付:`ProductionConfig`、Python 3.13 容器镜像、Docker 入口脚本、环境变量示例、安装/部署/校验脚本、生产部署与运维文档
### 本地最快验证路径
@ -101,7 +101,7 @@ npm run watch:css
### 当前版本状态说明
- 当前版本以“核心可用版 + 生产部署交付”作为阶段性基线,功能范围暂不继续扩展
- 已交付 Linux 单机生产部署方案:Caddy + systemd + Gunicorn,并补齐部署脚本与运维文档
- 已交付 Linux 单机生产部署方案:Python 3.13 容器 + Gunicorn,并补齐部署脚本与运维文档
- 如未来恢复功能迭代,可优先考虑桌席安排、更细粒度权限 / CSRF 加固,以及更丰富的统计分析能力
### 当前内部版本已知限制

13
deploy/caddy/Caddyfile

@ -1,13 +0,0 @@
example.com {
encode zstd gzip
log {
output stderr
}
reverse_proxy 127.0.0.1:8000 {
header_up X-Forwarded-For {remote_host}
header_up X-Forwarded-Proto {scheme}
header_up Host {host}
}
}

6
deploy/env/hw.env.example

@ -1,5 +1,5 @@
# HappyWedding production environment variables
# Copy this file to /etc/hw/hw.env and fill in real values.
# Copy this file to /opt/hw.conf.d/hw.env and fill in real values.
HW_CONFIG=production
HW_SECRET_KEY=replace-with-a-long-random-secret
@ -7,5 +7,9 @@ HW_SECRET_KEY=replace-with-a-long-random-secret
# Recommended production database example (MySQL)
DATABASE_URL=mysql+pymysql://hw_user:replace-password@127.0.0.1:3306/hw
# Optional container runtime tuning
# HW_GUNICORN_WORKERS=2
# HW_GUNICORN_PORT=8000
# SQLite is only suitable for lightweight or temporary environments.
# DATABASE_URL=sqlite:////opt/hw/instance/happywedding.db

16
deploy/systemd/hw.service

@ -1,16 +0,0 @@
[Unit]
Description=HappyWedding Gunicorn Service
After=network.target
[Service]
Type=simple
User=hw
Group=hw
WorkingDirectory=/opt/hw
EnvironmentFile=/etc/hw/hw.env
ExecStart=/opt/hw/.venv/bin/gunicorn --workers 2 --bind 127.0.0.1:8000 run:app
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target

34
docker/entrypoint.sh

@ -0,0 +1,34 @@
#!/bin/sh
set -eu
APP_DIR="${APP_DIR:-/opt/hw}"
CONFIG_DIR="${CONFIG_DIR:-/opt/hw.conf.d}"
ENV_FILE="${ENV_FILE:-${CONFIG_DIR}/hw.env}"
INSTANCE_DIR="${INSTANCE_DIR:-${APP_DIR}/instance}"
SEED_MARKER_FILE="${SEED_MARKER_FILE:-${INSTANCE_DIR}/.seed-all-complete}"
if [ -f "$ENV_FILE" ]; then
set -a
# shellcheck disable=SC1090
. "$ENV_FILE"
set +a
fi
export FLASK_APP="${FLASK_APP:-run.py}"
export HW_CONFIG="${HW_CONFIG:-production}"
export PYTHONPATH="${PYTHONPATH:-${APP_DIR}}"
mkdir -p "$INSTANCE_DIR/csv_previews"
python -m flask db upgrade
if [ "${RUN_SEED_ALL:-0}" = "1" ] && [ ! -f "$SEED_MARKER_FILE" ]; then
python -m flask seed-all
: > "$SEED_MARKER_FILE"
fi
if [ "$#" -eq 0 ]; then
set -- gunicorn --workers "${HW_GUNICORN_WORKERS:-2}" --bind "0.0.0.0:${HW_GUNICORN_PORT:-8000}" run:app
fi
exec "$@"

4
docs/development-setup.md

@ -412,7 +412,7 @@ FLASK_APP=run.py .venv/bin/flask seed-stress --household-count 500 --members-per
- `ProductionConfig` 已设置 `PREFERRED_URL_SCHEME=https`
- `ProductionConfig.validate()` 会强制检查 `HW_SECRET_KEY``DATABASE_URL`
- `ProductionConfig` 已包含基础数据库连接池参数(`pool_pre_ping`、`pool_recycle`)
- 正式生产部署文档与 systemd / Caddy 模板`docs/production-deployment.md`
- 正式生产部署文档与容器化部署脚本`docs/production-deployment.md`
仍建议后续按正式上线需要继续补充:
@ -450,7 +450,7 @@ FLASK_APP=run.py .venv/bin/flask seed-stress --household-count 500 --members-per
## 12. 当前阶段说明
当前仓库已经完成核心可用版开发,并补齐 Linux 单机生产部署文档、systemd / Caddy 模板与配套脚本。按照当前约定,功能需求先暂停扩展,本阶段以部署落地、文档同步与必要修复为主。
当前仓库已经完成核心可用版开发,并补齐 Linux 单机生产容器化部署文档与配套脚本。按照当前约定,功能需求先暂停扩展,本阶段以部署落地、文档同步与必要修复为主。
如未来恢复功能迭代,可优先参考以下方向:

211
docs/production-deployment.md

@ -1,47 +1,55 @@
# HappyWedding 生产部署说明(Linux + systemd + Caddy
# HappyWedding 生产部署说明(Linux + Docker 容器
本文档说明如何在 Linux 单机上以 **Caddy + systemd + Gunicorn** 的方式部署 HappyWedding(`hw`)。
本文档说明如何在 Linux 单机上以 **Python 3.13 容器 + Gunicorn** 的方式部署 HappyWedding(`hw`)。
## 1. 部署拓扑
部署拓扑固定为:
```text
Internet
-> Caddy (80/443)
-> Gunicorn (127.0.0.1:8000)
Internet / 内网入口
-> 宿主机端口(默认 8000,可自行前置 nginx / Caddy / LB)
-> Docker container
-> Gunicorn (0.0.0.0:8000)
-> Flask app (run:app)
```
约定:
- Caddy 负责 HTTPS、反向代理与对外暴露
- Gunicorn 仅监听本机 `127.0.0.1:8000`
- systemd 负责托管 Gunicorn 进程
- 不再使用 systemd 托管应用进程
- 应用通过 Docker 容器运行,使用 Docker 的 `--restart unless-stopped` 负责常驻
- Flask 通过环境变量进入 `production` 配置
- 容器启动时会自动执行数据库迁移
- 容器镜像内已包含 Python 运行时依赖与编译后的 CSS 产物
## 2. 目录约定
文档和配套脚本统一使用以下目录:
方案统一使用以下目录:
- 应用目录:`/opt/hw`
- 环境变量目录:`/etc/hw`
- systemd unit:`/etc/systemd/system/hw.service`
- 环境文件:`/etc/hw/hw.env`
- 配置目录:`/opt/hw.conf.d`
- 环境文件:`/opt/hw.conf.d/hw.env`
- 可写实例目录:`/opt/hw/instance`
- CSV 预览目录:`/opt/hw/instance/csv_previews`
说明:
- `/opt/hw` 同时作为宿主机部署目录和容器内工作目录
- `/opt/hw.conf.d` 同时作为宿主机配置目录和容器内只读挂载目录
- `/opt/hw/instance` 会挂载进容器,保留 SQLite、CSV 预览等运行时数据
## 3. 服务器前置要求
请先在主机上准备:
请先在目标主机上准备:
1. Linux systemd 环境
2. Python 3
3. Caddy
4. 数据库(推荐 MySQL)
5. 可访问仓库代码的方式(git clone / rsync / scp 等)
1. Linux 主机
2. Docker Engine
- 若主机是 Debian / Ubuntu,可使用 `scripts/install-linux-container.sh` 自动安装
- 正式部署前请确认 Docker daemon 已启动
3. 数据库(推荐 MySQL)
4. 可访问仓库代码的方式(git clone / rsync / scp 等)
> 当前脚本**不负责安装 MySQL 或 Caddy 包本身**,它们应由你按主机环境单独安装
> 当前脚本不负责安装 MySQL。本方案默认应用运行时只依赖 Docker,不再依赖宿主机 Python / Node 运行环境
## 4. 生产配置项
@ -61,6 +69,10 @@ HappyWedding 当前依赖以下关键环境变量:
HW_CONFIG=production
HW_SECRET_KEY=replace-with-a-long-random-secret
DATABASE_URL=mysql+pymysql://hw_user:replace-password@127.0.0.1:3306/hw
# 可选:Gunicorn worker 数量
# HW_GUNICORN_WORKERS=2
# HW_GUNICORN_PORT=8000
```
### 4.1 生成随机密钥
@ -71,15 +83,15 @@ DATABASE_URL=mysql+pymysql://hw_user:replace-password@127.0.0.1:3306/hw
bash scripts/generate-secrets.sh
```
把输出内容填入 `/etc/hw/hw.env``HW_SECRET_KEY=`
把输出内容填入 `/opt/hw.conf.d/hw.env``HW_SECRET_KEY=`
### 4.2 数据库说明
- 正式生产建议使用 MySQL
- SQLite 仅适合轻量或临时环境
- 首次上线前至少执行一次
- 容器启动时会自动执行
- `flask db upgrade`
- 是否执行 `flask seed-all` 取决于你的数据库是否已初始化基础管理员和选项数据
- 是否执行 `flask seed-all` 取决于你是否在首次部署时显式开启 `RUN_SEED_ALL=1`
## 5. 首次部署步骤
@ -103,25 +115,25 @@ sudo git clone <your-repo-url> /opt/hw
在仓库根目录执行:
```bash
sudo bash scripts/install-linux-systemd.sh
sudo bash scripts/install-linux-container.sh
```
该脚本会:
1. 创建系统用户 `hw`(如不存在)
2. 创建 `/opt/hw`、`/etc/hw`、`/opt/hw/instance`、`/opt/hw/instance/csv_previews`
3. 创建虚拟环境 `/opt/hw/.venv`
4. 安装 / 更新 pip
5. 为后续 Tailwind CLI 编译准备 Node.js / npm 运行环境(需主机预先安装)
6. 安装 systemd 模板到 `/etc/systemd/system/hw.service`
7. 若 `/etc/hw/hw.env` 不存在,则从模板创建一份
1. 检查 Docker 是否可用
2. 若主机为 apt 系发行版且未安装 Docker,则自动安装 Docker
3. 创建 `/opt/hw`
4. 创建 `/opt/hw.conf.d`
5. 创建 `/opt/hw/instance`
6. 创建 `/opt/hw/instance/csv_previews`
7. 若 `/opt/hw.conf.d/hw.env` 不存在,则从模板初始化一份
### 5.3 编辑环境变量文件
打开并修改:
```bash
sudo vi /etc/hw/hw.env
sudo vi /opt/hw.conf.d/hw.env
```
至少填好:
@ -135,137 +147,128 @@ sudo vi /etc/hw/hw.env
执行:
```bash
sudo bash scripts/deploy-systemd.sh
```
如果你确定这是一个全新数据库,需要同时初始化基础数据:
```bash
sudo RUN_SEED_ALL=1 bash scripts/deploy-systemd.sh
sudo bash scripts/deploy-container.sh
```
该脚本会:
1. 使用 `/etc/hw/hw.env` 加载环境变量
2. 安装 `requirements.txt` 依赖
3. 安装 npm 依赖(如 `node_modules` 不存在或有变更)
4. 执行 `npm run build:css` 生成 `app/static/css/main.css`
5. 执行 `flask db upgrade`
6. 可选执行 `flask seed-all`
7. `systemctl daemon-reload`
8. `systemctl restart hw`
9. `systemctl enable hw`
## 6. 配置 systemd
仓库提供的模板文件是:
- `deploy/systemd/hw.service`
核心行为:
1. 读取 `/opt/hw.conf.d/hw.env`
2. 在 `/opt/hw` 下构建 Python 3.13 容器镜像
3. 停止并删除旧容器(如存在)
4. 挂载:
- `/opt/hw/instance -> /opt/hw/instance`
- `/opt/hw.conf.d -> /opt/hw.conf.d:ro`
5. 启动容器,并使用 `--restart unless-stopped`
6. 容器启动时自动执行 `flask db upgrade`
7. 启动 Gunicorn 对外监听 `0.0.0.0:8000`
- 以 `hw` 用户运行
- 工作目录固定为 `/opt/hw`
- 使用 `/etc/hw/hw.env` 作为环境变量文件
- 通过 Gunicorn 启动:
如果你确认这是一个全新空库,需要同时初始化基础数据:
```bash
/opt/hw/.venv/bin/gunicorn --workers 2 --bind 127.0.0.1:8000 run:app
sudo RUN_SEED_ALL=1 bash scripts/deploy-container.sh
```
如果你需要调整 worker 数量,可以修改 `/etc/systemd/system/hw.service` 中的 `ExecStart=`
> `seed-all` 会在首次成功执行后写入 `/opt/hw/instance/.seed-all-complete` 标记,避免每次重启容器都重复灌种子数据。
## 7. 配置 Caddy
## 6. 镜像与运行时行为
仓库提供的模板文件是
当前镜像交付包含
- `deploy/caddy/Caddyfile`
- Python 3.13 运行时
- `requirements.txt` 的 Python 依赖
- Node 构建阶段生成的 `app/static/css/main.css`
- 入口脚本 `docker/entrypoint.sh`
建议把模板复制到你的 Caddy 配置位置,例如:
入口脚本行为
```bash
sudo cp deploy/caddy/Caddyfile /etc/caddy/Caddyfile
```
然后把 `example.com` 改成你的真实域名。
模板行为:
1. 开启 `encode zstd gzip`
2. 反代到 `127.0.0.1:8000`
3. 透传 `Host`、`X-Forwarded-For`、`X-Forwarded-Proto`
4. `/health` 会直接由 Flask 响应
重载 Caddy:
1. 加载 `/opt/hw.conf.d/hw.env`(若存在)
2. 设置:
- `FLASK_APP=run.py`
- `HW_CONFIG=production`
3. 确保 `/opt/hw/instance/csv_previews` 存在
4. 执行 `python -m flask db upgrade`
5. 如 `RUN_SEED_ALL=1` 且尚未打过标记,则执行 `python -m flask seed-all`
6. 启动:
```bash
sudo systemctl reload caddy
gunicorn --workers ${HW_GUNICORN_WORKERS:-2} --bind 0.0.0.0:${HW_GUNICORN_PORT:-8000} run:app
```
## 8. 部署后验证
## 7. 部署后校验
仓库提供脚本:
- `scripts/verify-deployment.sh`
- `scripts/verify-container-deployment.sh`
默认检查:
1. systemd 服务 `hw` 是否 active
2. `127.0.0.1:8000` 是否有监听
3. `http://127.0.0.1/health` 是否返回 `status=ok`
1. Docker 容器 `hw` 是否运行中
2. Docker health 状态是否 healthy(如镜像含 healthcheck)
3. 宿主机 `8000` 端口是否有监听
4. `http://127.0.0.1:8000/health` 是否返回 `status=ok`
执行:
```bash
sudo bash scripts/verify-deployment.sh
sudo bash scripts/verify-container-deployment.sh
```
果你的健康检查地址不是默认值,可以覆盖
需覆盖端口或健康检查地址
```bash
sudo HEALTHCHECK_URL="https://your-domain/health" bash scripts/verify-deployment.sh
sudo HOST_PORT=18000 HEALTHCHECK_URL="http://127.0.0.1:18000/health" bash scripts/verify-container-deployment.sh
```
## 9. 常用管理命令
## 8. 常用管理命令
查看容器状态:
```bash
docker ps --filter name=hw
```
查看服务状态:
查看容器日志
```bash
sudo systemctl status hw --no-pager
docker logs -f hw
```
重启应用:
```bash
sudo systemctl restart hw
docker restart hw
```
查看应用日志
停止应用
```bash
sudo journalctl -u hw -f
docker stop hw
```
查看 Caddy 日志
重新部署最新代码
```bash
sudo journalctl -u caddy -f
cd /opt/hw
sudo bash scripts/deploy-container.sh
sudo bash scripts/verify-container-deployment.sh
```
## 10. 已知限制与注意事项
## 9. 已知限制与注意事项
1. 当前系统正式发布前仍建议补充 CSRF 防护
2. 当前前端样式依赖 Tailwind CLI 编译产物,生产环境需要保证部署流程执行过 `npm install``npm run build:css`
2. 当前前端样式依赖 Tailwind CLI 编译产物,但该产物已在镜像构建阶段自动生成
3. 当前生产配置会强制要求设置 `HW_SECRET_KEY``DATABASE_URL`,未设置时应用会直接启动失败
4. 如果使用 SQLite,请确保 `/opt/hw/instance` 可写
5. CSV 导入预览依赖 `/opt/hw/instance/csv_previews`,目录权限必须正常
6. 如你需要对外提供 HTTPS,可自行在容器前增加 nginx / Caddy / LB;当前交付只负责应用容器本身
## 11. 本轮交付文件索引
## 10. 本轮交付文件索引
- `deploy/systemd/hw.service`
- `deploy/caddy/Caddyfile`
- `Dockerfile`
- `.dockerignore`
- `docker/entrypoint.sh`
- `deploy/env/hw.env.example`
- `scripts/generate-secrets.sh`
- `scripts/install-linux-systemd.sh`
- `scripts/deploy-systemd.sh`
- `scripts/verify-deployment.sh`
- `scripts/install-linux-container.sh`
- `scripts/deploy-container.sh`
- `scripts/verify-container-deployment.sh`

175
docs/production-ops.md

@ -1,54 +1,65 @@
# HappyWedding 生产运维说明
# HappyWedding 生产运维说明(Docker 容器)
本文档对应 HappyWedding(`hw`)在 Linux 主机上以 **Caddy + systemd + Gunicorn** 方式运行时的日常运维操作。
本文档对应 HappyWedding(`hw`)在 Linux 主机上以 **Python 3.13 容器 + Gunicorn** 方式运行时的日常运维操作。
## 1. 服务组成
线上服务主要由以下部分组成:
1. `caddy`
- 对外提供 HTTP/HTTPS
- 反向代理到 `127.0.0.1:8000`
1. `hw` 容器
- 由 Docker 运行
- 容器内启动 Gunicorn
- Gunicorn 加载 `run:app`
- 容器启动时自动执行数据库迁移
2. `hw.service`
- 由 systemd 托管
- 启动 Gunicorn
- Gunicorn 载入 `run:app`
> 运维前提:Docker CLI 可用且 Docker daemon 已启动。
3. 数据库
2. 数据库
- 推荐 MySQL
- 连接串由 `/etc/hw/hw.env` 中的 `DATABASE_URL` 控制
- 连接串由 `/opt/hw.conf.d/hw.env` 中的 `DATABASE_URL` 控制
3. 宿主持久化目录
- `/opt/hw/instance`
- `/opt/hw/instance/csv_previews`
如需 HTTPS 或域名接入,可在容器前自行挂 nginx / Caddy / 负载均衡;本文档只覆盖应用容器本身。
## 2. 日常查看命令
### 2.1 查看应用服务状态
### 2.1 查看容器状态
```bash
sudo systemctl status hw --no-pager
docker ps --filter name=hw
```
### 2.2 查看应用日志
### 2.2 查看容器日志
```bash
sudo journalctl -u hw -f
docker logs -f hw
```
### 2.3 查看 Caddy 日志
查看最近 200 行:
```bash
sudo journalctl -u caddy -f
docker logs --tail 200 hw
```
### 2.4 查看健康检查
### 2.3 查看健康检查
```bash
curl -fsS http://127.0.0.1/health
docker inspect --format '{{json .State.Health}}' hw
```
如果走域名验证:
### 2.4 查看健康检查接口
```bash
curl -fsS https://your-domain/health
curl -fsS http://127.0.0.1:8000/health
```
如你发布到了其他端口:
```bash
curl -fsS http://127.0.0.1:<your-port>/health
```
## 3. 常规发布流程
@ -57,15 +68,15 @@ curl -fsS https://your-domain/health
```bash
cd /opt/hw
sudo bash scripts/deploy-systemd.sh
sudo bash scripts/verify-deployment.sh
sudo bash scripts/deploy-container.sh
sudo bash scripts/verify-container-deployment.sh
```
建议的发布后检查:
1. `systemctl status hw --no-pager`
2. `journalctl -u hw -n 100 --no-pager`
3. `curl -fsS https://your-domain/health`
1. `docker ps --filter name=hw`
2. `docker logs --tail 100 hw`
3. `curl -fsS http://127.0.0.1:8000/health`
4. 打开浏览器访问首页和登录页
## 4. 首次初始化基础数据
@ -74,33 +85,39 @@ sudo bash scripts/verify-deployment.sh
```bash
cd /opt/hw
sudo RUN_SEED_ALL=1 bash scripts/deploy-systemd.sh
sudo RUN_SEED_ALL=1 bash scripts/deploy-container.sh
```
> 已有生产数据时,不要重复无脑执行 `seed-all`,虽然项目当前多数 seed 命令偏幂等,但生产环境仍应谨慎
> 已有生产数据时,不要重复无脑执行 `seed-all`。当前脚本会通过 `/opt/hw/instance/.seed-all-complete` 做一次性标记,但生产环境仍应谨慎操作
## 5. 修改环境变量后的操作
如果你修改了 `/etc/hw/hw.env`,需要重启服务让其生效:
如果你修改了 `/opt/hw.conf.d/hw.env`,需要重新部署容器让其生效:
```bash
sudo systemctl restart hw
sudo bash /opt/hw/scripts/verify-deployment.sh
cd /opt/hw
sudo bash scripts/deploy-container.sh
sudo bash scripts/verify-container-deployment.sh
```
## 6. Caddy 配置变更
## 6. 重启 / 停止 / 删除容器
修改 Caddy 配置后建议
重启
```bash
sudo systemctl reload caddy
docker restart hw
```
若怀疑配置有问题,可先查看
停止
```bash
sudo systemctl status caddy --no-pager
sudo journalctl -u caddy -n 100 --no-pager
docker stop hw
```
删除(下次需重新部署):
```bash
docker rm -f hw
```
## 7. 回滚建议
@ -108,12 +125,13 @@ sudo journalctl -u caddy -n 100 --no-pager
当前仓库未内置自动回滚脚本,建议按以下原则手工回滚:
1. 保留最近一个稳定版本代码目录或 git tag
2. 回滚代码到稳定版本
2. 回滚 `/opt/hw` 代码到稳定版本
3. 重新执行:
```bash
sudo bash scripts/deploy-systemd.sh
sudo bash scripts/verify-deployment.sh
cd /opt/hw
sudo bash scripts/deploy-container.sh
sudo bash scripts/verify-container-deployment.sh
```
如果本次发布包含数据库迁移:
@ -134,49 +152,56 @@ mysqldump --single-transaction --quick --default-character-set=utf8mb4 -u <user>
### 8.2 SQLite(仅轻量环境)
停止服务后备份:
停止容器后备份:
```bash
sudo systemctl stop hw
docker stop hw
cp /opt/hw/instance/happywedding.db /opt/hw/instance/happywedding.db.bak
sudo systemctl start hw
docker start hw
```
## 9. 常见故障排查
### 9.1 `hw.service` 启动失败
### 9.1 容器启动失败
先看:
```bash
sudo systemctl status hw --no-pager
sudo journalctl -u hw -n 200 --no-pager
docker ps -a --filter name=hw
docker logs --tail 200 hw
```
优先检查:
1. `/etc/hw/hw.env` 是否存在
1. `/opt/hw.conf.d/hw.env` 是否存在
2. `HW_SECRET_KEY` 是否仍是占位值
3. `DATABASE_URL` 是否为空或格式错误
4. `/opt/hw/.venv` 是否存在
5. Gunicorn 是否已安装到虚拟环境中
4. `/opt/hw/instance` 是否可写
5. 数据库是否可连通
### 9.2 验脚本失败:端口未监听
### 9.2 验脚本失败:端口未监听
说明 Gunicorn 没有在 `127.0.0.1:8000` 正常起来。检查:
说明容器没有在预期端口正常起来。检查:
```bash
sudo journalctl -u hw -n 200 --no-pager
docker logs --tail 200 hw
ss -tln | grep ':8000 '
```
如果宿主机没有 `ss`(例如部分 macOS 环境),可改用:
```bash
lsof -nP -iTCP:8000 -sTCP:LISTEN
```
### 9.3 `/health` 不通
可能原因:
1. `hw.service` 没起来
2. Caddy 没 reload 成功
3. 域名未正确指向主机
4. 防火墙未放行 80/443
1. 容器未正常运行
2. 容器内 Gunicorn 未起来
3. 宿主机端口映射不正确
4. 你实际发布的不是默认端口 8000
### 9.4 CSV 相关功能异常
@ -185,43 +210,25 @@ sudo journalctl -u hw -n 200 --no-pager
- `/opt/hw/instance`
- `/opt/hw/instance/csv_previews`
它们需要对运行用户 `hw` 可写。
它们需要对容器运行用户可写。
## 10. 安全提醒
1. 生产环境不要使用默认 `HW_SECRET_KEY`
2. `/etc/hw/hw.env` 应限制读取权限
3. Gunicorn 仅监听本机,不要直接暴露到公网
2. `/opt/hw.conf.d/hw.env` 应限制读取权限
3. 如需对外暴露,请在容器前增加反向代理或防火墙规则,不建议裸暴露管理后台
4. 当前项目仍建议在正式发布前补充 CSRF
5. 如果主机是内网 / 离线环境,需要确保发布包中已经包含编译后的 `app/static/css/main.css`,或确保部署流程可在主机上执行 `npm run build:css`
## 10.1 前端样式构建提醒
当前生产样式不再依赖 CDN,而是依赖 Tailwind CLI 编译产物。
如发布包含样式改动,建议在 `/opt/hw` 下额外确认:
```bash
cd /opt/hw
npm install
npm run build:css
```
然后再执行:
```bash
sudo bash scripts/deploy-systemd.sh
```
5. 如果宿主机是内网 / 离线环境,需要确保部署时 Docker 能成功构建镜像,或提前准备好基础镜像与依赖访问能力
## 11. 建议保留的最小操作清单
建议把以下命令记录到你的运维笔记中:
```bash
sudo systemctl status hw --no-pager
sudo systemctl restart hw
sudo journalctl -u hw -f
sudo journalctl -u caddy -f
sudo bash /opt/hw/scripts/verify-deployment.sh
curl -fsS https://your-domain/health
docker ps --filter name=hw
docker logs -f hw
docker restart hw
sudo bash /opt/hw/scripts/deploy-container.sh
sudo bash /opt/hw/scripts/verify-container-deployment.sh
curl -fsS http://127.0.0.1:8000/health
```

3
docs/requirements.md

@ -457,11 +457,10 @@
- CSV 导入导出(模板下载、导出全部、导出当前筛选、上传预览、校验、冲突处理、确认导入)
- light / dark 双主题切换
- 压力测试数据生成功能(`seed-stress`)
- Linux 单机生产部署交付(`ProductionConfig`、Gunicorn、systemd 服务模板、Caddy 反向代理模板、环境变量示例、部署脚本、校验脚本、生产部署与运维文档)
- Linux 单机生产部署交付(`ProductionConfig`、Python 3.13 容器镜像、Docker 入口脚本、环境变量示例、安装/部署/校验脚本、生产部署与运维文档)
### 当前阶段边界
- 当前版本已形成“核心可用版 + 生产部署交付”的阶段性基线
- 按当前约定,功能需求先暂停扩展,优先保证部署落地、文档同步与必要缺陷修复
- 如未来恢复功能迭代,可优先考虑更丰富统计报表分析、桌席安排、历史往来追溯增强、数据快照回滚、更细权限控制与更智能的还礼参考

4
docs/technical-design.md

@ -1039,8 +1039,8 @@ hw/
- 中文 / 拼音 / 首字母搜索
- CSV 模板下载、导出全部、导出当前筛选、上传预览、校验、冲突处理、确认导入与导入审计日志
- Python + Playwright E2E 基础设施与核心流程测试(管理员编辑、长辈录入、管理端新增户、家庭成员 CRUD、账号管理提交链路、审计日志筛选与详情、CSV 导入导出、认证与权限边界)
- Linux 单机生产部署闭环:`ProductionConfig`、Gunicorn 依赖、systemd 服务模板、Caddy 反向代理模板、环境变量示例、部署脚本、校验脚本、生产部署文档与运维文档
- 当前最新一次本地全量测试:`.venv/bin/python -m pytest tests` → `102 passed`
- Linux 单机生产部署闭环:`ProductionConfig`、Python 3.13 容器镜像、Docker 入口脚本、环境变量示例、安装/部署/校验脚本、生产部署文档与运维文档
- 当前最新一次本地全量测试:`.venv/bin/python -m pytest tests` → `107 passed`
### 当前阶段边界

8
docs/todo-progress.md

@ -105,16 +105,16 @@
- [x] 独立统计与报表页
- [x] light / dark 双主题切换
- [x] Python + Playwright 端到端测试基础设施与主要操作闭环补齐
- [x] Linux 单机生产部署交付:`ProductionConfig`、Gunicorn、systemd 模板、Caddy 模板、环境变量示例、部署脚本、校验脚本、生产文档与运维文档
- [x] Linux 单机生产部署交付:`ProductionConfig`、Python 3.13 容器镜像、Docker 入口脚本、环境变量示例、安装/部署/校验脚本、生产文档与运维文档
## 3. 当前重点任务建议
当前核心可用版本已经形成闭环,范围包括:登录与权限、一键开发启动与演示 seed、压力测试 seed-stress、后台左侧边栏、管理首页只读总览、中文 / 拼音 / 首字母搜索、组合筛选、排序、筛选统计、管理首页与长辈页动态刷新、户级单条编辑、管理端新增一户、管理端家庭成员 CRUD、管理端礼金明细 CRUD 与自动汇总回写、长辈专用搜索与 bottom-sheet 有限字段编辑、独立统计与报表页、账号管理、审计日志、CSV 导入导出、light / dark 双主题、Python + Playwright 端到端测试,以及 Linux 单机生产部署文档与脚本交付。
当前核心可用版本已经形成闭环,范围包括:登录与权限、一键开发启动与演示 seed、压力测试 seed-stress、后台左侧边栏、管理首页只读总览、中文 / 拼音 / 首字母搜索、组合筛选、排序、筛选统计、管理首页与长辈页动态刷新、户级单条编辑、管理端新增一户、管理端家庭成员 CRUD、管理端礼金明细 CRUD 与自动汇总回写、长辈专用搜索与 bottom-sheet 有限字段编辑、独立统计与报表页、账号管理、审计日志、CSV 导入导出、light / dark 双主题、Python + Playwright 端到端测试,以及 Linux 单机生产容器化部署文档与脚本交付。
当前阶段说明:
- 功能需求先暂停扩展,当前以部署落地、文档同步、运维校验和必要缺陷修复为主
- 最新一次本地全量测试结果:`.venv/bin/python -m pytest tests` → `102 passed`
- 最新一次本地全量测试结果:`.venv/bin/python -m pytest tests` → `107 passed`
- 如未来恢复功能迭代,可参考桌席安排、CSRF / 安全加固、统计报表增强、历史往来能力等方向
## 4. 风险与注意事项
@ -161,7 +161,7 @@ MySQL 与 SQLite 存在差异,开发时应减少特性耦合。
- 已完成页面样式基线从 Skeleton CSS 切换到 Tailwind CSS,并进一步迁移到 Tailwind CLI 编译模式
- 已将审计服务扩展到长辈页与管理端业务保存场景(`update_household_entry` / `update_household`
- 已完成管理员侧礼金明细闭环:支持明细新增、编辑、软删除、户级礼金自动汇总回写与审计日志
- 已完成 Linux 单机生产部署文档、systemd / Caddy 模板与配套脚本交付
- 已完成 Linux 单机生产容器化部署文档与配套脚本交付
- 当前进入部署与文档同步阶段,功能需求暂不继续扩展
- 已完成管理端家庭成员管理闭环:在户级编辑页支持成员列表展示、成员新增/编辑/删除,以及对应审计日志与测试覆盖
- 已完成管理首页搜索与统计增强:支持中文 / 拼音 / 首字母搜索、组合筛选、排序、筛选结果计数与摘要统计

81
scripts/deploy-container.sh

@ -0,0 +1,81 @@
#!/usr/bin/env bash
set -euo pipefail
APP_DIR="${APP_DIR:-/opt/hw}"
CONFIG_DIR="${CONFIG_DIR:-/opt/hw.conf.d}"
ENV_FILE="${ENV_FILE:-$CONFIG_DIR/hw.env}"
CONTAINER_NAME="${CONTAINER_NAME:-hw}"
IMAGE_NAME="${IMAGE_NAME:-hw}"
IMAGE_TAG="${IMAGE_TAG:-py313}"
APP_PORT="${APP_PORT:-8000}"
RUN_SEED_ALL="${RUN_SEED_ALL:-0}"
HOST_PORT="${HOST_PORT:-8000}"
ALLOW_NON_ROOT="${ALLOW_NON_ROOT:-0}"
CONTAINER_UID="${CONTAINER_UID:-10001}"
CONTAINER_GID="${CONTAINER_GID:-10001}"
require_root() {
if [[ "$ALLOW_NON_ROOT" == "1" ]]; then
return
fi
if [[ "${EUID}" -ne 0 ]]; then
printf 'Please run this script as root.\n' >&2
exit 1
fi
}
require_file() {
local path="$1"
if [[ ! -f "$path" ]]; then
printf 'Required file not found: %s\n' "$path" >&2
exit 1
fi
}
require_docker_daemon() {
if ! docker info >/dev/null 2>&1; then
printf 'Docker daemon is not available. Please start Docker and re-run this script.\n' >&2
exit 1
fi
}
main() {
require_root
require_file "$ENV_FILE"
require_file "$APP_DIR/Dockerfile"
require_file "$APP_DIR/run.py"
require_docker_daemon
install -d -m 755 "$APP_DIR/instance"
install -d -m 755 "$APP_DIR/instance/csv_previews"
if [[ "$ALLOW_NON_ROOT" == "1" ]]; then
chown -R "$CONTAINER_UID:$CONTAINER_GID" "$APP_DIR/instance" >/dev/null 2>&1 || true
else
chown -R "$CONTAINER_UID:$CONTAINER_GID" "$APP_DIR/instance"
fi
docker build -t "${IMAGE_NAME}:${IMAGE_TAG}" "$APP_DIR"
if docker ps -a --format '{{.Names}}' | grep -Fxq "$CONTAINER_NAME"; then
docker rm -f "$CONTAINER_NAME" >/dev/null
fi
docker run -d \
--name "$CONTAINER_NAME" \
--env-file "$ENV_FILE" \
-e RUN_SEED_ALL="$RUN_SEED_ALL" \
-e HW_GUNICORN_PORT="$APP_PORT" \
-p "$HOST_PORT:$APP_PORT" \
-v "$APP_DIR/instance:/opt/hw/instance" \
-v "$CONFIG_DIR:/opt/hw.conf.d:ro" \
--restart unless-stopped \
"${IMAGE_NAME}:${IMAGE_TAG}"
printf 'Container deployment completed.\n'
printf 'Container: %s\n' "$CONTAINER_NAME"
printf 'Image: %s:%s\n' "$IMAGE_NAME" "$IMAGE_TAG"
printf 'Published port: %s -> %s\n' "$HOST_PORT" "$APP_PORT"
}
main "$@"

55
scripts/deploy-systemd.sh

@ -1,55 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
APP_DIR="${APP_DIR:-/opt/hw}"
ENV_FILE="${ENV_FILE:-/etc/hw/hw.env}"
SERVICE_NAME="${SERVICE_NAME:-hw}"
RUN_SEED_ALL="${RUN_SEED_ALL:-0}"
require_root() {
if [[ "${EUID}" -ne 0 ]]; then
printf 'Please run this script as root.\n' >&2
exit 1
fi
}
require_file() {
local path="$1"
if [[ ! -f "$path" ]]; then
printf 'Required file not found: %s\n' "$path" >&2
exit 1
fi
}
main() {
require_root
require_file "$ENV_FILE"
require_file "$APP_DIR/requirements.txt"
require_file "$APP_DIR/run.py"
set -a
# shellcheck disable=SC1090
source "$ENV_FILE"
set +a
"$APP_DIR/.venv/bin/python" -m pip install --upgrade pip
"$APP_DIR/.venv/bin/python" -m pip install -r "$APP_DIR/requirements.txt"
export FLASK_APP=run.py
export HW_CONFIG="${HW_CONFIG:-production}"
(cd "$APP_DIR" && "$APP_DIR/.venv/bin/python" -m flask db upgrade)
if [[ "$RUN_SEED_ALL" == "1" ]]; then
(cd "$APP_DIR" && "$APP_DIR/.venv/bin/python" -m flask seed-all)
fi
systemctl daemon-reload
systemctl restart "$SERVICE_NAME"
systemctl enable "$SERVICE_NAME"
printf 'Deployment completed.\n'
printf 'Use: systemctl status %s --no-pager\n' "$SERVICE_NAME"
}
main "$@"

101
scripts/install-linux-container.sh

@ -0,0 +1,101 @@
#!/usr/bin/env bash
set -euo pipefail
APP_DIR="${APP_DIR:-/opt/hw}"
CONFIG_DIR="${CONFIG_DIR:-/opt/hw.conf.d}"
DEFAULT_HOST_USER="${SUDO_USER:-$(id -un)}"
APP_USER="${APP_USER:-$DEFAULT_HOST_USER}"
APP_GROUP="${APP_GROUP:-$(id -gn "$APP_USER" 2>/dev/null || id -gn)}"
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
ENV_TEMPLATE_SOURCE="${ENV_TEMPLATE_SOURCE:-$PROJECT_ROOT/deploy/env/hw.env.example}"
ENV_FILE_NAME="${ENV_FILE_NAME:-hw.env}"
DOCKER_INSTALL_SCRIPT_URL="https://get.docker.com"
ALLOW_NON_ROOT="${ALLOW_NON_ROOT:-0}"
CONTAINER_UID="${CONTAINER_UID:-10001}"
CONTAINER_GID="${CONTAINER_GID:-10001}"
require_root() {
if [[ "$ALLOW_NON_ROOT" == "1" ]]; then
return
fi
if [[ "${EUID}" -ne 0 ]]; then
printf 'Please run this script as root.\n' >&2
exit 1
fi
}
command_exists() {
command -v "$1" >/dev/null 2>&1
}
require_command() {
if ! command_exists "$1"; then
printf 'Required command not found: %s\n' "$1" >&2
exit 1
fi
}
install_docker_if_missing() {
if command_exists docker; then
return
fi
if command_exists apt-get; then
printf 'Docker not found. Installing Docker via %s ...\n' "$DOCKER_INSTALL_SCRIPT_URL"
curl -fsSL "$DOCKER_INSTALL_SCRIPT_URL" | sh
return
fi
printf 'Docker is not installed and automatic installation is only supported on apt-based hosts.\n' >&2
printf 'Please install Docker manually, then re-run this script.\n' >&2
exit 1
}
main() {
require_root
require_command curl
install_docker_if_missing
install -d -m 755 -o "$APP_USER" -g "$APP_GROUP" "$APP_DIR"
install -d -m 755 -o "$APP_USER" -g "$APP_GROUP" "$APP_DIR/instance"
install -d -m 755 -o "$APP_USER" -g "$APP_GROUP" "$APP_DIR/instance/csv_previews"
if [[ "$ALLOW_NON_ROOT" == "1" ]]; then
install -d -m 750 "$CONFIG_DIR"
else
install -d -m 750 -o root -g "$APP_GROUP" "$CONFIG_DIR"
fi
if [[ "$ALLOW_NON_ROOT" == "1" ]]; then
chown -R "$CONTAINER_UID:$CONTAINER_GID" "$APP_DIR/instance" >/dev/null 2>&1 || true
else
chown -R "$CONTAINER_UID:$CONTAINER_GID" "$APP_DIR/instance"
fi
if [[ ! -f "$CONFIG_DIR/$ENV_FILE_NAME" ]]; then
if [[ "$ALLOW_NON_ROOT" == "1" ]]; then
install -m 640 "$ENV_TEMPLATE_SOURCE" "$CONFIG_DIR/$ENV_FILE_NAME"
else
install -m 640 -o root -g "$APP_GROUP" "$ENV_TEMPLATE_SOURCE" "$CONFIG_DIR/$ENV_FILE_NAME"
fi
printf 'Created %s from template. Please edit it before deployment.\n' "$CONFIG_DIR/$ENV_FILE_NAME"
else
printf 'Keeping existing %s\n' "$CONFIG_DIR/$ENV_FILE_NAME"
fi
if [[ "$ALLOW_NON_ROOT" != "1" ]] && id -u "$APP_USER" >/dev/null 2>&1; then
usermod -aG docker "$APP_USER" >/dev/null 2>&1 || true
fi
printf '\nContainer deployment prerequisites are ready.\n'
printf 'Next steps:\n'
printf '1. Sync project files into %s\n' "$APP_DIR"
printf '2. Edit %s/%s with real production values\n' "$CONFIG_DIR" "$ENV_FILE_NAME"
if [[ "$ALLOW_NON_ROOT" == "1" ]]; then
printf '3. Run ALLOW_NON_ROOT=1 bash scripts/deploy-container.sh\n'
else
printf '3. Run sudo bash scripts/deploy-container.sh\n'
fi
}
main "$@"

55
scripts/install-linux-systemd.sh

@ -1,55 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
APP_USER="${APP_USER:-hw}"
APP_GROUP="${APP_GROUP:-$APP_USER}"
APP_DIR="${APP_DIR:-/opt/hw}"
ENV_DIR="${ENV_DIR:-/etc/hw}"
SERVICE_NAME="${SERVICE_NAME:-hw}"
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
require_root() {
if [[ "${EUID}" -ne 0 ]]; then
printf 'Please run this script as root.\n' >&2
exit 1
fi
}
ensure_user() {
if ! id -u "$APP_USER" >/dev/null 2>&1; then
useradd --system --create-home --shell /usr/sbin/nologin "$APP_USER"
fi
}
main() {
require_root
ensure_user
install -d -m 755 -o "$APP_USER" -g "$APP_GROUP" "$APP_DIR"
install -d -m 755 -o "$APP_USER" -g "$APP_GROUP" "$APP_DIR/instance"
install -d -m 755 -o "$APP_USER" -g "$APP_GROUP" "$APP_DIR/instance/csv_previews"
install -d -m 750 -o root -g "$APP_GROUP" "$ENV_DIR"
if [[ ! -d "$APP_DIR/.venv" ]]; then
python3 -m venv "$APP_DIR/.venv"
fi
"$APP_DIR/.venv/bin/python" -m pip install --upgrade pip
install -m 644 "$PROJECT_ROOT/deploy/systemd/hw.service" "/etc/systemd/system/${SERVICE_NAME}.service"
if [[ ! -f "$ENV_DIR/hw.env" ]]; then
install -m 640 -o root -g "$APP_GROUP" "$PROJECT_ROOT/deploy/env/hw.env.example" "$ENV_DIR/hw.env"
printf 'Created %s/hw.env from template. Please edit it before starting the service.\n' "$ENV_DIR"
else
printf 'Keeping existing %s/hw.env\n' "$ENV_DIR"
fi
printf '\nNext steps:\n'
printf '1. Sync project files into %s\n' "$APP_DIR"
printf '2. Edit %s/hw.env with real production values\n' "$ENV_DIR"
printf '3. Copy deploy/caddy/Caddyfile into your Caddy config and replace the domain\n'
printf '4. Run scripts/deploy-systemd.sh as root\n'
}
main "$@"

72
scripts/verify-container-deployment.sh

@ -0,0 +1,72 @@
#!/usr/bin/env bash
set -euo pipefail
CONTAINER_NAME="${CONTAINER_NAME:-hw}"
HOST_PORT="${HOST_PORT:-${APP_PORT:-8000}}"
HEALTHCHECK_URL="${HEALTHCHECK_URL:-http://127.0.0.1:${HOST_PORT}/health}"
ALLOW_NON_ROOT="${ALLOW_NON_ROOT:-0}"
fail() {
printf 'Verification failed: %s\n' "$1" >&2
exit 1
}
command_exists() {
command -v "$1" >/dev/null 2>&1
}
require_docker_daemon() {
if ! docker info >/dev/null 2>&1; then
fail "docker daemon is not available"
fi
}
port_is_listening() {
local port="$1"
if command_exists ss; then
ss -tln | grep -q ":${port} "
return
fi
if command_exists lsof; then
lsof -nP -iTCP:"${port}" -sTCP:LISTEN >/dev/null 2>&1
return
fi
if command_exists netstat; then
netstat -an | grep -E -q "[\.:]${port}[[:space:]].*LISTEN"
return
fi
fail "no supported port inspection command found (need one of: ss, lsof, netstat)"
}
main() {
require_docker_daemon
if ! docker ps --format '{{.Names}}' | grep -Fxq "$CONTAINER_NAME"; then
fail "container '$CONTAINER_NAME' is not running"
fi
health_status="$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' "$CONTAINER_NAME")"
if [[ "$health_status" != "healthy" && "$health_status" != "none" ]]; then
fail "container health status is '$health_status'"
fi
if ! port_is_listening "$HOST_PORT"; then
fail "nothing is listening on TCP port ${HOST_PORT}"
fi
health_payload="$(curl --fail --silent --show-error --max-time 10 "$HEALTHCHECK_URL")" || fail "healthcheck request failed: $HEALTHCHECK_URL"
if ! printf '%s' "$health_payload" | grep -q '"status":"ok"\|"status": "ok"'; then
fail "healthcheck response does not contain status=ok"
fi
printf 'Verification succeeded.\n'
printf 'Container: %s\n' "$CONTAINER_NAME"
printf 'Port: %s\n' "$HOST_PORT"
printf 'Healthcheck: %s\n' "$HEALTHCHECK_URL"
}
main "$@"

34
scripts/verify-deployment.sh

@ -1,34 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
SERVICE_NAME="${SERVICE_NAME:-hw}"
APP_PORT="${APP_PORT:-8000}"
HEALTHCHECK_URL="${HEALTHCHECK_URL:-http://127.0.0.1/health}"
fail() {
printf 'Verification failed: %s\n' "$1" >&2
exit 1
}
main() {
if ! systemctl is-active --quiet "$SERVICE_NAME"; then
fail "systemd service '$SERVICE_NAME' is not active"
fi
if ! ss -tln | grep -q ":${APP_PORT} "; then
fail "nothing is listening on TCP port ${APP_PORT}"
fi
health_payload="$(curl --fail --silent --show-error --max-time 10 "$HEALTHCHECK_URL")" || fail "healthcheck request failed: $HEALTHCHECK_URL"
if ! printf '%s' "$health_payload" | grep -q '"status":"ok"\|"status": "ok"'; then
fail "healthcheck response does not contain status=ok"
fi
printf 'Verification succeeded.\n'
printf 'Service: %s\n' "$SERVICE_NAME"
printf 'Port: %s\n' "$APP_PORT"
printf 'Healthcheck: %s\n' "$HEALTHCHECK_URL"
}
main "$@"
Loading…
Cancel
Save