Compare commits

..
3 Commits
6 changed files with 199 additions and 2 deletions
+6
View File
@@ -28,3 +28,9 @@ package-lock.json
# local vendor for agent test env (not shipped)
.vendor/
vendor_wheels/
# 正式环境部署产物(证书私钥、备份)
deploy/tls/certs/
backups/
ALERT.log
backup.log
+13
View File
@@ -16,3 +16,16 @@ APP_BOOTSTRAP_ADMIN_PASSWORD=change-me-in-local-env
# 内网测试可暂时关闭登录失败锁定;正式环境不得开启
# APP_LOGIN_RATE_LIMIT_DISABLED=1
# ---- 同宿主机第二套环境(正式)必须设置的隔离参数 ----
# compose 项目名(默认 deploy;正式环境改为 caiwuzongzhang-prod
# COMPOSE_PROJECT_NAME=caiwuzongzhang-prod
# 应用容器名与宿主机端口映射(正式默认只绑本机回环,由反代对外)
# APP_CONTAINER_NAME=caiwuzongzhang-prod-app
# APP_PORT_MAP=127.0.0.1:4174:4173
# ---- 正式 HTTPS 反代(COMPOSE_PROFILES=tls 启用 proxy 服务)----
# COMPOSE_PROFILES=tls
# PROXY_CONTAINER_NAME=caiwuzongzhang-proxy
# PROXY_PORT_MAP=8443:8443
# 首次启用前先执行 ./tls/gen-cert.sh 生成证书
+49
View File
@@ -46,6 +46,45 @@ docker exec caiwuzongzhang-app python -c "import sqlite3; s=sqlite3.connect('/ap
docker cp caiwuzongzhang-app:/tmp/b.db ../backups/app.db.$STAMP
```
## 3.1 正式环境部署(同宿主机隔离,2026-08-30 首次上线采用)
测试与正式同宿主机并行,靠 compose 项目名/容器名/端口三隔离:
| 项 | 测试环境 | 正式环境 |
|---|---|---|
| 目录 | `/home/leefer/caiwuzongzhang-test` | `/home/leefer/caiwuzongzhang-prod` |
| compose 项目 | `deploy`(默认) | `COMPOSE_PROJECT_NAME=caiwuzongzhang-prod` |
| 应用容器 | `caiwuzongzhang-app` | `caiwuzongzhang-prod-app` |
| 端口 | `4173`(对外) | `127.0.0.1:4174`(仅本机,反代上游) |
| HTTPS | 无 | nginx 反代 `8443``COMPOSE_PROFILES=tls` |
| 访问 | http://192.168.200.36:4173/ | https://192.168.200.36:8443/ |
首次部署顺序:
```bash
git clone <repo> /home/leefer/caiwuzongzhang-prod && cd /home/leefer/caiwuzongzhang-prod
git checkout release/prod
mkdir -p data/files backups && chown -R 10001:10001 data # 容器内 app uid
cd deploy
cp .env.example .env # 按下述差异填写
./tls/gen-cert.sh # 生成内部 CA + 服务器证书(SAN 含对外 IP)
TAG=<发布镜像tag> docker compose up -d # 不带 --build,直接用已验收镜像
docker logs <应用容器> 2>&1 | grep "shown once" # 首启一次性初始口令,只出现一次
```
正式 `.env` 与测试的差异(铁律):
- `COMPOSE_PROJECT_NAME=caiwuzongzhang-prod``APP_CONTAINER_NAME=caiwuzongzhang-prod-app``APP_PORT_MAP=127.0.0.1:4174:4173`
- `COMPOSE_PROFILES=tls``PROXY_CONTAINER_NAME=caiwuzongzhang-proxy``PROXY_PORT_MAP=8443:8443`
- `APP_UID=10001` / `APP_GID=10001`data/ 已 chown 10001
- 管理员用户名自定(如 `jinniu-admin`);**不设** `APP_BOOTSTRAP_ADMIN_PASSWORD`(首启控制台取随机一次性口令,首登立即改密)
- **不设** `APP_LOGIN_RATE_LIMIT_DISABLED`(限流默认开启)
证书与信任分发:
- `tls/certs/ca.crt` 发给各公司电脑安装到「受信任的根证书颁发机构」(安装指引见交接文档);`ca.key`/`server.key` 永不离开宿主机。
- 服务器证书有效期 5 年,到期前用 `./tls/gen-cert.sh` 重签并 `docker compose restart proxy`
## 4. 回滚(应用层,5 分钟内)
```bash
@@ -68,6 +107,16 @@ cd deploy && ./rollback.sh <上一个镜像tag>
cron `*/5 * * * *` 运行 `deploy/monitor.sh`;连续失败或磁盘 ≥80% 时查看 `deploy/ALERT.log` 并按需扩容/清理。容器重启策略 `unless-stopped`
宿主机 crontableefer2026-08-30 起,正式环境):
```cron
15 2 * * * cd /home/leefer/caiwuzongzhang-prod/deploy && CONTAINER=caiwuzongzhang-prod-app ./backup.sh >> backup.log 2>&1
*/5 * * * * cd /home/leefer/caiwuzongzhang-prod/deploy && CONTAINER=caiwuzongzhang-prod-app HEALTH_URL=http://127.0.0.1:4174/ ./monitor.sh >/dev/null 2>&1
01 3 1 * * rsync -a /home/leefer/caiwuzongzhang-prod/backups/monthly/ /vol1/caiwuzongzhang-backups/monthly/
```
(第三行为月度异盘副本:`/vol1` 是与系统盘不同的物理卷;如后续提供真正的异机目标,改为该目标。)
## 7. 升级数据库
应用启动时自动执行迁移(`MIGRATIONS`,当前版本 10)。迁移只前不改写历史;回退 schema 用对应 down 迁移,先备份后操作。
+55 -2
View File
@@ -1,10 +1,16 @@
# 测试/正式环境部署编排(正式口令不得写入本文件)。
# 使用:复制 .env.example 为 .env 填写;TAG 指定镜像标签(默认取当前提交 sha)。
# 例:TAG=v1.0.0-rc1 docker compose -f compose.yaml up -d --build
#
# 同宿主机多套环境(测试 + 正式)必须各自设置:
# COMPOSE_PROJECT_NAME / APP_CONTAINER_NAME / APP_PORT_MAP 互不相同。
# 正式环境启用 HTTPS 反代:.env 中设 COMPOSE_PROFILES=tls(先跑 tls/gen-cert.sh)。
name: ${COMPOSE_PROJECT_NAME:-deploy}
services:
app:
container_name: caiwuzongzhang-app
container_name: ${APP_CONTAINER_NAME:-caiwuzongzhang-app}
image: caiwuzongzhang:${TAG:-latest}
build:
context: ..
@@ -15,7 +21,7 @@ services:
# APP_UID/APP_GID,正式与测试环境各自填写,默认镜像内 app=10001)
user: "${APP_UID:-10001}:${APP_GID:-10001}"
ports:
- "4173:4173"
- "${APP_PORT_MAP:-4173:4173}"
volumes:
- ../data:/app/data
init: true
@@ -30,9 +36,56 @@ services:
pids_limit: 128
mem_limit: 512m
cpus: 1.0
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:4173/', timeout=2)"]
interval: 15s
timeout: 3s
retries: 5
start_period: 10s
# 可选 HTTPS 反向代理(正式环境用):COMPOSE_PROFILES=tls 启用。
# 证书由 tls/gen-cert.sh 生成的内部 CA 签发(内网自受管信任),公司电脑
# 安装 tls/certs/ca.crt 一次即可无告警访问。
proxy:
container_name: ${PROXY_CONTAINER_NAME:-caiwuzongzhang-proxy}
image: nginx:1.27-alpine
profiles: ["tls"]
ports:
- "${PROXY_PORT_MAP:-8443:8443}"
volumes:
- ./tls/nginx.conf:/etc/nginx/conf.d/default.conf:ro
- ./tls/certs/server.crt:/etc/nginx/tls/server.crt:ro
- ./tls/certs/server.key:/etc/nginx/tls/server.key:ro
# 与证书文件属主一致的 uid/gid(默认 101=镜像 nginx 用户;宿主机生成证书时
# 设为属主 uid/gid,保持 server.key 0600 不放宽)
user: "${PROXY_UID:-101}:${PROXY_GID:-101}"
depends_on:
- app
restart: unless-stopped
read_only: true
tmpfs:
- /var/cache/nginx:size=16m,uid=${PROXY_UID:-101},gid=${PROXY_GID:-101},mode=700
- /var/run:size=1m,uid=${PROXY_UID:-101},gid=${PROXY_GID:-101},mode=700
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
pids_limit: 64
mem_limit: 64m
cpus: 0.5
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
healthcheck:
test: ["CMD", "wget", "-q", "--no-check-certificate", "-O", "/dev/null", "https://127.0.0.1:8443/"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
+45
View File
@@ -0,0 +1,45 @@
#!/usr/bin/env bash
# 生成内网专用 CA 与服务器证书(自受管内部信任,不涉及公网域名)。
# 用法:在正式环境 deploy/ 目录执行 ./tls/gen-cert.sh
# 可用环境变量覆盖:
# CERT_IP 对外访问 IP(默认 192.168.200.36
# CERT_DNS 额外 DNS 名称(空格分隔,默认 caiwuzongzhang.jinniu.internal
# 输出:tls/certs/{ca.crt,ca.key,server.crt,server.key}key 永不入库、不分发)
# 分发:仅把 ca.crt 发给公司电脑安装(受信任的根证书颁发机构)。
set -euo pipefail
ROOT="$(cd "$(dirname "$0")" && pwd)"
CERTS="$ROOT/certs"
mkdir -p "$CERTS"
cd "$CERTS"
CERT_IP="${CERT_IP:-192.168.200.36}"
CERT_DNS="${CERT_DNS:-caiwuzongzhang.jinniu.internal}"
SAN="IP:${CERT_IP}"
for d in $CERT_DNS; do SAN="$SAN,DNS:$d"; done
if [[ ! -f ca.key ]]; then
# 内部根 CA:10 年,仅本系统使用
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 \
-keyout ca.key -out ca.crt -days 3650 -nodes -subj "/CN=Jinniu Ledger Internal CA" \
-addext "basicConstraints=critical,CA:TRUE" \
-addext "keyUsage=critical,keyCertSign,cRLSign"
chmod 600 ca.key
echo "ca 已生成"
fi
openssl req -newkey ec -pkeyopt ec_paramgen_curve:P-256 \
-keyout server.key -out server.csr -nodes \
-subj "/CN=${CERT_DNS%% *}" >/dev/null 2>&1
cat > server.ext <<EXT
subjectAltName=${SAN}
basicConstraints=CA:FALSE
keyUsage=digitalSignature,keyEncipherment
extendedKeyUsage=serverAuth
EXT
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
-out server.crt -days 1825 -sha256 -extfile server.ext >/dev/null 2>&1
chmod 600 server.key
rm -f server.csr server.ext ca.srl
echo "server 证书已生成(SAN: ${SAN}),有效期 5 年"
echo "分发文件:${CERTS}/ca.crt(其余文件不得离开本机)"
+31
View File
@@ -0,0 +1,31 @@
# 正式环境 HTTPS 反代(nginx)。证书挂载自 deploy/tls/certs/gen-cert.sh 生成)。
# 上游为同 compose 网络内的 app:4173;对外仅暴露本代理的 8443。
server {
listen 8443 ssl;
listen [::]:8443 ssl;
http2 on;
server_name _;
ssl_certificate /etc/nginx/tls/server.crt;
ssl_certificate_key /etc/nginx/tls/server.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:2m;
ssl_session_timeout 1h;
# 银行流水 Excel 上传上限
client_max_body_size 25m;
proxy_http_version 1.1;
proxy_read_timeout 120s;
proxy_send_timeout 120s;
location / {
proxy_pass http://app:4173;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
}
}