mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-10-13 20:51:09 +00:00
- backend 健康检查改为发送正确的 Host 头(而非放宽 ALLOWED_HOSTS);
配合 ALLOW_LOCALHOST_HOSTS=false,Host: localhost 现在返回 400,容器自检仍 exit 0
- OAUTH_ALLOWED_HOSTS / CORS_STRICT_STARTUP / SCHEDULER_ALLOW_CODE_EXEC 接入 compose 注入
- ⚠️ OAUTH_ALLOWED_HOSTS 是 list[str],必须写 JSON 数组:本次部署曾因写成逗号分隔字符串
导致启动期 SettingsError、容器重启、网关 502(已修复,并在 compose 默认值中固化 JSON 形态)
- DEPLOYMENT.md 新增「配置取值格式」章节记录该坑与排查方法
- AUDIT-REPORT.md 更新修复进展(含复核 t12 驳回与 t13/t14 通过、残余项)
线上验证:四端 200;Host=localhost → 400;健康检查 exit 0;公网 docs/.DS_Store → 403
246 lines
8.7 KiB
YAML
246 lines
8.7 KiB
YAML
# ============================================
|
||
# FastapiAdmin Docker Compose 配置文件
|
||
# ============================================
|
||
# 使用方式:
|
||
# 开发环境: docker compose --env-file .env up -d
|
||
# 生产环境: docker compose --env-file .env -f docker-compose.yaml up -d
|
||
# ============================================
|
||
|
||
services:
|
||
# ==================== 数据库服务 ====================
|
||
mysql:
|
||
container_name: mysql
|
||
image: mysql:8.0
|
||
restart: unless-stopped
|
||
platform: linux/amd64
|
||
environment:
|
||
TZ: "Asia/Shanghai"
|
||
MYSQL_ROOT_PASSWORD: "${MYSQL_ROOT_PASSWORD:?错误: 请设置 MYSQL_ROOT_PASSWORD 环境变量}"
|
||
MYSQL_DATABASE: "${MYSQL_DATABASE:-fastapiadmin}"
|
||
MYSQL_USER: "${MYSQL_USER:-fastapiadmin}"
|
||
MYSQL_PASSWORD: "${MYSQL_PASSWORD:?错误: 请设置 MYSQL_PASSWORD 环境变量}"
|
||
ports:
|
||
- "127.0.0.1:${MYSQL_PORT:-3306}:3306"
|
||
volumes:
|
||
- mysql_data:/var/lib/mysql
|
||
- /etc/localtime:/etc/localtime:ro
|
||
# 首次启动时自动执行 init SQL 脚本
|
||
- ./mysql/init:/docker-entrypoint-initdb.d:ro
|
||
networks:
|
||
- app_network
|
||
healthcheck:
|
||
test:
|
||
["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p${MYSQL_ROOT_PASSWORD}"]
|
||
interval: 10s
|
||
timeout: 10s
|
||
retries: 10
|
||
start_period: 30s
|
||
logging:
|
||
driver: "json-file"
|
||
options:
|
||
max-size: "10m"
|
||
max-file: "3"
|
||
deploy:
|
||
resources:
|
||
limits:
|
||
memory: 1g
|
||
reservations:
|
||
memory: 256m
|
||
|
||
# ==================== Redis 服务 ====================
|
||
redis:
|
||
container_name: redis
|
||
image: redis:7-alpine
|
||
restart: unless-stopped
|
||
platform: linux/amd64
|
||
environment:
|
||
TZ: "Asia/Shanghai"
|
||
ports:
|
||
- "127.0.0.1:${REDIS_PORT:-6379}:6379"
|
||
volumes:
|
||
- redis_data:/data
|
||
# 挂载自定义 redis.conf(若存在则覆盖默认配置)
|
||
- ./redis/conf/redis.conf:/usr/local/etc/redis/redis.conf:ro
|
||
command: >
|
||
redis-server /usr/local/etc/redis/redis.conf
|
||
--requirepass ${REDIS_PASSWORD:?错误: 请设置 REDIS_PASSWORD 环境变量}
|
||
networks:
|
||
- app_network
|
||
healthcheck:
|
||
test:
|
||
["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
|
||
interval: 10s
|
||
timeout: 5s
|
||
retries: 5
|
||
start_period: 10s
|
||
logging:
|
||
driver: "json-file"
|
||
options:
|
||
max-size: "10m"
|
||
max-file: "3"
|
||
deploy:
|
||
resources:
|
||
limits:
|
||
memory: 512m
|
||
reservations:
|
||
memory: 128m
|
||
|
||
# ==================== 后端服务 ====================
|
||
backend:
|
||
container_name: backend
|
||
build:
|
||
context: ../
|
||
dockerfile: ./docker/backend/Dockerfile
|
||
args:
|
||
DEPLOY_ENV: "${DEPLOY_ENV:-prod}"
|
||
image: "backend:${BACKEND_IMAGE_TAG:-3.0.0}"
|
||
restart: unless-stopped
|
||
platform: linux/amd64
|
||
environment:
|
||
TZ: "Asia/Shanghai"
|
||
# 运行环境,对应 backend/env/.env.{dev,prod}
|
||
ENVIRONMENT: "${DEPLOY_ENV:-prod}"
|
||
# MySQL 连接配置
|
||
DATABASE_HOST: "mysql"
|
||
DATABASE_PORT: "3306"
|
||
DATABASE_USER: "${MYSQL_USER:-fastapiadmin}"
|
||
DATABASE_PASSWORD: "${MYSQL_PASSWORD:?错误: 请设置 MYSQL_PASSWORD 环境变量}"
|
||
DATABASE_NAME: "${MYSQL_DATABASE:-fastapiadmin}"
|
||
# Redis 连接配置
|
||
REDIS_HOST: "redis"
|
||
REDIS_PORT: "6379"
|
||
REDIS_PASSWORD: "${REDIS_PASSWORD}"
|
||
REDIS_DB_NAME: "1"
|
||
REDIS_ENABLE: "true"
|
||
# 日志级别
|
||
LOGGER_LEVEL: "${LOGGER_LEVEL:-INFO}"
|
||
# 信任反向代理(nginx)传来的 X-Forwarded-Proto/For:否则 prod 的
|
||
# 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}"
|
||
# Host 头校验范围:false 时 ALLOWED_HOSTS 收敛为仅生产域名(推荐)。
|
||
# 容器健康检查已改为发送正确 Host,因此这里可以为 false。
|
||
ALLOW_LOCALHOST_HOSTS: "${ALLOW_LOCALHOST_HOSTS:-true}"
|
||
# OAuth 回调/换取令牌允许的 Host。
|
||
# 注意:应用里该字段是 list[str],pydantic-settings 要求 **JSON 数组**字面量,
|
||
# 写成逗号分隔字符串会导致启动期 SettingsError(已踩过,服务会起不来)。
|
||
OAUTH_ALLOWED_HOSTS: '${OAUTH_ALLOWED_HOSTS:-["service.fastapiadmin.com","*.fastapiadmin.com"]}'
|
||
# 生产未配置 PROD_CORS_ORIGINS 时是否直接启动失败(默认 false:仅告警并按空列表处理)
|
||
CORS_STRICT_STARTUP: "${CORS_STRICT_STARTUP:-false}"
|
||
# 定时任务是否允许执行用户提交的代码块(等同 RCE 能力,生产保持 false)
|
||
SCHEDULER_ALLOW_CODE_EXEC: "${SCHEDULER_ALLOW_CODE_EXEC:-false}"
|
||
ports:
|
||
# 仅绑定本机:数据面不对公网开放,容器之间经 app_network 互联
|
||
- "127.0.0.1:${BACKEND_PORT:-8001}:8001"
|
||
volumes:
|
||
# ── 开发热更新(可选)─────────────────────
|
||
# 取消注释将宿主机代码覆盖容器内代码。生产环境保持注释,
|
||
# 使用镜像内自带的代码(Dockerfile 已 COPY backend)。
|
||
# ─────────────────────────────────────────
|
||
# - ../backend:/home
|
||
# 本地存储源根目录持久化(种子节点 host=static/upload):
|
||
# 必须挂载,否则每次部署重建容器会丢失用户上传的文件
|
||
- ./backend/static/upload:/home/static/upload
|
||
depends_on:
|
||
mysql:
|
||
condition: service_healthy
|
||
redis:
|
||
condition: service_healthy
|
||
networks:
|
||
- app_network
|
||
# 健康检查必须带正确的 Host 头:应用启用了 TrustedHostMiddleware,
|
||
# Host 为 localhost 会被判 400;这里用「发正确 Host」而不是「放宽 ALLOWED_HOSTS」,
|
||
# 避免为了容器自检而降低对外 Host 校验强度。
|
||
healthcheck:
|
||
test:
|
||
[
|
||
"CMD-SHELL",
|
||
"python -c 'import urllib.request; req = urllib.request.Request(\"http://localhost:8001/api/v1/monitor/health/check\", headers={\"Host\": \"service.fastapiadmin.com\", \"X-Forwarded-Proto\": \"https\"}); urllib.request.urlopen(req, timeout=5)' || exit 1",
|
||
]
|
||
interval: 15s
|
||
timeout: 10s
|
||
retries: 5
|
||
start_period: 40s
|
||
logging:
|
||
driver: "json-file"
|
||
options:
|
||
max-size: "10m"
|
||
max-file: "3"
|
||
deploy:
|
||
resources:
|
||
limits:
|
||
memory: 1g
|
||
cpus: "1.0"
|
||
reservations:
|
||
memory: 256m
|
||
|
||
# ==================== Nginx 服务 ====================
|
||
nginx:
|
||
container_name: nginx
|
||
image: nginx:1.25-alpine
|
||
restart: unless-stopped
|
||
platform: linux/amd64
|
||
environment:
|
||
TZ: "Asia/Shanghai"
|
||
ports:
|
||
- "${HTTP_PORT:-80}:80"
|
||
- "${HTTPS_PORT:-443}:443"
|
||
volumes:
|
||
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
|
||
- ./nginx/web:/usr/share/nginx/html/web:ro
|
||
- ./nginx/ssl:/etc/nginx/ssl:ro
|
||
- ./nginx/app:/usr/share/nginx/html/app:ro
|
||
- ./nginx/docs:/usr/share/nginx/html/docs:ro
|
||
depends_on:
|
||
backend:
|
||
condition: service_started
|
||
networks:
|
||
- app_network
|
||
healthcheck:
|
||
test: ["CMD", "nginx", "-t"]
|
||
interval: 30s
|
||
timeout: 5s
|
||
retries: 3
|
||
logging:
|
||
driver: "json-file"
|
||
options:
|
||
max-size: "10m"
|
||
max-file: "3"
|
||
deploy:
|
||
resources:
|
||
limits:
|
||
memory: 256m
|
||
cpus: "0.5"
|
||
reservations:
|
||
memory: 64m
|
||
|
||
# ==================== 持久化卷 ====================
|
||
volumes:
|
||
mysql_data:
|
||
driver: local
|
||
driver_opts:
|
||
type: none
|
||
device: ./mysql/data
|
||
o: bind
|
||
redis_data:
|
||
driver: local
|
||
driver_opts:
|
||
type: none
|
||
device: ./redis/data
|
||
o: bind
|
||
|
||
# ==================== 网络 ====================
|
||
networks:
|
||
app_network:
|
||
driver: bridge
|