chore(deploy): 成品部署链路、依赖分组与运维基建

部署方式:服务器不再接收源码、不执行构建,改为本地构建成品后交付
- deploy-artifacts.sh:构建→备份→校验→原子替换,支持回滚、镜像/备份轮转、
  mysql 与 postgres 双变体镜像;后端以 docker save|ssh|docker load 交付
- 依赖分组:requirements/{base,storage}.txt + db/{mysql,postgres,sqlite}.txt,
  镜像按所选数据库只装对应驱动(MySQL 变体 565MB → 521MB)

编排与运行时加固
- compose:MySQL/Redis/backend 端口仅绑 127.0.0.1;密钥(SECRET_KEY、
  DATA_ENCRYPTION_KEY、DATA_ENCRYPTION_OLD_KEYS)、PROD_CORS_ORIGINS、
  FORWARDED_ALLOW_IPS、DEMO_ENABLE 一律由 docker/.env 注入
- Dockerfile:对齐依赖分组、以非 root(uid=1000) 运行;.dockerignore 排除全部真实 .env.*
- nginx:静态资源长缓存、隐藏文件拒绝、接口文档仅内网可访问、放行 ACME http-01 校验路径

工程基建
- 新增 CI:backend(uv sync + ruff + pytest)与 frontend(web type-check+vitest、app type-check)
- 新增 DEPLOYMENT.md 运维手册(部署/回滚/密钥轮换/每日巡检/证书/排查)
This commit is contained in:
zhangtao
2026-10-04 04:26:03 +08:00
parent 0cf2e24b92
commit ce9401196a
20 changed files with 851 additions and 91 deletions
+28 -12
View File
@@ -1,28 +1,44 @@
# Docker 部署环境变量(案例)
# ============================================================================
# Docker 部署环境变量示例
# 用法:cp docker/.env.example docker/.env 然后改成真实值(.env 不会被提交)
# 注意:本文件会被提交到仓库,只放占位符,不要写真实口令
# ============================================================================
# 数据库配置
MYSQL_ROOT_PASSWORD=your_mysql_root_password
# ---------- 数据库(MySQL)----------
MYSQL_ROOT_PASSWORD=change_me_root_password
MYSQL_DATABASE=fastapiadmin
MYSQL_USER=fastapiadmin
MYSQL_PASSWORD=your_mysql_password
MYSQL_PASSWORD=change_me_app_password
MYSQL_PORT=3306
# 说明:compose 把 MySQL 端口绑定在 127.0.0.1(只对本机开放),
# 容器之间通过 app_network 互联,不依赖宿主机端口。
# Redis配置
REDIS_PASSWORD=your_redis_password
# ---------- Redis ----------
REDIS_PASSWORD=change_me_redis_password
REDIS_PORT=6379
# 后端配置
# ---------- 后端 ----------
BACKEND_PORT=8001
# 使用的后端镜像 tag:
# 3.0.0 表示「当前线上版本」(deploy-artifacts.sh 部署时写入)
# mysql-<时间戳> MySQL 驱动变体
# postgres-<时间戳> PostgreSQL 驱动变体(切换数据库时手动指定)
BACKEND_IMAGE_TAG=3.0.0
# 日志级别(DEBUG/INFO/WARNING/ERROR)
LOGGER_LEVEL=INFO
# 运行环境,对应 backend/env/.env.{dev,prod}
DEPLOY_ENV=prod
# 数据库类型(仅当使用外部数据库、不靠 compose 注入时才有意义)
# DATABASE_TYPE=mysql
# Nginx配置
# ---------- Nginx ----------
HTTP_PORT=80
HTTPS_PORT=443
# nginx 镜像版本(由 docker-compose.override.yaml 读取;留空用 1.27-alpine)
NGINX_TAG=1.27-alpine
# 项目配置
# ---------- 项目 ----------
PROJECT_NAME=FastapiAdmin
# 部署环境
DEPLOY_ENV=prod
# ---------- 部署方式 ----------
# 健康检查宽限期(秒):容器首次启动需要跑迁移,慢机器上可调大
HEALTHCHECK_START_PERIOD=40s
+33 -13
View File
@@ -1,26 +1,46 @@
# 使用 python:3.12-slim 镜像作为基础镜像
# ============================================================================
# FastapiAdmin 后端镜像(源码构建路径)
#
# 构建参数 DB 决定安装哪套数据库驱动:
# docker build --build-arg DB=mysql -f docker/backend/Dockerfile . # 默认
# docker build --build-arg DB=postgres -f docker/backend/Dockerfile .
#
# 注意:正式部署建议用 deploy-artifacts.sh(本地构建镜像 → docker save/load),
# 这样服务器上不需要源码,也不需要构建工具链。
# ============================================================================
FROM python:3.12-slim
# 使用 LABEL 替代 MAINTAINER
LABEL maintainer="948080782@qq.com"
# 设置时区
ENV TZ Asia/Shanghai
ARG DB=mysql
ENV TZ=Asia/Shanghai \
PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
LANG=C.UTF-8 \
DB=${DB}
# 设置容器内工作目录
WORKDIR /home
# 先复制依赖清单并安装(利用 layer 缓存:代码变更时不重装依赖)
COPY ./backend/requirements.txt .
# 依赖清单先复制(layer 缓存:只改代码时不重装依赖)
COPY ./backend/requirements/ /home/requirements/
# 安装依赖
RUN pip install --no-cache-dir -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
# 只装所选数据库的驱动 + 存储与加密依赖
RUN pip install --no-cache-dir \
-r /home/requirements/base.txt \
-r /home/requirements/db/${DB}.txt \
-r /home/requirements/storage.txt \
-i https://pypi.tuna.tsinghua.edu.cn/simple
# 复制应用代码(backend/.venv、logs、缓存等由根 .dockerignore 排除)
COPY ./backend/ .
# 应用代码(backend/.venv、logs、缓存、真实 .env.* 由根 .dockerignore 排除)
COPY ./backend/ /home/
# 以非 root 用户运行;运行时需要写的目录预先建好并交给该用户
RUN useradd --uid 1000 --create-home --shell /usr/sbin/nologin app \
&& mkdir -p /home/logs /home/static/upload /home/alembic/versions \
&& chown -R app:app /home
USER app
# 暴露端口
EXPOSE 8001
# 运行应用
CMD ["python", "main.py", "run", "--env=prod"]
+7
View File
@@ -0,0 +1,7 @@
# 优化项:静态资源缓存 + nginx 版本固定。撤销:删除本文件并重建 nginx。
services:
nginx:
image: nginx:${NGINX_TAG:-1.27-alpine}
volumes:
- ./nginx/cache-headers.inc:/etc/nginx/cache-headers.inc:ro
- ./nginx/acme:/usr/share/nginx/html/acme:ro
+17 -5
View File
@@ -20,7 +20,7 @@ services:
MYSQL_USER: "${MYSQL_USER:-fastapiadmin}"
MYSQL_PASSWORD: "${MYSQL_PASSWORD:?错误: 请设置 MYSQL_PASSWORD 环境变量}"
ports:
- "${MYSQL_PORT:-3306}:3306"
- "127.0.0.1:${MYSQL_PORT:-3306}:3306"
volumes:
- mysql_data:/var/lib/mysql
- /etc/localtime:/etc/localtime:ro
@@ -56,7 +56,7 @@ services:
environment:
TZ: "Asia/Shanghai"
ports:
- "${REDIS_PORT:-6379}:6379"
- "127.0.0.1:${REDIS_PORT:-6379}:6379"
volumes:
- redis_data:/data
# 挂载自定义 redis.conf(若存在则覆盖默认配置)
@@ -115,10 +115,22 @@ services:
# 日志级别
LOGGER_LEVEL: "${LOGGER_LEVEL:-INFO}"
# 信任反向代理(nginx)传来的 X-Forwarded-Proto/For:否则 prod 的
# HTTPSRedirectMiddleware 会把 https 请求误判为 http 造成 301 循环
FORWARDED_ALLOW_IPS: "*"
# HTTPSRedirectMiddleware 会把 https 请求误判为 http 造成 301 循环。
# 只信任应用网络子网(默认 10.255.178.0/24),不要用 "*"——否则客户端可伪造
# X-Forwarded-For 绕过登录限流与来源 IP 记录。
FORWARDED_ALLOW_IPS: "${FORWARDED_ALLOW_IPS:-10.255.178.0/24}"
# 令牌签名密钥与数据加密密钥(必须显式提供,禁止使用源码默认值)
SECRET_KEY: "${SECRET_KEY:?错误: 请设置 SECRET_KEY 环境变量(openssl rand -hex 32)}"
DATA_ENCRYPTION_KEY: "${DATA_ENCRYPTION_KEY:-}"
# 轮换前的旧主密钥(逗号分隔),仅用于解密历史密文
DATA_ENCRYPTION_OLD_KEYS: "${DATA_ENCRYPTION_OLD_KEYS:-}"
# 生产跨域白名单(逗号分隔)。未配置时应用不再回落通配
PROD_CORS_ORIGINS: "${PROD_CORS_ORIGINS:-}"
# 演示模式:为 true 时禁止写操作。当前线上为 true,如需可写请设为 false
DEMO_ENABLE: "${DEMO_ENABLE:-true}"
ports:
- "${BACKEND_PORT:-8001}:8001"
# 仅绑定本机:数据面不对公网开放,容器之间经 app_network 互联
- "127.0.0.1:${BACKEND_PORT:-8001}:8001"
volumes:
# ── 开发热更新(可选)─────────────────────
# 取消注释将宿主机代码覆盖容器内代码。生产环境保持注释,
+15
View File
@@ -0,0 +1,15 @@
# 静态资源长缓存 + 隐藏文件防护。被 nginx.conf 的 /web、/app location include。
# 撤销:删除本文件 + 移除 nginx.conf 中对应 include 行,然后重建 nginx。
location ~ /\. {
deny all;
access_log off;
log_not_found off;
}
location ~* ^/(web|app|docs)/(js|css|assets|static)/ {
add_header Cache-Control "public, max-age=31536000, immutable";
access_log off;
}
location ~* ^/(web|app|docs)/[^/]*\.(?:png|jpe?g|gif|svg|webp|ico|woff2?|ttf|eot)$ {
add_header Cache-Control "public, max-age=2592000";
access_log off;
}
+33 -2
View File
@@ -69,11 +69,22 @@ http {
limit_conn_zone $binary_remote_addr zone=conn_limit:10m;
# ==================== HTTP -> HTTPS 重定向 ====================
# HTTP server块 - 重定向到HTTPS
# HTTP server块 - 重定向到HTTPS(放行 ACME 校验路径,供证书自动续期使用)
server {
listen 80;
server_name service.fastapiadmin.com;
return 301 https://$server_name$request_uri;
# 证书续期校验路径(ACME http-01):必须在 301 之前放行,
# 否则签发/续期校验请求会被重定向到 https 而导致验证失败。
location ^~ /.well-known/acme-challenge/ {
root /usr/share/nginx/html/acme;
default_type "text/plain";
access_log off;
}
location / {
return 301 https://$server_name$request_uri;
}
}
# ==================== HTTPS Server ====================
@@ -101,17 +112,37 @@ http {
# ==================== 前端(/web) ====================
location /web {
include cache-headers.inc; # 静态资源长缓存(优化项,可删除)
alias /usr/share/nginx/html/web/dist;
try_files $uri $uri/ /web/index.html; #解决页面刷新404问题
}
location ~ /\. { deny all; access_log off; log_not_found off; }
# ==================== 小程序 H5(/app) ====================
# 已禁用:app 不开源,如需使用请取消注释
location /app {
include cache-headers.inc; # 静态资源长缓存(优化项,可删除)
alias /usr/share/nginx/html/app/dist/build/h5;
try_files $uri $uri/ /app/index.html; #解决页面刷新404问题
}
location ~ /\. { deny all; access_log off; log_not_found off; }
# ==================== 封堵接口文档(仅本机/内网可访问)====================
location ~ ^/api/v1/(docs|redoc|openapi\.json)$ {
allow 127.0.0.1;
allow 10.0.0.0/8;
allow 172.16.0.0/12;
allow 192.168.0.0/16;
deny all;
proxy_pass http://backend:8001;
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 $scheme;
}
# ==================== 后端 API 代理 ====================
location /api/v1 {
limit_req zone=api_limit burst=60 nodelay;