Gunicorn and Nginx: Production environment web server configuration

📂 Stage: Stage 6 - Online Deployment (Production) 🔗 Related chapters: Docker 部署全流程 · 环境变量与安全配置

The Flask project management backend that I have been working on for three months is finally going online! Have been using it beforeflask runSelf-test, as long as you open a few more tabs to simulate concurrency, the page will start to freeze, and occasionally it will directly crash with 502. After searching around, I found that Gunicorn multi-process processing dynamic requests + Nginx reverse proxy takes over static files, SSL and load balancing entrance, which is the "golden entry combination" for Python Web production deployment. Today we will build a complete solution with logs, daemons, and security configurations.


1. Why give up?flask run, choose Gunicorn?

First look at the capabilities boundaries of the two, and you will understand why the built-in server cannot be used in the production environment:

Function/FeaturesFlask built-inflask runGunicorn (Green Unicorn)
Applicable scenariosLocal development and debugging onlyIndustrial-grade Python WSGI production container
Concurrency modelSingle thread + single processMultiple Workers, supports sync/gevent/uvicorn
High concurrency processing❌ If there are more than 10 requests, it will freeze or get 503✅ The number of workers can be adjusted as needed to easily handle hundreds of concurrency
Stability❌ Will not automatically restart after crash✅ Can cooperate with systemd/supervisor to automatically restart
Deployment restrictions❌ Ports cannot be exposed publicly✅ Securely deploy on local ports, leaving only Nginx to the outside world

Simply put:flask runIt is a toy car for developers to use for debugging. The Gunicorn is a real high-speed racing car. Coupled with a professional "dispatcher" Nginx, each type of request can be processed efficiently.


2. Installation and basic configuration of Gunicorn

2.1 Environment preparation

Please make sure your project is running in a virtual environment to avoid global dependency pollution. Install directly after activating the virtual environment:

# 激活虚拟环境后执行
(venv)$ pip install gunicorn

2.2 Startup mode

Gunicorn supports two startup methods: command line parameter passing (suitable for temporary testing) and configuration file (recommended, more standardized and easier to maintain).

Command line quick start

Suitable for first attempts and quick verification:

# 假设 app.py 是入口文件,app 是 Flask 实例变量
(venv)$ gunicorn -w 4 -b 127.0.0.1:8000 app:app

# 如果使用了“应用工厂模式”(生产环境推荐),需要用引号包裹工厂调用
(venv)$ gunicorn -w 4 -b 127.0.0.1:8000 "app:create_app('production')"

📌 Parameter quick check -w 4: Start 4 Worker processes (the golden formula will be introduced later) -b 127.0.0.1:8000: Bind the local port 8000, do not directly expose it to the public network, and leave Nginx as the entrance

Configuration file startup (required for production)

Create in the project root directorygunicorn.conf.py, all configurations are managed centrally:

# gunicorn.conf.py
import multiprocessing

# ============== 核心配置 ==============
# 绑定地址/端口:本地 8000,不对外暴露
bind = "127.0.0.1:8000"
# Worker 数量:CPU核心数×2+1(IO 密集型项目可适当增加,CPU 密集型不要超太多)
workers = multiprocessing.cpu_count() * 2 + 1
# Worker 类型:默认 sync(纯 CRUD 接口);可选用 gevent(IO密集型,需单独安装);uvicorn(ASGI异步)
worker_class = "sync"
# 超时时间:Worker 超过 30s 没响应,自动重启避免死锁
timeout = 30
# 优雅退出时间:重启时给 Worker 30s 处理完已有请求
graceful_timeout = 30
# 连接保持时间:配合 Nginx 的 keepalive
keepalive = 65
# 最大请求数:Worker 处理 1000 次请求后自动重启(预防内存泄漏)
max_requests = 1000
# 随机抖动数:避免所有 Worker 同时重启
max_requests_jitter = 50

# ============== 日志配置 ==============
# "-" 表示输出到标准输出/错误,方便配合 systemd 的 journalctl 查看
accesslog = "-"
errorlog = "-"
loglevel = "info"
# 自定义访问日志格式(保留 IP、时间、请求路径、状态码、Referer 等关键信息)
access_log_format = '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"'

# ============== 钩子函数(可选) ==============
def on_starting(server):
    print("🚀 Gunicorn 正在启动...")

def on_exit(server):
    print("👋 Gunicorn 正在优雅退出...")

Start using the configuration file:

(venv)$ gunicorn -c gunicorn.conf.py "app:create_app('production')"

3. Nginx reverse proxy configuration

Gunicorn has no problem handling dynamic requests, but it is too reluctant to let it be responsible for SSL encryption, static file serving, and Gzip compression at the same time - these professional things are left to Nginx.

3.1 Preparation

  1. Make sure the server has Nginx installed
    • Ubuntu/Debian:apt install nginx
    • CentOS:yum install nginx
  2. Prepare domain name and SSL certificate (recommend free Let's Encrypt, passcertbotAutomatic application)
  3. Put static files (CSS/JS/images) and media files (uploaded by users) into a directory accessible to Nginx, for example/var/www/daoman/

3.2 Configuration file

create/etc/nginx/conf.d/daoman.conf(Independent configuration for easy maintenance, do not modify it directlynginx.conf):

# /etc/nginx/conf.d/daoman.conf

# 1. 定义 Gunicorn 上游服务器(后续若扩展多 Worker 节点,直接添加 server 即可)
upstream daoman_app {
    server 127.0.0.1:8000;
    keepalive 32;   # 保持 32 个长连接,降低握手开销
}

# 2. HTTP 请求强制跳转 HTTPS(生产环境必须!)
server {
    listen 80;
    listen [::]:80;
    server_name daomanpy.com www.daomanpy.com;

    # 将所有 HTTP 流量永久重定向到 HTTPS
    return 301 https://$host$request_uri;
}

# 3. HTTPS 主服务配置
server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name daomanpy.com www.daomanpy.com;

    # ============== SSL 证书配置(certbot 会自动生成这一段) ==============
    ssl_certificate /etc/letsencrypt/live/daomanpy.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/daomanpy.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;   # 禁用不安全的 TLSv1.0/1.1
    ssl_prefer_server_ciphers off;
    ssl_session_cache shared:SSL:10m;   # 缓存 SSL 会话,减少握手时间
    ssl_session_timeout 1d;

    # ============== 安全响应头(防 XSS、点击劫持、嗅探攻击) ==============
    add_header X-Frame-Options "SAMEORIGIN" 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;

    # ============== Gzip 压缩(降低带宽消耗、提升加载速度) ==============
    gzip on;
    gzip_vary on;
    gzip_min_length 1024;   # 小于 1KB 不压缩
    gzip_types text/plain text/css text/xml application/json
               application/javascript application/xml+rss
               application/atom+xml image/svg+xml;

    # ============== 静态/媒体文件服务(Nginx 直接处理,完全不经过 Gunicorn) ==============
    location /static/ {
        alias /var/www/daoman/static/;
        expires 30d;                      # 缓存 30 天
        add_header Cache-Control "public, immutable";
    }

    location /media/ {
        alias /var/www/daoman/media/;
        expires 7d;                       # 缓存 7 天
        add_header Cache-Control "public";
    }

    # ============== 动态请求代理到 Gunicorn ==============
    location / {
        proxy_pass http://daoman_app;     # 转发到上游服务器
        proxy_http_version 1.1;           # 必须用 1.1 才能支持 keepalive
        proxy_set_header Connection "";   # 清空 Connection 头,复用长连接
        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_redirect off;

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

        # 允许上传的最大文件大小(默认 1MB 可能不够)
        client_max_body_size 20M;
    }
}

💡 Let certbot automatically manage SSL certificates When configuring for the first time, comment out thelisten 443The relevant lines, to tell Nginx to only handle HTTP, then runcertbot --nginx -d daomanpy.com -d www.daomanpy.com, it will automatically apply for a certificate and generate SSL configuration for you.

3.3 Test and reload Nginx

# 1. 检查配置文件语法
sudo nginx -t

# 2. 语法无误则重载(不会中断已有连接)
sudo nginx -s reload

4. Systemd daemon management

Gunicorn commands must not be run manually in a production environment. Once the process crashes or the server is restarted, the service will be gone. Use systemd (the service manager that comes with Linux) to protect it.

4.1 Create service file

create/etc/systemd/system/daoman.service

# /etc/systemd/system/daoman.service
[Unit]
Description=Daoman Flask 项目管理后台
# 启动顺序:在网络、PostgreSQL、Redis 之后启动(若不需要数据库/缓存,可删除对应服务)
After=network.target postgresql.service redis.service

[Service]
Type=notify
# 运行用户/组:使用 www-data(Nginx 默认用户),避免权限问题
User=www-data
Group=www-data
# 项目根目录
WorkingDirectory=/opt/daoman
# 虚拟环境路径及生产环境变量
Environment="PATH=/opt/daoman/venv/bin"
Environment="FLASK_ENV=production"
# 其他敏感环境变量建议通过 .env 加载,不要直接写在这里

# 启动命令(必须使用绝对路径)
ExecStart=/opt/daoman/venv/bin/gunicorn \
    -c /opt/daoman/gunicorn.conf.py \
    "app:create_app('production')"

# 重载命令(优雅重启,不中断正在处理的请求)
ExecReload=/bin/kill -s HUP $MAINPID
TimeoutStopSec=5
PrivateTmp=true
# 自动重启策略:异常退出后立即重启
Restart=always
RestartSec=10

[Install]
# 设置开机自启
WantedBy=multi-user.target

4.2 Activation and Management Services

# 重新加载 systemd 配置(每次修改 .service 文件后都需要执行)
sudo systemctl daemon-reload

# 启动并设置开机自启
sudo systemctl enable --now daoman

# 查看服务状态
sudo systemctl status daoman

# 重启服务(例如发布新代码后)
sudo systemctl restart daoman

# 优雅重载(仅修改配置,无需重启进程)
sudo systemctl reload daoman

# 实时查看日志(排查问题的神器)
sudo journalctl -u daoman -f

5. Summary of production deployment architecture and best practices

Complete architecture diagram

graph LR
    A[用户] -->|HTTPS请求| B[Nginx]
    B -->|静态/媒体文件| C[直接返回]
    B -->|动态请求| D[Gunicorn]
    D -->|读写数据| E[(PostgreSQL)]
    D -->|缓存/会话| F[(Redis)]

3 Key Best Practices

  1. How to determine the number of **Workers? ** Pure CRUD interface buttonCPU 核心数 × 2 + 1That’s it; if it is IO-intensive (crawler background, frequent calls to third-party APIs), you can zoom in to核心数 × 4, but don't overdo it - the overhead of process switching can also reduce performance.

  2. Safety first, guard from the entrance

  • Force HTTPS, leave no HTTP port
  • Gunicorn only binds127.0.0.1, never exposed to the outside world
  • Set the static/media file directory permissions towww-data:www-data, do not use 777
  • Sensitive information (database password, SECRET_KEY, etc.) passes.envor systemd injection, never write code
  1. Seamless publishing process Every time you release a new version, just:
    git pull                    # 拉取最新代码
    sudo systemctl reload daoman   # 优雅重载 Gunicorn,用户无感知

The entire process will not interrupt the request being processed and truly achieves hot update.


🔗 Extended reading