主题
10 — 部署规范
立规时间:2026-11-08 核心原则(用户明确要求):软件一律用 Docker 运行,除非有"必须裸装"的理由。
一、核心原则
默认:容器化
所有业务软件、中间件、数据库、工具链,默认以容器方式运行:
- 通过
docker compose声明式管理 - 用官方镜像,不自行编译
- 状态数据用 named volume 或 bind mount 持久化
容器化的收益(为什么这么定)
| 收益 | 说明 |
|---|---|
| 环境隔离 | 各软件依赖互不污染,系统只用 Ubuntu 基础包 |
| 版本可控 | 换版本 = 改 tag + 重建,不担心残留 |
| 可回滚 | 镜像 tag 固定即版本固定,出问题秒回上一版 |
| 清理彻底 | docker compose down -v 即可完全清除 |
| 可迁移 | compose 文件 + volume 备份即可搬到新机 |
| 宿主机干净 | 不污染 apt、不用担心 apt autoremove 误删依赖 |
裸装例外(必须满足以下之一)
| 例外条件 | 示例 |
|---|---|
| 必须在网络栈就绪前运行 | ufw(防火墙)、cloud-init |
| 需要直接操作宿主机内核/网络 | 容器运行时本体 docker-ce、containerd |
| 需要在容器外启用才有效 | fail2ban(封禁宿主 iptables) |
| 作为容器运行时的前置依赖 | shadowsocks-libev / privoxy(为 dockerd 拉镜像提供出口) |
| 官方明确不支持容器化 | — |
二、当前裸装清单(及理由)
| 软件 | 裸装理由 | 判断 |
|---|---|---|
docker-ce / containerd | 容器运行时本体,无法自举 | ✅ 合理 |
ufw | 防火墙需内核层生效 | ✅ 合理 |
fail2ban | 需写宿主 iptables 封禁 | ✅ 合理 |
shadowsocks-libev + privoxy | 为 docker pull 提供出口;装进容器会形成先有鸡还是先有蛋 | ✅ 合理 |
cloud-init / systemd / 内核等 | 系统基础组件 | ✅ 合理 |
python3 / git / curl | 系统自带或运维工具链 | ✅ 可接受 |
除以上之外,不应再有裸装软件。
三、容器化清单
| 服务 | 镜像 | 目录 | 端口 | 状态 |
|---|---|---|---|---|
| Caddy 2.11.9 | caddy:2.11.9-alpine | /opt/stacks/caddy/ | 127.0.0.1:80 | ✅ 运行中(Web 入口) |
| Docs site | tenodx/docs:1.0.0(自建,VitePress→Caddy) | /opt/stacks/docs/ | 127.0.0.1:8088 | ✅ 运行中(见 14-文档站.md) |
| Firmware Registry | tenodx/fw-registry:1.0.0(自建,FastAPI) | /opt/stacks/fw-registry/ | 127.0.0.1:8000 | ✅ 运行中(见 13-资源服务器.md) |
| PostgreSQL 18 | postgres:18-alpine | /opt/stacks/postgres/ | 127.0.0.1:5432 | ✅ 运行中 |
| Uptime Kuma 2.5.9 | louislam/uptime-kuma:2 | /opt/stacks/uptime-kuma/ | 127.0.0.1:3001 | ✅ 运行中 |
| NapCat(OneBot 11) | mlikiowa/napcat-docker:latest | /opt/stacks/napcat/ | 127.0.0.1:6099/3000/3002 | ✅ 运行中(待 QQ 登录) |
NapCat 是唯一需要
privileged: true的容器 —— 镜像的容器环境伪装逻辑要求 CAP_SYS_ADMIN。这是已知高权限风险,评估见11-应用服务.md第 2 节。Caddy 是 Web 入口,配置见
12-Web入口.md。新增 Web 服务应挂到 Caddy 后面, 而不是再开一个公网端口。自建镜像的源码位置:
server/fw-registry/、docs-site/。 重建方式均为docker compose up -d --build(构建上下文就是该目录)。
公网暴露面(原则)
当前只有 22/tcp 对公网开放。 新增服务一律 127.0.0.1 绑定, 需要 Web 访问就挂到 Caddy(12-Web入口.md 第 6 节)。 任何时候都不应出现第二个 0.0.0.0 监听端口,除非明确决定并记录理由。
四、部署约定(新增服务时照此执行)
1. 目录结构
/opt/stacks/<service-name>/
├── compose.yaml # 必须(不用 docker run)
├── .env # 密码等敏感变量,权限 600
└── backup.sh # 有状态服务必须提供备份脚本2. compose.yaml 必备项
yaml
services:
xxx:
image: <官方镜像>:<明确版本> # 不用 latest,锁版本便于回滚
container_name: tenodx-xxx
restart: unless-stopped # 开机自启 + 崩溃重启
environment: # 敏感值走 .env,不写进 yaml
- FOO=${FOO}
ports:
- "127.0.0.1:<port>:<port>" # 内部服务只绑 loopback!
volumes:
- data:/path/in/container # 状态数据必须持久化
healthcheck: # 必须有,否则无法判断就绪
test: [...]
logging: # 必须有,否则日志撑爆磁盘
driver: json-file
options: { max-size: "50m", max-file: "3" }
networks:
- tenodx-net
volumes:
data:
name: tenodx-<service>-data
networks:
tenodx-net:
name: tenodx-net
external: false # 首次由本栈创建,之后复用3. 端口暴露规则(重要)
⚠️ Docker 发布端口会绕过 ufw。
-p 8080:80等于向公网开放 8080,即使 ufw 没放行。
| 服务类型 | 端口写法 |
|---|---|
| 数据库、缓存、内部 API | 127.0.0.1:PORT:PORT —— 只允许宿主机访问(SSH 隧道运维) |
| 需要被其他容器访问 | 不发布端口,通过 tenodx-net 网络用服务名连接 |
| 需要对公网提供 Web | 由 nginx 容器统一接 80/443,其余全部 loopback |
4. 网络约定
所有服务加入同一个 tenodx-net 桥接网络,容器间用服务名互相访问,不走宿主机端口。
5. 版本策略
- 使用明确版本 tag(
postgres:18-alpine),不用:latest - 升级流程:改 tag →
docker compose up -d→ 验证 → 失败则改回 tag 重建
6. 日志
- 依赖
daemon.json的全局日志轮转(50 MB × 3) - 每个 compose 服务再显式声明一次,双保险
7. 备份
有状态服务必须提供 backup.sh,并挂 /etc/cron.d/ 定时执行:
- 逻辑备份优先(数据库用
pg_dump/mysqldump),不要直接文件拷贝数据目录 - 保留 7 天,备份文件权限 600
- 必须验证备份完整性(
gzip -t+ 非空)
8. 监控(强制)
用户要求:"uptime 应该及时追加新服务的检测,所有服务都应该被监测,加入定期维护。"
任何新增服务都必须在同一次变更里加上 Uptime Kuma 监控,否则视为变更未完成。
bash
# 1) 在 /opt/stacks/uptime-kuma/monitors.json 追加一条
{ "name": "服务名", "type": "http", "url": "http://tenodx-xxx:8080/health", "interval": 60 }
# 2) 同步(幂等)
/opt/stacks/uptime-kuma/sync.sh按服务位置选择监控类型:
| 服务位置 | 类型 | 地址写法 |
|---|---|---|
同 tenodx-net 的容器 | http / postgres / 原生 | 用容器名:tenodx-xxx:端口 |
监听 0.0.0.0 的宿主服务 | port / http | host.docker.internal:端口 |
只绑 127.0.0.1 的宿主服务 | push | 加到 host-checks.sh,由宿主 cron 推 |
四个必知陷阱(详见 11-应用服务.md 第 5 节):
- TCP 检查的类型名是
port,写tcp会报Unknown Monitor Type - 容器里的
127.0.0.1指容器自己,不是宿主机 docker-proxy会先接受 TCP 握手,对发布端口用port类型会假阳性 UP- push 的
msg不能含空格(未编码 URL 会让推送静默失败)
五、常用运维命令
bash
cd /opt/stacks/<service>
docker compose ps # 状态
docker compose logs -f --tail 50 # 日志
docker compose restart # 重启
docker compose up -d # 应用变更
docker compose down # 停止并删除容器(保留卷)
docker compose down -v # ⚠️ 连数据卷一起删除
docker compose config # 校验 yaml备份与恢复(PostgreSQL 示例)
bash
# 手动备份
/opt/stacks/postgres/backup.sh
# 恢复
gunzip -c /var/backups/postgres/tenodx-YYYYMMDD-HHMMSS.sql.gz \
| docker exec -i tenodx-postgres psql -U tenodx -d tenodx进入数据库
bash
# 容器内(最简单)
docker exec -it tenodx-postgres psql -U tenodx -d tenodx
# 或从本机经 SSH 隧道(端口只绑了 loopback,必须走隧道)
ssh -L 5432:127.0.0.1:5432 tenodx
# 然后本机: psql -h 127.0.0.1 -U tenodx -d tenodx六、待办 / 后续规划
| 项 | 说明 |
|---|---|
docker compose 版 nginx | 部署 Web 时用容器 nginx,接 80/443 |
| 应用容器 | 待技术栈确认(Node / Java / Python) |
| 日志集中 | 可选:接入 loki 或 journald driver |
| 监控 | 可选:node-exporter + cadvisor |
| ✅ 已按本规范容器化部署 |