# s-file 客户侧离线部署(两台服务器:内网机 + 外网机) > 本目录是**交付用**部署;本机联调用的两套栈同起在一台机器上,见 [../部署说明.md](../部署说明.md)。 > 客户现场没有网络、没有光闸模拟目录,一切靠 `docker load` 的镜像 tar + 真实光闸摆渡目录。 --- ## 1. 拓扑 ``` 内网(客户办公网) 外网(外部报送网) ┌────────────────────────┐ ┌────────────────────────┐ │ 内网服务器 A │ │ 外网服务器 B │ │ ┌──────────────────┐ │ 光闸/单向 │ ┌──────────────────┐ │ │ │ sfile-iw-web :80 │◄─┼── 摆渡设备 ──►┼─ │ sfile-ow-web :80 │ │ │ │ sfile-iw-app :8080│ │ (不开放网络) │ │ sfile-ow-app │ │ │ │ pg/redis/mq(内部) │ │ │ │ pg/redis/mq(内部)│ │ └──────────────────┘ │ │ └──────────────────┘ │ │ /data/sfile/gate/out │─── 发 ────► │ /data/sfile/gate/in │ │ /data/sfile/gate/in │◄─── 收 ──── │ /data/sfile/gate/out │ └───────────┬────────────┘ └───────────┬────────────┘ │ http://host.docker.internal: ┌──────┴───────────┐ │ MeshSafe 文件安全检测 │ 同机容器 / 独立服务器 / 集群均可,只要求 内网机能 HTTP 访问到 └──────────────────┘ ``` - 两台服务器之间**不开任何 TCP 端口**,文件只经光闸在 `gate/out` ⇄ 对端 `gate/in` 之间单向搬运。 - 每台机器一个独立 compose 栈,同一份 `docker-compose.yml`,差别全在 `.env`。 - 摆渡目录由光闸设备挂载/同步进宿主机,应用只认容器内固定路径 `/data/transfer/send`、`/data/transfer/receive`。 --- ## 2. 交付包内容 本目录本身**就是**交付包,`.env`、`sql`、`images/*.tar` 都已就位,整目录拷走即可装机(不需要现场再回仓库取任何东西)。 ``` deploy/offline/ ├── docker-compose.yml 一台机器的 5 个服务(web/app/pg/redis/mq),现场不改 ├── .env.iw 内网机出厂配置(已填好,含口令)← install.sh iw 会拷成 .env ├── .env.ow 外网机出厂配置(同上,口令与内网机不同) ├── .env.example 空白模板,只在客户环境差异大到出厂值不适用时才用 ├── VERSION.txt 本次出盘的版本/平台/时间/镜像清单 ├── config/application.yml 静态默认配置,里面不放任何口令 ├── nginx/default.conf.template 前端反代(内/外网共用,靠 SFILE_SITE_ROOT 选站点) ├── sql/01-init-table.sql 全量建表(92K,含 x_file_security) ├── sql/02-init-data.sql 初始数据(2.8K,admin、机构、平台编码) ├── images/*.tar 5 个镜像,共 650M(docker save 原始大小) ├── images/SHA256SUMS.txt 交付盘校验值,install.sh 开机先比对它 └── scripts/ ├── install.sh 现场一键安装(要带 iw / ow 参数) ├── smoke_test.py 现场自动化验收(18 条断言) └── export-images.sh 我方出盘脚本,现场用不到 ``` > 仓库里 `images/*.tar`、`SHA256SUMS.txt`、`dist/` 已被 `.gitignore` 排除——镜像 tar 不进气仓,只走交付盘。 镜像清单与已验证版本(tag 必须与 tar 里的完全一致,`install.sh` 会核对 sha256、架构与 tag 三者): | 镜像 | 交付 tag | tar 大小 | 本机验证版本 | 说明 | |---|---|---|---|---| | `sfile-app` | `1.0.0` | 313M | JDK8 运行时 eclipse-temurin:8-jre-jammy | 内含 s-file.jar | | `sfile-web` | `1.0.0` | 39M | nginx:alpine | **内网包与外网包都在同一个镜像里** | | `postgres` | `18` | 161M | 18.3 | | | `redis` | `8` | 53M | 8.6.3 | | | `rabbitmq` | `3.13-management-alpine` | 84M | 3.13.7 | | --- ## 3. 现场要先定的 6 件事 | 项 | 变量 | 建议 | |---|---|---| | 服务器 CPU 架构 | — | `uname -m`:x86_64→amd64,aarch64(鲲鹏/飞腾)→arm64。**出盘前必须确认**,架构不符装不上 | | 前端对外端口 | `WEB_PORT` | 出厂值 80;被占用改 8080/9080,同步改客户访问地址 | | 数据盘根目录 | `SFILE_DATA_ROOT` | 独立挂载点,出厂值 `/data/sfile`。上传文件+库+日志都在这,按 500MB/文件上限估:`文件平均大小 × 日均份数 × 保留天数`,一般 ≥200G | | 光闸摆渡目录 | `GATE_SEND_DIR` / `GATE_RECV_DIR` | 与光闸厂商确认挂载点,**发出/收到是两个不同目录**,两侧机器各自配自己的 | | MeshSafe 地址与密钥 | `MESHSAFE_BASE_URL` / `MESHSAFE_SECRET` | 同机:`http://host.docker.internal:44380`;独立机:`http://:<端口>`;`MESHSAFE_SECRET` 出厂是占位值,**接了检测就必须按 MeshSafe 交付单改**,没接就 `MESHSAFE_ENABLED=false` | | 各类口令 | `POSTGRES_PASSWORD` 等 | 出厂已给随机值可直接跑,正式交付按客户安全要求轮换(见 §5.1) | 机器规格建议:8 核 / 16G 起(应用堆 `-Xmx` 3G,PG + MQ + Redis 还要约 2-3G,留足页缓存),系统盘 100G + 数据盘独立。 --- ## 4. 出盘(我方,需要联网一次) ```bash # 前置:backend/target/s-file.jar 与 frontend/ 已构建 cd deploy/offline ./scripts/export-images.sh 1.0.0 # amd64 TARGET_PLATFORM=linux/arm64 ./scripts/export-images.sh 1.0.0 # 鲲鹏/飞腾 ``` 脚本会把 `images/*.tar`、`sql/*.sql`、`.env.iw/.env.ow` 的镜像 tag、`SHA256SUMS.txt` 就地刷新, 再打一份 `dist/sfile-offline-<版本>-<平台>.tar.gz` 用于拷盘/刻录。带盘就带这个 gz。 --- ## 5. 装机(客户侧,两台机器各执行一次) ```bash # 1) 解包(两台机器用同一个盘,解出来的目录一模一样) tar -xzf sfile-offline-1.0.0-linux-amd64.tar.gz -C /opt/ cd /opt/sfile-offline-1.0.0-linux-amd64 # 2) 只改必须改的:MeshSafe 密钥(不接检测就关掉开关) vi .env.iw # 内网机;外网机改 .env.ow # 3) 一键装:按角色建 .env -> 自检 -> 校验 tar -> docker load -> 建目录 -> 起栈 -> 等健康 ./scripts/install.sh iw # 内网机 ./scripts/install.sh ow # 外网机 ``` `install.sh` 第一次执行会把 `.env.iw`/`.env.ow` 拷成 `.env`,之后只读 `.env`——现场改动不会被覆盖。 想回到出厂值:`rm .env && ./scripts/install.sh iw`。 参数与已有 `.env` 的角色不一致时直接拒绝,防止在内网机上起了外网栈。 装完会打印访问地址。首次登录后**立刻改掉 admin 的初始口令**。 ### 5.1 出厂口令(交付单要抄一份,并建议现场轮换) | 变量 | 内网机出厂值 | 外网机出厂值 | 现场是否必须改 | |---|---|---|---| | `POSTGRES_PASSWORD` | `Sfile-iw-Pg-N1tOz1MxgdaS` | `Sfile-ow-Pg-pT-673--i1rk` | 建议(只在本机容器网络内生效,不对网暴露) | | `MQ_PASSWORD` | `Sfile-iw-Mq-UTqVSnCDZ0LB` | `Sfile-ow-Mq-sfW5Cv-Nk-cA` | 建议(同上) | | `JWT_SECRET` | `86a6b015…7d70`(64 位) | `9eeeea03…6988`(64 位) | 必须保密;泄露等于能伪造登录态,两侧已不同 | | `USER_DEFAULT_PASSWORD` | `Sfile-iw-Usr-FBLS-0fA` | `Sfile-ow-Usr-WqDJ7PDS` | **必须改**:新建用户的初始密码,业务员要知道 | | admin 登录口令 | 由 `backend/db/init-data.sql` 灌入 | 同左 | **必须改**:装完第一次登录就改 | | `MESHSAFE_SECRET` | `CHANGE_ME_meshsafe_secret` | `CHANGE_ME_meshsafe_secret` | **故意留空**:以 MeshSafe 侧 `MESHSAFE_API_CLIENTS` 白名单为准,装机前替换 | > `install.sh` 只对**启用了 MeshSafe 且 secret 还是占位值**的情况卡住;`MESHSAFE_ENABLED=false` 时其余占位值不拦,方便先装后接检测。 ### 5.2 内网机 / 外网机配置差异 `.env.iw` 与 `.env.ow` 已按下表分别填好,这里列出来只为让客户网络/安全评审能对上: | 变量 | 内网机 | 外网机 | |---|---|---| | `SFILE_ROLE` | `iw` | `ow` | | `SFILE_TRANSFER_LOCATION` | `1` | `2` | | `SFILE_SITE_ROOT` | `/usr/share/nginx/inside` | `/usr/share/nginx/outside` | | `GATE_SEND_DIR` | `/data/sfile/gate/out`(光闸的**内→外**发出目录) | `/data/sfile/gate/out`(光闸的**外→内**发出目录) | | `GATE_RECV_DIR` | `/data/sfile/gate/in`(光闸的**外→内**投递目录) | `/data/sfile/gate/in`(光闸的**内→外**投递目录) | | `MESHSAFE_CHANNEL` / `MESHSAFE_CLIENT_ID` | `sfile-iw` | `sfile-ow` | | `JWT_SECRET` / `POSTGRES_PASSWORD` / `MQ_PASSWORD` | 一套 | 另一套(已确认不同) | | `WEB_PORT` | 80 | 80(两台机器各占自己的 80,不冲突) | | `APP_SUBNET` / `APP_NODE_IP` / `APP_WORK_ID` | `172.30.88.0/24` / `.10` / `1` | `172.30.98.0/24` / `.10` / `2` | > 两侧网段/机器号故意错开:客户如果先把两台合并成一台机器做演示,不改也能同起。 > `SFILE_TRANSFER_LOCATION` 写反是本方案最容易踩的坑:值对调后,两侧都以为自己是发送端,摆渡目录里的包永远不会被解包。 --- ## 6. 验收清单(现场按这个走一遍) | # | 动作 | 期望 | |---|---|---| | 1 | 浏览器开 `http://<内网机IP>:<端口>` | 左上角有「内网」徽标,能登录 | | 2 | 开 `http://<外网机IP>:<端口>` | 无「内网」徽标(设计上只有内网侧显示标识),能登录 | | 3 | 内网上传一个正常文件 | 「安全检测」列几秒内从**检测中**变**检测通过** | | 4 | 内网上传 EICAR 测试串 | 变**检测不通过**,理由含 `Eicar-Test-Signature` 或 `内嵌 EICAR` | | 5 | 对该文件点分享/传输 | 被拒:`安全检测未通过,禁止分享与传输` | | 6 | 内网把通过的文件「传输」到外网 | 内网 `GATE_SEND_DIR` 出现 `_.zip` | | 7 | 等 1 分钟内网监听任务 | 外网「内穿外」目录出现 `<原名>_<时间戳>` 文件夹,文件在内 | | 8 | 外网该文件的安全检测 | 自动送检并变**检测通过** | | 9 | 内网对不通过文件做人工复核放行 | 状态变**检测通过**、标记**人工复核**,之后可传输 | | 10 | `docker compose ps` 两台机各 5 个 healthy | 无 `Restarting`/`unhealthy` | 第 3-9 步可用 `scripts/smoke_test.py` 自动跑(在有 python3 + requests 的机器上,指向客户 IP),现场没条件就手工点。 ```bash SFILE_IW_BASE=http://<内网机IP>:<端口>/api \ SFILE_OW_BASE=http://<外网机IP>:<端口>/api \ SFILE_ADMIN_USER=admin SFILE_ADMIN_PWD='<现场初始口令>' \ python3 scripts/smoke_test.py ``` --- ## 7. 运维 ```bash docker compose ps # 状态 docker logs -f sfile-iw-app # 后端日志(含送检/摆渡全过程) docker compose up -d app # 改完 .env 或 config 后重启后端 docker compose down # 停止(不动数据) docker compose down -v --remove-orphans # ⚠ 数据在 bind 目录里,不会丢;但别顺手执行 ``` **备份只需三样**:`$SFILE_DATA_ROOT/pgdata`(库)、`$SFILE_DATA_ROOT/upload`(文件本体)、`.env`(口令)。 恢复:换机器后原路径放回 + `docker load` + `install.sh`。 数据库升级:新版本的增量 SQL 放 `backend/db/upgrade-*.sql`,人工执行 ```bash docker exec -i sfile-iw-pg psql -U sfile -d s_file < upgrade-20261101-meshsafe-file-security.sql ``` (`sql/01/02` 只在**空库首次启动**执行,已有数据卷会被忽略。) --- ## 8. 已知坑与取舍 1. **占位值检查是有意的,别图省事先注释掉**:`install.sh` 只在「`MESHSAFE_ENABLED=true` 且 `MESHSAFE_SECRET` 还是占位值」时拒绝启动,其余 `CHANGE_ME` 也会拦。没接检测就老实写 `MESHSAFE_ENABLED=false`。 2. **交付盘必须整目录拷**:`install.sh` 会核 5 个 tar 的 sha256、个数、以及 `.env` 里 5 个镜像 tag 是否都能 load 到。只拷脚本不拷 `images/`(tar 不进气仓,见 `.gitignore`)是最常见的现场翻车。 3. **架构不符装不上**:`sfile-app`/`sfile-web` 是在构建机上 `docker build` 出来的,信创 ARM 机必须用 `TARGET_PLATFORM=linux/arm64` 重新出盘(`install.sh` 会拦你)。 4. **PostgreSQL 18 的数据目录挂载点是 `/var/lib/postgresql`**(15 及以前是 `/var/lib/postgresql/data`)。自己换库版本时改 `.env` 的 `PG_DATA_DIR`,否则容器会报「检测到未挂载的数据」并反复重启。 5. **nginx 反代必须透传带端口的 Host**(模板里已用 `$http_host`)。改成 `$host` 会让后端 `RequestUtils.getRootDomain` 对无端口地址抛 `IllegalStateException`,症状就是**登录 500**。 6. **发号器 workerId 必须能取到,否则应用起不来。** 后端按 `code.snowflake.workid.` 取机器号,取不到时回退默认值 `99`,而 99 超出雪花 5 位上限,启动即 `IllegalArgumentException: worker Id can't be greater than 31`。本方案的做法是把 app 的容器 IP 锁死(`APP_NODE_IP`)并由 compose 自动写进 JVM 参数,`.env` 里 `APP_SUBNET / APP_GATEWAY / APP_NODE_IP` 三个值必须同网段。**只想改代码解决的话**,把 `CodeConfiguration#snowFlakeCodeGenerator` 的兜底 `99L` 改成一个合法值即可解除这层 IP 耦合——但那样同侧多实例会撞 ID,需自行权衡。 7. **MeshSafe 凭证**:`MESHSAFE_CLIENT_ID`/`SECRET` 必须在 MeshSafe 侧 `MESHSAFE_API_CLIENTS` 白名单里存在,否则送检全部 401。`MESHSAFE_FAIL_OPEN=true` 时检测服务挂了不阻断业务(只记失败),等保从严请置 `false`。 8. **`host.docker.internal` 需要 Docker ≥ 20.10**;MeshSafe 在独立服务器时不需要它,可把 compose 里那两行 `extra_hosts` 删掉。 9. **PDF 盖章依赖 spire.pdf 是桩实现**:本机因拿不到商业包做了替代 jar,运行时调用会抛 `UnsupportedOperationException`。交付前若客户要电子签章功能,必须换回 `e-iceblue:spire.pdf:3.7.4` 正式包重新打 jar。 10. **摆渡延迟固定为监听周期**(默认 1 分钟一次)。客户嫌慢就调 `FILE_TRANSFER_MONITOR_CRON`,不要改成秒级——它会整目录扫描。 11. **两侧数据库完全独立**,同一份 `init-data.sql` 灌出来 admin 初始口令一致,交付时两侧都要改。 12. **装完才发现角色装反了**(内网机上跑了 `install.sh ow`):`.env` 与参数角色不一致时 `install.sh` 直接拒绝,所以真发生一定是 `rm .env` 之后重装。compose 项目名跟着角色走,旧项目还在占用 `APP_SUBNET`,会报 `invalid pool request: Pool overlaps with other one on this address space`。先把旧项目拆掉再起: `docker compose -p sfile-<旧角色> down` → `rm .env` → `./scripts/install.sh <正确角色>`。 13. **检测结论按文件内容复用**:MeshSafe 的 `fileCode` 是内容寻址,同一份字节重复上传会直接沿用上次结论,不会重新扫。这是性能上的设计,但等保评审可能会问「改判是否会被复用绕过」,现场演示验收时请用**每次内容都不同**的测试文件。