Files
FastapiAdmin/docker/docker-compose.yaml
T
zhangtao c73daa7eae chore(deploy): 健康检查发正确 Host、list 型配置改 JSON、Host 校验收窄
- 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
2026-10-04 05:12:37 +08:00

246 lines
8.7 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ============================================
# 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