跳到主要内容

部署

将 Flask 应用部署到生产环境需要考虑性能、安全性、可靠性和可扩展性。开发时使用的 Flask 内置服务器不适合生产环境,你需要使用专门的 WSGI 服务器和正确的配置来确保应用稳定运行。

理解生产环境部署

什么是"生产环境"

"生产环境"指的是非开发环境,无论你的应用是公开服务数百万用户,还是内部服务少数用户,只要是正式使用的环境都属于生产环境。

重要警告:不要在生产环境使用 Flask 开发服务器。开发服务器的设计目标是方便开发,它不具备以下生产环境必需的特性:

  • 多进程/多线程支持
  • 请求处理优化
  • 安全防护
  • 稳定性保障
  • 性能优化

WSGI 架构

Flask 是一个 WSGI(Web Server Gateway Interface)应用。理解 WSGI 架构有助于正确部署:

客户端 → HTTP 服务器(Nginx/Apache) → WSGI 服务器(Gunicorn/uWSGI) → Flask 应用

各组件的职责:

组件职责
HTTP 服务器处理静态文件、SSL 终止、负载均衡、请求路由
WSGI 服务器管理 Python 进程、处理并发、执行应用代码
Flask 应用业务逻辑处理

小型部署可以省略 HTTP 服务器,直接让 WSGI 服务器处理所有请求。

WSGI 服务器选择

Gunicorn

Gunicorn 是最流行的 Python WSGI 服务器之一,特点如下:

  • 纯 Python 实现,安装简单
  • 支持多种工作模式(sync、gevent、eventlet)
  • 配置简单,文档完善
  • 与大多数托管平台兼容良好

注意:Gunicorn 原生不支持 Windows(但可以在 WSL 中运行)。

安装

pip install gunicorn

基本使用

# 基本语法:gunicorn 模块名:应用变量
gunicorn myapp:app

# 指定工作进程数(推荐:CPU 核心数 * 2 + 1)
gunicorn -w 4 myapp:app

# 绑定地址和端口
gunicorn -b 0.0.0.0:8000 myapp:app

# 使用应用工厂模式
gunicorn 'myapp:create_app()'

# 后台运行
gunicorn -D myapp:app

工作模式

Gunicorn 支持多种工作模式:

同步工作模式(默认)

适合 CPU 密集型或请求时间短的应用:

# 默认使用 sync worker
gunicorn -w 4 myapp:app

异步工作模式(gevent)

适合 I/O 密集型、需要大量并发连接的应用:

# 安装 gevent
pip install gevent

# 使用 gevent worker
gunicorn -k gevent -w 4 myapp:app

异步工作模式(eventlet)

另一个异步选择:

# 安装 eventlet
pip install eventlet

# 使用 eventlet worker
gunicorn -k eventlet -w 4 myapp:app

重要:使用 gevent 或 eventlet 时,需要 greenlet >= 1.0,否则 Flask 的上下文变量(如 request)可能无法正常工作。

配置文件

使用配置文件管理复杂配置:

# gunicorn.conf.py
import multiprocessing

# 服务器绑定
bind = "0.0.0.0:8000"

# 工作进程
workers = multiprocessing.cpu_count() * 2 + 1
worker_class = "sync" # 或 'gevent', 'eventlet'
worker_connections = 1000 # gevent 模式下每个 worker 的并发连接数
threads = 1 # 每个 worker 的线程数

# 超时设置
timeout = 30 # worker 超时时间(秒)
keepalive = 2 # Keep-Alive 超时时间
graceful_timeout = 30 # 优雅关闭超时

# 日志
accesslog = "-" # 访问日志输出到 stdout
errorlog = "-" # 错误日志输出到 stderr
loglevel = "info"
access_log_format = '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s" %(D)s'

# 进程管理
daemon = False # 是否后台运行
pidfile = None # PID 文件路径
umask = 0 # 进程 umask
user = None # 运行用户
group = None # 运行组

# 安全
limit_request_line = 4094 # 请求行最大大小
limit_request_fields = 100 # 请求头字段数量限制
limit_request_field_size = 8190 # 请求头字段大小限制

使用配置文件:

gunicorn -c gunicorn.conf.py myapp:app

uWSGI

uWSGI 是功能更丰富的 WSGI 服务器:

pip install uwsgi

# 基本使用
uwsgi --http :8000 --wsgi-file myapp.py --callable app

# 使用配置文件
uwsgi --ini uwsgi.ini

uWSGI 配置文件示例:

# uwsgi.ini
[uwsgi]
module = myapp:app
master = true
processes = 4
socket = 0.0.0.0:8000
chmod-socket = 660
vacuum = true
die-on-term = true

# 日志
logto = /var/log/uwsgi/%n.log

# 性能
buffer-size = 32768
thunder-lock = true

# 监控
stats = 0.0.0.0:9191

Waitress

Waitress 是纯 Python 实现的 WSGI 服务器,支持 Windows:

pip install waitress

# 命令行启动
waitress-serve --port=8000 myapp:app

# 或在代码中启动
from waitress import serve
serve(app, host='0.0.0.0', port=8000)

服务器对比

特性GunicornuWSGIWaitress
语言Python + CCPython
Windows 支持需 WSL需 WSL原生支持
异步支持gevent/eventlet原生
配置复杂度简单复杂简单
内存占用
功能丰富度
推荐场景大多数应用需要高级功能Windows 开发

反向代理配置

在生产环境中,通常使用 Nginx 或 Apache 作为反向代理,处理静态文件和 SSL 终止。

Nginx 配置

基本配置:

# /etc/nginx/sites-available/myapp
server {
listen 80;
server_name example.com www.example.com;

# 重定向到 HTTPS
return 301 https://$server_name$request_uri;
}

server {
listen 443 ssl http2;
server_name example.com www.example.com;

# SSL 证书
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

# SSL 配置
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;

# 安全头
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;

# 静态文件
location /static {
alias /var/www/myapp/static;
expires 30d;
add_header Cache-Control "public, immutable";
}

# 上传文件
location /uploads {
alias /var/www/myapp/uploads;
expires 7d;
}

# 代理到 Gunicorn
location / {
proxy_pass http://127.0.0.1:8000;
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 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;

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

# WebSocket 支持(如需要)
location /ws {
proxy_pass http://127.0.0.1:8000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}

# 健康检查
location /health {
proxy_pass http://127.0.0.1:8000/health;
access_log off;
}
}

正确获取客户端 IP

当使用反向代理时,request.remote_addr 会返回代理服务器的 IP。需要使用 ProxyFix 中间件:

from werkzeug.middleware.proxy_fix import ProxyFix

app = Flask(__name__)

# 信任一层代理
app.wsgi_app = ProxyFix(
app.wsgi_app,
x_for=1, # X-Forwarded-For
x_proto=1, # X-Forwarded-Proto
x_host=1, # X-Forwarded-Host
x_port=1, # X-Forwarded-Port
x_prefix=1 # X-Forwarded-Prefix
)

x_for=1 表示信任一层代理。如果你有多层代理,需要设置相应的数量。

Apache 配置

使用 mod_wsgi:

# /etc/apache2/sites-available/myapp.conf
<VirtualHost *:80>
ServerName example.com

# 重定向到 HTTPS
Redirect permanent / https://example.com/
</VirtualHost>

<VirtualHost *:443>
ServerName example.com

# SSL 配置
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/example.com/cert.pem
SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem

# WSGI 配置
WSGIDaemonProcess myapp user=www-data group=www-data threads=4
WSGIScriptAlias / /var/www/myapp/wsgi.py

<Directory /var/www/myapp>
WSGIProcessGroup myapp
WSGIApplicationGroup %{GLOBAL}
Require all granted
</Directory>

# 静态文件
Alias /static /var/www/myapp/static
<Directory /var/www/myapp/static>
Require all granted
ExpiresActive On
ExpiresDefault "access plus 30 days"
</Directory>
</VirtualHost>

生产环境配置

配置管理

使用环境区分配置:

# config.py
import os
from datetime import timedelta

class Config:
"""基础配置"""
SECRET_KEY = os.environ.get('SECRET_KEY')
SQLALCHEMY_TRACK_MODIFICATIONS = False

# Session 安全
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = 'Lax'
PERMANENT_SESSION_LIFETIME = timedelta(hours=24)

class DevelopmentConfig(Config):
"""开发配置"""
DEBUG = True
SQLALCHEMY_DATABASE_URI = 'sqlite:///dev.db'
SQLALCHEMY_ECHO = True

class ProductionConfig(Config):
"""生产配置"""
DEBUG = False
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL')

# 数据库连接池
SQLALCHEMY_ENGINE_OPTIONS = {
'pool_size': 10,
'pool_recycle': 3600,
'pool_pre_ping': True,
'max_overflow': 5
}

class TestingConfig(Config):
"""测试配置"""
TESTING = True
SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:'
WTF_CSRF_ENABLED = False

config = {
'development': DevelopmentConfig,
'production': ProductionConfig,
'testing': TestingConfig
}

环境变量

使用 .env 文件管理敏感配置:

# .env(不要提交到版本控制)
SECRET_KEY=your-secret-key-here
DATABASE_URL=postgresql://user:password@localhost/myapp
REDIS_URL=redis://localhost:6379/0
MAIL_SERVER=smtp.example.com
MAIL_PORT=587
MAIL_USE_TLS=true
MAIL_USERNAME=[email protected]
MAIL_PASSWORD=your-email-password

加载环境变量:

from dotenv import load_dotenv
import os

load_dotenv()

app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY')
app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('DATABASE_URL')

安全配置检查清单

# security_check.py
import os

def check_production_security(app):
"""生产环境安全检查"""
errors = []

# 检查 SECRET_KEY
if not app.config.get('SECRET_KEY'):
errors.append('SECRET_KEY 未设置')
elif len(app.config['SECRET_KEY']) < 32:
errors.append('SECRET_KEY 长度不足(建议至少 32 字符)')

# 检查调试模式
if app.config.get('DEBUG'):
errors.append('生产环境不应启用 DEBUG 模式')

# 检查 Session 安全
if not app.config.get('SESSION_COOKIE_SECURE'):
errors.append('SESSION_COOKIE_SECURE 应为 True')

if not app.config.get('SESSION_COOKIE_HTTPONLY'):
errors.append('SESSION_COOKIE_HTTPONLY 应为 True')

# 检查数据库连接
if 'sqlite' in app.config.get('SQLALCHEMY_DATABASE_URI', ''):
errors.append('生产环境不建议使用 SQLite')

if errors:
for error in errors:
print(f'[安全警告] {error}')
return False

return True

Docker 部署

Dockerfile

# Dockerfile
FROM python:3.11-slim

# 设置环境变量
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
ENV FLASK_APP=run.py

# 设置工作目录
WORKDIR /app

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

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

# 复制应用代码
COPY . .

# 创建非 root 用户
RUN useradd -m myuser && chown -R myuser:myuser /app
USER myuser

# 暴露端口
EXPOSE 8000

# 健康检查
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" || exit 1

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

Docker Compose

# docker-compose.yml
version: '3.8'

services:
web:
build: .
restart: always
ports:
- "8000:8000"
environment:
- FLASK_ENV=production
- SECRET_KEY=${SECRET_KEY}
- DATABASE_URL=postgresql://user:password@db:5432/myapp
- REDIS_URL=redis://redis:6379/0
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
volumes:
- static_volume:/app/static
- media_volume:/app/uploads
networks:
- app-network

db:
image: postgres:15-alpine
restart: always
environment:
- POSTGRES_USER=user
- POSTGRES_PASSWORD=password
- POSTGRES_DB=myapp
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user -d myapp"]
interval: 10s
timeout: 5s
retries: 5
networks:
- app-network

redis:
image: redis:7-alpine
restart: always
volumes:
- redis_data:/data
networks:
- app-network

nginx:
image: nginx:alpine
restart: always
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- static_volume:/var/www/static:ro
- media_volume:/var/www/uploads:ro
- ./certs:/etc/nginx/certs:ro
depends_on:
- web
networks:
- app-network

volumes:
postgres_data:
redis_data:
static_volume:
media_volume:

networks:
app-network:
driver: bridge

多阶段构建

优化镜像大小:

# Dockerfile(多阶段构建)
# 构建阶段
FROM python:3.11 AS builder

WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir --target=/app/deps -r requirements.txt

# 运行阶段
FROM python:3.11-slim

WORKDIR /app

# 复制依赖
COPY --from=builder /app/deps /usr/local/lib/python3.11/site-packages

# 复制应用代码
COPY . .

RUN useradd -m myuser && chown -R myuser:myuser /app
USER myuser

EXPOSE 8000
CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:8000", "run:app"]

云平台部署

Heroku

# Procfile
web: gunicorn run:app

# runtime.txt
python-3.11.0

# requirements.txt 必须包含 gunicorn

# 部署命令
heroku create myapp
heroku config:set SECRET_KEY=your-secret-key
heroku config:set DATABASE_URL=your-database-url
git push heroku main

PythonAnywhere

# 创建虚拟环境
mkvirtualenv myapp --python=/usr/bin/python3.11
pip install -r requirements.txt

# WSGI 配置文件
import sys
import os

path = '/home/username/myapp'
if path not in sys.path:
sys.path.append(path)

os.environ['SECRET_KEY'] = 'your-secret-key'
os.environ['DATABASE_URL'] = 'your-database-url'

from run import app as application

AWS Elastic Beanstalk

# .ebextensions/python.config
option_settings:
aws:elasticbeanstalk:container:python:
WSGIPath: run:app
aws:elasticbeanstalk:application:environment:
SECRET_KEY: your-secret-key

# 部署
eb init -p python-3.11 myapp
eb create myapp-env
eb deploy

Google Cloud Run

# Dockerfile
FROM python:3.11-slim

WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

# Cloud Run 需要监听 PORT 环境变量
CMD exec gunicorn --bind :$PORT --workers 1 --threads 8 --timeout 0 run:app
# 部署
gcloud run deploy myapp --source . --platform managed --region us-central1

日志和监控

日志配置

# logging_config.py
import logging
from logging.handlers import RotatingFileHandler, SMTPHandler
import os

def setup_logging(app):
"""配置日志"""
if not app.debug and not app.testing:
# 文件日志
if not os.path.exists('logs'):
os.mkdir('logs')

file_handler = RotatingFileHandler(
'logs/app.log',
maxBytes=10 * 1024 * 1024, # 10MB
backupCount=10
)
file_handler.setFormatter(logging.Formatter(
'%(asctime)s %(levelname)s: %(message)s '
'[in %(pathname)s:%(lineno)d]'
))
file_handler.setLevel(logging.INFO)
app.logger.addHandler(file_handler)

# 邮件日志(仅错误)
if app.config.get('MAIL_SERVER'):
auth = None
if app.config.get('MAIL_USERNAME'):
auth = (app.config['MAIL_USERNAME'], app.config['MAIL_PASSWORD'])

mail_handler = SMTPHandler(
mailhost=(app.config['MAIL_SERVER'], app.config['MAIL_PORT']),
fromaddr=f"no-reply@{app.config['MAIL_SERVER']}",
toaddrs=app.config['ADMINS'],
subject='Application Error',
credentials=auth,
secure=()
)
mail_handler.setLevel(logging.ERROR)
app.logger.addHandler(mail_handler)

app.logger.setLevel(logging.INFO)
app.logger.info('Application startup')

结构化日志

import json
import logging

class JSONFormatter(logging.Formatter):
"""JSON 格式日志"""

def format(self, record):
log_data = {
'timestamp': self.formatTime(record),
'level': record.levelname,
'message': record.getMessage(),
'module': record.module,
'function': record.funcName,
'line': record.lineno
}

if hasattr(record, 'request_id'):
log_data['request_id'] = record.request_id

if record.exc_info:
log_data['exception'] = self.formatException(record.exc_info)

return json.dumps(log_data)

健康检查端点

@app.route('/health')
def health():
"""健康检查"""
return {'status': 'healthy'}, 200

@app.route('/ready')
def ready():
"""就绪检查"""
try:
# 检查数据库连接
db.session.execute('SELECT 1')
return {'status': 'ready'}, 200
except Exception as e:
return {'status': 'not ready', 'error': str(e)}, 503

性能优化

数据库优化

# 连接池配置
app.config['SQLALCHEMY_ENGINE_OPTIONS'] = {
'pool_size': 10, # 连接池大小
'pool_recycle': 3600, # 连接回收时间
'pool_pre_ping': True, # 连接健康检查
'max_overflow': 5 # 超出 pool_size 后的额外连接
}

# 查询优化
from sqlalchemy.orm import joinedload

# 预加载关系,避免 N+1 查询
posts = Post.query.options(joinedload(Post.author)).all()

缓存

from flask_caching import Cache

cache = Cache(app, config={
'CACHE_TYPE': 'redis',
'CACHE_REDIS_URL': app.config['REDIS_URL']
})

@app.route('/popular-posts')
@cache.cached(timeout=300) # 缓存 5 分钟
def popular_posts():
posts = Post.query.order_by(Post.views.desc()).limit(10).all()
return render_template('popular.html', posts=posts)

# 视图参数缓存
@app.route('/post/<int:id>')
@cache.cached(timeout=600, key_prefix=lambda: f'post_{request.view_args["id"]}')
def post_detail(id):
post = Post.query.get_or_404(id)
return render_template('post.html', post=post)

静态文件优化

# Nginx 配置
location /static {
alias /var/www/myapp/static;

# 启用 Gzip
gzip on;
gzip_types text/css application/javascript application/json;
gzip_min_length 1000;

# 缓存
expires 1y;
add_header Cache-Control "public, immutable";

# 防盗链
valid_referers none blocked example.com *.example.com;
if ($invalid_referer) {
return 403;
}
}

CI/CD 配置

GitHub Actions

# .github/workflows/deploy.yml
name: Deploy

on:
push:
branches: [main]

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3

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

- name: Install dependencies
run: |
pip install -r requirements.txt
pip install pytest pytest-cov

- name: Run tests
run: pytest --cov=app --cov-report=xml

- name: Upload coverage
uses: codecov/codecov-action@v3

deploy:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v3

- name: Deploy to server
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.HOST }}
username: ${{ secrets.USERNAME }}
key: ${{ secrets.SSH_KEY }}
script: |
cd /var/www/myapp
git pull origin main
source venv/bin/activate
pip install -r requirements.txt
flask db upgrade
sudo systemctl restart myapp

GitLab CI

# .gitlab-ci.yml
stages:
- test
- deploy

test:
stage: test
image: python:3.11
script:
- pip install -r requirements.txt
- pip install pytest pytest-cov
- pytest --cov=app
coverage: '/TOTAL.+?(\d+%)/'

deploy:
stage: deploy
image: alpine:latest
only:
- main
script:
- apk add openssh-client
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
- ssh $SSH_USER@$SSH_HOST "cd /var/www/myapp && git pull && sudo systemctl restart myapp"

部署检查清单

部署前

  • 设置强随机 SECRET_KEY
  • 禁用 DEBUG 模式
  • 配置生产数据库
  • 设置正确的 SESSION_COOKIE_SECURE
  • 配置 HTTPS 证书
  • 设置环境变量,不硬编码敏感信息
  • 运行数据库迁移
  • 收集静态文件

运行时

  • 配置日志记录
  • 设置错误通知
  • 配置健康检查
  • 设置监控告警
  • 配置备份策略
  • 设置自动重启

安全

  • 使用 HTTPS
  • 配置安全响应头
  • 设置请求大小限制
  • 配置速率限制
  • 启用防火墙
  • 定期更新依赖
# 安全响应头中间件
@app.after_request
def add_security_headers(response):
response.headers['X-Content-Type-Options'] = 'nosniff'
response.headers['X-Frame-Options'] = 'SAMEORIGIN'
response.headers['X-XSS-Protection'] = '1; mode=block'
response.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains'
return response

小结

本章详细介绍了 Flask 应用的生产部署:

  1. WSGI 架构:理解 HTTP 服务器和 WSGI 服务器的职责
  2. 服务器选择:Gunicorn、uWSGI、Waitress 的对比和配置
  3. 反向代理:Nginx 和 Apache 的配置
  4. 安全配置:环境变量、Session 安全、HTTPS
  5. Docker 部署:Dockerfile 和 Docker Compose
  6. 云平台:Heroku、AWS、GCP 等部署选项
  7. 监控运维:日志、健康检查、性能优化
  8. CI/CD:自动化测试和部署流程

正确的部署配置是保障应用稳定运行的基础,请务必遵循安全最佳实践。

参考资料