FastAPI生产环境部署实战:从开发到上线的完整指南
Orion K Lv6

FastAPI生产环境部署实战:从开发到上线的完整指南

将FastAPI应用从开发环境部署到生产环境是一个复杂的过程,涉及容器化、负载均衡、监控、安全等多个方面。作为一名负责过多个FastAPI项目生产部署的工程师,我想分享一些实战经验和最佳实践。

容器化部署

1. 多阶段Docker构建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# Dockerfile
FROM python:3.11-slim as builder

# 设置工作目录
WORKDIR /app

# 安装系统依赖
RUN apt-get update && apt-get install -y \
gcc \
g++ \
libpq-dev \
&& rm -rf /var/lib/apt/lists/*

# 复制依赖文件
COPY requirements.txt .
COPY requirements-prod.txt .

# 创建虚拟环境并安装依赖
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"

# 安装Python依赖
RUN pip install --no-cache-dir --upgrade pip && \
pip install --no-cache-dir -r requirements-prod.txt

# 生产阶段
FROM python:3.11-slim as production

# 安装运行时依赖
RUN apt-get update && apt-get install -y \
libpq5 \
curl \
&& rm -rf /var/lib/apt/lists/*

# 创建非root用户
RUN groupadd -r appuser && useradd -r -g appuser appuser

# 复制虚拟环境
COPY --from=builder /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"

# 设置工作目录
WORKDIR /app

# 复制应用代码
COPY --chown=appuser:appuser . .

# 创建必要的目录
RUN mkdir -p /app/logs && chown -R appuser:appuser /app/logs

# 切换到非root用户
USER appuser

# 健康检查
HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8000/health || exit 1

# 暴露端口
EXPOSE 8000

# 启动命令
CMD ["gunicorn", "-c", "gunicorn.conf.py", "main:app"]

2. Gunicorn生产配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# gunicorn.conf.py
import multiprocessing
import os

# 服务器配置
bind = "0.0.0.0:8000"
workers = int(os.getenv("WORKERS", multiprocessing.cpu_count() * 2 + 1))
worker_class = "uvicorn.workers.UvicornWorker"
worker_connections = int(os.getenv("WORKER_CONNECTIONS", 1000))

# 性能调优
max_requests = int(os.getenv("MAX_REQUESTS", 1000))
max_requests_jitter = int(os.getenv("MAX_REQUESTS_JITTER", 100))
preload_app = True
keepalive = int(os.getenv("KEEPALIVE", 5))

# 超时设置
timeout = int(os.getenv("TIMEOUT", 30))
graceful_timeout = int(os.getenv("GRACEFUL_TIMEOUT", 30))

# 日志配置
accesslog = "-"
errorlog = "-"
loglevel = os.getenv("LOG_LEVEL", "info")
access_log_format = '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s" %(D)s'

# 进程管理
proc_name = "fastapi-app"
pidfile = "/tmp/gunicorn.pid"

# SSL配置(如果需要)
keyfile = os.getenv("SSL_KEYFILE")
certfile = os.getenv("SSL_CERTFILE")

# 钩子函数
def when_ready(server):
server.log.info("Server is ready. Spawning workers")

def worker_int(worker):
worker.log.info("worker received INT or QUIT signal")

def pre_fork(server, worker):
server.log.info("Worker spawned (pid: %s)", worker.pid)

def post_fork(server, worker):
server.log.info("Worker spawned (pid: %s)", worker.pid)

def pre_exec(server):
server.log.info("Forked child, re-executing.")

def when_ready(server):
server.log.info("Server is ready. Spawning workers")

def worker_abort(worker):
worker.log.info("worker received SIGABRT signal")

3. Docker Compose开发环境

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
# docker-compose.yml
version: '3.8'

services:
app:
build:
context: .
dockerfile: Dockerfile
target: production
ports:
- "8000:8000"
environment:
- DATABASE_URL=postgresql://user:password@db:5432/fastapi_db
- REDIS_URL=redis://redis:6379/0
- LOG_LEVEL=info
- WORKERS=4
depends_on:
- db
- redis
volumes:
- ./logs:/app/logs
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s

db:
image: postgres:15-alpine
environment:
- POSTGRES_DB=fastapi_db
- POSTGRES_USER=user
- POSTGRES_PASSWORD=password
volumes:
- postgres_data:/var/lib/postgresql/data
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
ports:
- "5432:5432"
restart: unless-stopped

redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
restart: unless-stopped
command: redis-server --appendonly yes

nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
- ./ssl:/etc/nginx/ssl
depends_on:
- app
restart: unless-stopped

volumes:
postgres_data:
redis_data:

Kubernetes部署

1. Kubernetes配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
# k8s/namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
name: fastapi-app

---
# k8s/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: fastapi-config
namespace: fastapi-app
data:
LOG_LEVEL: "info"
WORKERS: "4"
MAX_REQUESTS: "1000"
TIMEOUT: "30"

---
# k8s/secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: fastapi-secrets
namespace: fastapi-app
type: Opaque
data:
DATABASE_URL: cG9zdGdyZXNxbDovL3VzZXI6cGFzc3dvcmRAZGI6NTQzMi9mYXN0YXBpX2Ri
REDIS_URL: cmVkaXM6Ly9yZWRpczozNjM3OS8w
JWT_SECRET_KEY: eW91ci1qd3Qtc2VjcmV0LWtleQ==

---
# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: fastapi-app
namespace: fastapi-app
labels:
app: fastapi-app
spec:
replicas: 3
selector:
matchLabels:
app: fastapi-app
template:
metadata:
labels:
app: fastapi-app
spec:
containers:
- name: fastapi-app
image: your-registry/fastapi-app:latest
ports:
- containerPort: 8000
env:
- name: LOG_LEVEL
valueFrom:
configMapKeyRef:
name: fastapi-config
key: LOG_LEVEL
- name: WORKERS
valueFrom:
configMapKeyRef:
name: fastapi-config
key: WORKERS
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: fastapi-secrets
key: DATABASE_URL
- name: REDIS_URL
valueFrom:
secretKeyRef:
name: fastapi-secrets
key: REDIS_URL
- name: JWT_SECRET_KEY
valueFrom:
secretKeyRef:
name: fastapi-secrets
key: JWT_SECRET_KEY
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
volumeMounts:
- name: logs
mountPath: /app/logs
volumes:
- name: logs
emptyDir: {}
imagePullSecrets:
- name: registry-secret

---
# k8s/service.yaml
apiVersion: v1
kind: Service
metadata:
name: fastapi-service
namespace: fastapi-app
spec:
selector:
app: fastapi-app
ports:
- protocol: TCP
port: 80
targetPort: 8000
type: ClusterIP

---
# k8s/ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: fastapi-ingress
namespace: fastapi-app
annotations:
kubernetes.io/ingress.class: nginx
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/rate-limit: "100"
nginx.ingress.kubernetes.io/rate-limit-window: "1m"
spec:
tls:
- hosts:
- api.yourdomain.com
secretName: fastapi-tls
rules:
- host: api.yourdomain.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: fastapi-service
port:
number: 80

---
# k8s/hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: fastapi-hpa
namespace: fastapi-app
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: fastapi-app
minReplicas: 3
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80

2. Helm Chart配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
# helm/Chart.yaml
apiVersion: v2
name: fastapi-app
description: A Helm chart for FastAPI application
type: application
version: 0.1.0
appVersion: "1.0.0"

---
# helm/values.yaml
replicaCount: 3

image:
repository: your-registry/fastapi-app
pullPolicy: IfNotPresent
tag: "latest"

imagePullSecrets:
- name: registry-secret

nameOverride: ""
fullnameOverride: ""

serviceAccount:
create: true
annotations: {}
name: ""

podAnnotations: {}

podSecurityContext:
fsGroup: 2000

securityContext:
capabilities:
drop:
- ALL
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 1000

service:
type: ClusterIP
port: 80
targetPort: 8000

ingress:
enabled: true
className: "nginx"
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/rate-limit: "100"
nginx.ingress.kubernetes.io/rate-limit-window: "1m"
hosts:
- host: api.yourdomain.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: fastapi-tls
hosts:
- api.yourdomain.com

resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 250m
memory: 256Mi

autoscaling:
enabled: true
minReplicas: 3
maxReplicas: 10
targetCPUUtilizationPercentage: 70
targetMemoryUtilizationPercentage: 80

nodeSelector: {}

tolerations: []

affinity: {}

config:
logLevel: info
workers: 4
maxRequests: 1000
timeout: 30

secrets:
databaseUrl: "postgresql://user:password@db:5432/fastapi_db"
redisUrl: "redis://redis:6379/0"
jwtSecretKey: "your-jwt-secret-key"

负载均衡和反向代理

1. Nginx配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
# nginx.conf
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;

events {
worker_connections 1024;
use epoll;
multi_accept on;
}

http {
include /etc/nginx/mime.types;
default_type application/octet-stream;

# 日志格式
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for" '
'rt=$request_time uct="$upstream_connect_time" '
'uht="$upstream_header_time" urt="$upstream_response_time"';

access_log /var/log/nginx/access.log main;

# 基础配置
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
client_max_body_size 100M;

# Gzip压缩
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_proxied any;
gzip_comp_level 6;
gzip_types
text/plain
text/css
text/xml
text/javascript
application/json
application/javascript
application/xml+rss
application/atom+xml
image/svg+xml;

# 限流配置
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s;

# 上游服务器
upstream fastapi_backend {
least_conn;
server app1:8000 max_fails=3 fail_timeout=30s;
server app2:8000 max_fails=3 fail_timeout=30s;
server app3:8000 max_fails=3 fail_timeout=30s;
keepalive 32;
}

# HTTP重定向到HTTPS
server {
listen 80;
server_name api.yourdomain.com;
return 301 https://$server_name$request_uri;
}

# HTTPS服务器
server {
listen 443 ssl http2;
server_name api.yourdomain.com;

# SSL配置
ssl_certificate /etc/nginx/ssl/cert.pem;
ssl_certificate_key /etc/nginx/ssl/key.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;

# 安全头
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Frame-Options DENY always;
add_header X-Content-Type-Options nosniff always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;

# 健康检查
location /health {
proxy_pass http://fastapi_backend;
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;
access_log off;
}

# API路由
location /api/ {
limit_req zone=api burst=20 nodelay;

proxy_pass http://fastapi_backend;
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;

# 超时设置
proxy_connect_timeout 30s;
proxy_send_timeout 30s;
proxy_read_timeout 30s;

# 缓冲设置
proxy_buffering on;
proxy_buffer_size 4k;
proxy_buffers 8 4k;
proxy_busy_buffers_size 8k;
}

# 登录接口特殊限流
location /api/auth/login {
limit_req zone=login burst=5 nodelay;

proxy_pass http://fastapi_backend;
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;
}

# 静态文件缓存
location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}

# 错误页面
error_page 404 /404.html;
error_page 500 502 503 504 /50x.html;

location = /50x.html {
root /usr/share/nginx/html;
}
}
}

2. HAProxy配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# haproxy.cfg
global
daemon
maxconn 4096
log stdout local0

# SSL配置
ssl-default-bind-ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384
ssl-default-bind-options ssl-min-ver TLSv1.2 no-tls-tickets

defaults
mode http
timeout connect 5000ms
timeout client 50000ms
timeout server 50000ms
option httplog
option dontlognull
option redispatch
retries 3

# 健康检查
option httpchk GET /health
http-check expect status 200

# 统计页面
stats enable
stats uri /stats
stats refresh 30s
stats admin if TRUE

# 前端配置
frontend fastapi_frontend
bind *:80
bind *:443 ssl crt /etc/ssl/certs/yourdomain.pem

# 重定向HTTP到HTTPS
redirect scheme https if !{ ssl_fc }

# 限流
stick-table type ip size 100k expire 30s store http_req_rate(10s)
http-request track-sc0 src
http-request deny if { sc_http_req_rate(0) gt 20 }

# 路由规则
use_backend fastapi_backend

# 后端配置
backend fastapi_backend
balance roundrobin

# 服务器配置
server app1 app1:8000 check inter 2000ms rise 2 fall 3
server app2 app2:8000 check inter 2000ms rise 2 fall 3
server app3 app3:8000 check inter 2000ms rise 2 fall 3

# 健康检查
option httpchk GET /health
http-check expect status 200

# 连接复用
http-reuse always

监控和日志

1. Prometheus监控配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# prometheus.yml
global:
scrape_interval: 15s
evaluation_interval: 15s

rule_files:
- "fastapi_rules.yml"

alerting:
alertmanagers:
- static_configs:
- targets:
- alertmanager:9093

scrape_configs:
- job_name: 'fastapi-app'
static_configs:
- targets: ['app:8000']
metrics_path: /metrics
scrape_interval: 15s

- job_name: 'nginx'
static_configs:
- targets: ['nginx:9113']

- job_name: 'postgres'
static_configs:
- targets: ['postgres-exporter:9187']

- job_name: 'redis'
static_configs:
- targets: ['redis-exporter:9121']

---
# fastapi_rules.yml
groups:
- name: fastapi_alerts
rules:
- alert: HighErrorRate
expr: rate(http_requests_total{status=~"5.."}[5m]) > 0.1
for: 5m
labels:
severity: critical
annotations:
summary: "High error rate detected"
description: "Error rate is {{ $value }} errors per second"

- alert: HighResponseTime
expr: histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m])) > 1
for: 5m
labels:
severity: warning
annotations:
summary: "High response time detected"
description: "95th percentile response time is {{ $value }} seconds"

- alert: HighMemoryUsage
expr: process_resident_memory_bytes / 1024 / 1024 > 500
for: 5m
labels:
severity: warning
annotations:
summary: "High memory usage detected"
description: "Memory usage is {{ $value }} MB"

2. Grafana仪表板配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
{
"dashboard": {
"id": null,
"title": "FastAPI Application Dashboard",
"tags": ["fastapi", "python"],
"timezone": "browser",
"panels": [
{
"id": 1,
"title": "Request Rate",
"type": "graph",
"targets": [
{
"expr": "rate(http_requests_total[5m])",
"legendFormat": "{{method}} {{endpoint}}"
}
],
"yAxes": [
{
"label": "Requests/sec"
}
]
},
{
"id": 2,
"title": "Response Time",
"type": "graph",
"targets": [
{
"expr": "histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))",
"legendFormat": "95th percentile"
},
{
"expr": "histogram_quantile(0.50, rate(http_request_duration_seconds_bucket[5m]))",
"legendFormat": "50th percentile"
}
],
"yAxes": [
{
"label": "Seconds"
}
]
},
{
"id": 3,
"title": "Error Rate",
"type": "graph",
"targets": [
{
"expr": "rate(http_requests_total{status=~\"4..|5..\"}[5m])",
"legendFormat": "{{status}}"
}
],
"yAxes": [
{
"label": "Errors/sec"
}
]
},
{
"id": 4,
"title": "Memory Usage",
"type": "graph",
"targets": [
{
"expr": "process_resident_memory_bytes / 1024 / 1024",
"legendFormat": "Memory (MB)"
}
],
"yAxes": [
{
"label": "MB"
}
]
}
],
"time": {
"from": "now-1h",
"to": "now"
},
"refresh": "5s"
}
}

3. ELK Stack日志配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# docker-compose.elk.yml
version: '3.8'

services:
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:8.8.0
environment:
- discovery.type=single-node
- xpack.security.enabled=false
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
ports:
- "9200:9200"
volumes:
- elasticsearch_data:/usr/share/elasticsearch/data

logstash:
image: docker.elastic.co/logstash/logstash:8.8.0
ports:
- "5044:5044"
volumes:
- ./logstash.conf:/usr/share/logstash/pipeline/logstash.conf
depends_on:
- elasticsearch

kibana:
image: docker.elastic.co/kibana/kibana:8.8.0
ports:
- "5601:5601"
environment:
- ELASTICSEARCH_HOSTS=http://elasticsearch:9200
depends_on:
- elasticsearch

filebeat:
image: docker.elastic.co/beats/filebeat:8.8.0
user: root
volumes:
- ./filebeat.yml:/usr/share/filebeat/filebeat.yml:ro
- /var/lib/docker/containers:/var/lib/docker/containers:ro
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./logs:/app/logs:ro
depends_on:
- logstash

volumes:
elasticsearch_data:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# filebeat.yml
filebeat.inputs:
- type: log
enabled: true
paths:
- /app/logs/*.log
json.keys_under_root: true
json.add_error_key: true
multiline.pattern: '^\{'
multiline.negate: true
multiline.match: after

output.logstash:
hosts: ["logstash:5044"]

processors:
- add_docker_metadata:
host: "unix:///var/run/docker.sock"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# logstash.conf
input {
beats {
port => 5044
}
}

filter {
if [fields][service] == "fastapi" {
json {
source => "message"
}

date {
match => [ "timestamp", "ISO8601" ]
}

if [level] == "ERROR" or [level] == "CRITICAL" {
mutate {
add_tag => [ "error" ]
}
}
}
}

output {
elasticsearch {
hosts => ["elasticsearch:9200"]
index => "fastapi-logs-%{+YYYY.MM.dd}"
}
}

CI/CD流水线

1. GitHub Actions配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# .github/workflows/deploy.yml
name: Deploy FastAPI Application

on:
push:
branches: [ main ]
pull_request:
branches: [ main ]

env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}

jobs:
test:
runs-on: ubuntu-latest

services:
postgres:
image: postgres:15
env:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: test_db
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432

redis:
image: redis:7
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 6379:6379

steps:
- uses: actions/checkout@v3

- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install -r requirements-dev.txt

- name: Run tests
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test_db
REDIS_URL: redis://localhost:6379/0
run: |
pytest tests/ -v --cov=app --cov-report=xml

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
file: ./coverage.xml

build-and-push:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'

permissions:
contents: read
packages: write

steps:
- name: Checkout repository
uses: actions/checkout@v3

- name: Log in to Container Registry
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Extract metadata
id: meta
uses: docker/metadata-action@v4
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=sha,prefix={{branch}}-
type=raw,value=latest,enable={{is_default_branch}}

- name: Build and push Docker image
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

deploy:
needs: build-and-push
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'

steps:
- name: Checkout repository
uses: actions/checkout@v3

- name: Set up kubectl
uses: azure/setup-kubectl@v3
with:
version: 'v1.27.0'

- name: Configure kubectl
run: |
echo "${{ secrets.KUBE_CONFIG }}" | base64 -d > kubeconfig
export KUBECONFIG=kubeconfig

- name: Deploy to Kubernetes
run: |
export KUBECONFIG=kubeconfig
kubectl set image deployment/fastapi-app fastapi-app=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} -n fastapi-app
kubectl rollout status deployment/fastapi-app -n fastapi-app --timeout=300s

- name: Verify deployment
run: |
export KUBECONFIG=kubeconfig
kubectl get pods -n fastapi-app
kubectl get services -n fastapi-app

安全配置

1. 安全最佳实践

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
# security.py
from fastapi import FastAPI, Request, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.trustedhost import TrustedHostMiddleware
import secrets
import hashlib
import hmac
from typing import List

app = FastAPI()

# CORS配置
app.add_middleware(
CORSMiddleware,
allow_origins=["https://yourdomain.com"], # 生产环境中指定具体域名
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE"],
allow_headers=["*"],
expose_headers=["X-Request-ID"]
)

# 可信主机中间件
app.add_middleware(
TrustedHostMiddleware,
allowed_hosts=["api.yourdomain.com", "*.yourdomain.com"]
)

# 安全头中间件
@app.middleware("http")
async def add_security_headers(request: Request, call_next):
response = await call_next(request)

# 安全头
response.headers["X-Content-Type-Options"] = "nosniff"
response.headers["X-Frame-Options"] = "DENY"
response.headers["X-XSS-Protection"] = "1; mode=block"
response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains"
response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"

# CSP头
csp_policy = (
"default-src 'self'; "
"script-src 'self' 'unsafe-inline'; "
"style-src 'self' 'unsafe-inline'; "
"img-src 'self' data: https:; "
"font-src 'self' https:; "
"connect-src 'self' https:; "
"frame-ancestors 'none';"
)
response.headers["Content-Security-Policy"] = csp_policy

return response

# API密钥验证
class APIKeyAuth:
def __init__(self, api_keys: List[str]):
self.api_keys = set(api_keys)

def verify_api_key(self, api_key: str) -> bool:
return api_key in self.api_keys

def __call__(self, request: Request):
api_key = request.headers.get("X-API-Key")
if not api_key or not self.verify_api_key(api_key):
raise HTTPException(
status_code=401,
detail="Invalid or missing API key"
)
return api_key

# Webhook签名验证
def verify_webhook_signature(payload: bytes, signature: str, secret: str) -> bool:
"""验证webhook签名"""
expected_signature = hmac.new(
secret.encode(),
payload,
hashlib.sha256
).hexdigest()

return hmac.compare_digest(f"sha256={expected_signature}", signature)

# 速率限制
from collections import defaultdict
import time

class RateLimiter:
def __init__(self, max_requests: int = 100, window: int = 3600):
self.max_requests = max_requests
self.window = window
self.requests = defaultdict(list)

def is_allowed(self, identifier: str) -> bool:
now = time.time()
window_start = now - self.window

# 清理过期请求
self.requests[identifier] = [
req_time for req_time in self.requests[identifier]
if req_time > window_start
]

# 检查是否超过限制
if len(self.requests[identifier]) >= self.max_requests:
return False

# 记录新请求
self.requests[identifier].append(now)
return True

rate_limiter = RateLimiter(max_requests=1000, window=3600)

@app.middleware("http")
async def rate_limit_middleware(request: Request, call_next):
client_ip = request.client.host

if not rate_limiter.is_allowed(client_ip):
raise HTTPException(
status_code=429,
detail="Rate limit exceeded"
)

response = await call_next(request)
return response

2. 环境变量和密钥管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# config.py
import os
from typing import Optional
from pydantic import BaseSettings, validator
import secrets

class Settings(BaseSettings):
# 应用配置
app_name: str = "FastAPI App"
debug: bool = False
version: str = "1.0.0"

# 服务器配置
host: str = "0.0.0.0"
port: int = 8000
workers: int = 4

# 数据库配置
database_url: str
database_pool_size: int = 20
database_max_overflow: int = 30

# Redis配置
redis_url: str
redis_pool_size: int = 10

# JWT配置
jwt_secret_key: str
jwt_algorithm: str = "HS256"
jwt_expire_minutes: int = 30

# 外部服务配置
smtp_server: Optional[str] = None
smtp_port: int = 587
smtp_username: Optional[str] = None
smtp_password: Optional[str] = None

# 监控配置
sentry_dsn: Optional[str] = None
prometheus_enabled: bool = True

# 安全配置
allowed_hosts: list = ["*"]
cors_origins: list = ["*"]
api_keys: list = []

@validator("jwt_secret_key")
def validate_jwt_secret(cls, v):
if not v or len(v) < 32:
raise ValueError("JWT secret key must be at least 32 characters long")
return v

@validator("database_url")
def validate_database_url(cls, v):
if not v.startswith(("postgresql://", "mysql://", "sqlite:///")):
raise ValueError("Invalid database URL")
return v

class Config:
env_file = ".env"
case_sensitive = False

# 创建设置实例
settings = Settings()

# 生产环境密钥生成
def generate_secret_key() -> str:
"""生成安全的密钥"""
return secrets.token_urlsafe(32)

# 环境检查
def check_production_config():
"""检查生产环境配置"""
issues = []

if settings.debug:
issues.append("Debug mode is enabled in production")

if settings.jwt_secret_key == "your-secret-key":
issues.append("Default JWT secret key is being used")

if "*" in settings.allowed_hosts:
issues.append("Wildcard allowed hosts in production")

if "*" in settings.cors_origins:
issues.append("Wildcard CORS origins in production")

if not settings.sentry_dsn:
issues.append("Sentry DSN not configured for error tracking")

if issues:
raise ValueError(f"Production configuration issues: {', '.join(issues)}")

# 在生产环境启动时检查配置
if os.getenv("ENVIRONMENT") == "production":
check_production_config()

性能优化

1. 数据库连接池优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# database.py
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker
from sqlalchemy.pool import QueuePool
import asyncio

# 优化的数据库引擎配置
engine = create_async_engine(
settings.database_url,
# 连接池配置
poolclass=QueuePool,
pool_size=settings.database_pool_size,
max_overflow=settings.database_max_overflow,
pool_pre_ping=True,
pool_recycle=3600,

# 性能优化
echo=settings.debug,
future=True,

# 连接参数
connect_args={
"server_settings": {
"application_name": settings.app_name,
"jit": "off",
},
"command_timeout": 60,
}
)

AsyncSessionLocal = sessionmaker(
engine,
class_=AsyncSession,
expire_on_commit=False
)

# 数据库会话依赖
async def get_db():
async with AsyncSessionLocal() as session:
try:
yield session
except Exception:
await session.rollback()
raise
finally:
await session.close()

# 数据库健康检查
async def check_database_health():
try:
async with AsyncSessionLocal() as session:
await session.execute("SELECT 1")
return True
except Exception:
return False

2. 缓存策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# cache.py
import redis.asyncio as redis
import json
import pickle
from typing import Any, Optional, Union
from functools import wraps
import hashlib

# Redis连接池
redis_pool = redis.ConnectionPool.from_url(
settings.redis_url,
max_connections=settings.redis_pool_size,
retry_on_timeout=True,
socket_keepalive=True,
socket_keepalive_options={}
)

redis_client = redis.Redis(connection_pool=redis_pool)

class CacheManager:
def __init__(self, redis_client: redis.Redis):
self.redis = redis_client

async def get(self, key: str) -> Optional[Any]:
"""获取缓存值"""
try:
value = await self.redis.get(key)
if value:
return pickle.loads(value)
except Exception as e:
logger.error(f"Cache get error: {e}")
return None

async def set(self, key: str, value: Any, expire: int = 3600):
"""设置缓存值"""
try:
serialized_value = pickle.dumps(value)
await self.redis.setex(key, expire, serialized_value)
except Exception as e:
logger.error(f"Cache set error: {e}")

async def delete(self, key: str):
"""删除缓存"""
try:
await self.redis.delete(key)
except Exception as e:
logger.error(f"Cache delete error: {e}")

async def exists(self, key: str) -> bool:
"""检查缓存是否存在"""
try:
return await self.redis.exists(key)
except Exception as e:
logger.error(f"Cache exists error: {e}")
return False

cache_manager = CacheManager(redis_client)

# 缓存装饰器
def cache_result(expire: int = 3600, key_prefix: str = ""):
def decorator(func):
@wraps(func)
async def wrapper(*args, **kwargs):
# 生成缓存键
cache_key = f"{key_prefix}:{func.__name__}:{_generate_cache_key(args, kwargs)}"

# 尝试从缓存获取
cached_result = await cache_manager.get(cache_key)
if cached_result is not None:
return cached_result

# 执行函数并缓存结果
result = await func(*args, **kwargs)
await cache_manager.set(cache_key, result, expire)

return result
return wrapper
return decorator

def _generate_cache_key(args, kwargs) -> str:
"""生成缓存键"""
key_data = str(args) + str(sorted(kwargs.items()))
return hashlib.md5(key_data.encode()).hexdigest()

# 使用示例
@cache_result(expire=1800, key_prefix="user")
async def get_user_profile(user_id: int):
# 数据库查询逻辑
pass

总结和最佳实践

通过本文的全面介绍,我们学习了FastAPI生产环境部署的完整流程:

核心要点

  1. 容器化部署:使用多阶段Docker构建,优化镜像大小和安全性
  2. Kubernetes编排:实现自动扩缩容、健康检查和滚动更新
  3. 负载均衡:配置Nginx/HAProxy实现高可用和性能优化
  4. 监控告警:集成Prometheus、Grafana和ELK Stack
  5. CI/CD流水线:自动化测试、构建和部署流程
  6. 安全配置:实施多层安全防护措施
  7. 性能优化:数据库连接池、缓存策略等优化

部署检查清单

部署前检查:

  • 代码测试覆盖率达标
  • 安全配置审查完成
  • 性能测试通过
  • 监控告警配置完成
  • 备份恢复方案就绪

部署过程:

  • 蓝绿部署或滚动更新
  • 健康检查通过
  • 监控指标正常
  • 日志输出正常
  • 回滚方案准备

部署后验证:

  • 功能测试通过
  • 性能指标达标
  • 错误率在可接受范围
  • 用户反馈正常
  • 监控告警正常

运维建议

  1. 监控体系:建立完善的监控和告警体系
  2. 日志管理:集中化日志收集和分析
  3. 备份策略:定期备份数据和配置
  4. 安全更新:及时更新依赖和系统补丁
  5. 容量规划:根据业务增长规划资源
  6. 故障演练:定期进行故障恢复演练

FastAPI生产环境部署是一个系统工程,需要考虑安全、性能、可用性等多个方面。通过遵循最佳实践和持续优化,可以构建出稳定可靠的生产系统。

你在FastAPI生产部署方面有什么经验或遇到过什么挑战吗?欢迎在评论中分享讨论!

本站由 提供部署服务