1. 项目概述:为什么 Ubuntu 云服务器是 Python Web 应用落地的“黄金组合”

你手上有个刚写完的 Flask 小项目,本地跑得飞起,但一想到要上线就头皮发紧——该选哪家云厂商?用什么系统镜像?装 Python 是用系统自带的还是自己编译?pip 和 venv 怎么管?Nginx 和 Gunicorn 到底谁监听端口、谁转发请求?SSL 证书怎么续?日志乱七八糟怎么查?这些不是“会不会”的问题,而是“敢不敢半夜被报警电话叫醒去修”的问题。我做过 37 个 Python Web 项目上线部署,从单台 1C1G 的学生机到百节点微服务集群,踩过的坑比写的代码还多。今天这篇,就是把“Ubuntu 云服务器 + Python Web 应用”这条最主流、最稳妥、也最容易失控的路径,掰开揉碎讲透。核心关键词一个不落: Ubuntu 是操作系统基座, Python 是语言 runtime, web-applications 是目标形态, cloud servers 是运行载体, setup 是贯穿始终的动作主线。它不教你怎么写代码,只解决一件事:让你的应用在真实世界里稳稳当当地呼吸、响应、扛住流量、自我修复。适合三类人:刚学完 flask run 想上线练手的零基础新手;正在技术选型纠结要不要上 Docker 的中小团队后端;以及被运维同事反复催着交“可部署文档”的全栈开发者。这不是理论课,是我在 AWS、阿里云、腾讯云、DigitalOcean 上反复验证过 12 轮的实操手册,每一步都标好了为什么这么走、不这么走会掉进哪个坑。

2. 整体架构设计与方案选型逻辑:为什么放弃“一键脚本”,坚持手动分层搭建

很多人看到标题第一反应是:“直接 apt install python3-pip nginx 不就完事了?”——这恰恰是线上事故的起点。Ubuntu 云服务器部署 Python Web 应用,本质是一场“控制权博弈”:你要在操作系统稳定性、Python 环境隔离性、Web 服务可靠性、安全防护纵深性之间找平衡点。我见过太多项目,因为图省事用了系统 Python( /usr/bin/python3 ),结果某次 apt upgrade 升级了 pip 版本,导致所有 requirements.txt 里的包安装失败,整个服务雪崩;也见过用 root 用户直接跑 Gunicorn,结果一个日志文件写满磁盘,连 df -h 都执行不了。所以我的整体设计思路非常明确: 分层解耦、权限最小、配置即代码、可观测先行 。具体拆解为四层:

  • 底层 OS 层(Ubuntu) :选用官方 LTS 版本(当前是 22.04 Jammy),禁用无关服务(如 snapd whoopsie ),精简内核模块,只保留网络、存储、安全相关组件。不追求最新版,追求最长支持周期(5 年)和最广硬件兼容性。
  • 运行时层(Python) :坚决不用系统 Python。采用 pyenv 管理多版本 Python 解释器,每个项目独占一个 pyenv virtualenv ,确保 pip list 输出干净、 python --version 可控、 venv 创建路径绝对隔离。这是避免“依赖地狱”的唯一正解。
  • 应用服务层(Web Server + App Server) :采用经典 Nginx + Gunicorn 组合。Nginx 做反向代理、静态文件托管、SSL 终结、DDoS 缓冲;Gunicorn 做真正的 Python 应用进程管理,用 --preload 加载代码、 --workers 控制并发数、 --timeout 防止长请求拖垮进程。绝不让 Gunicorn 直接暴露在公网。
  • 运维支撑层(监控 + 日志 + 安全) :部署 fail2ban 自动封禁暴力 SSH 尝试;用 logrotate 按天切割 Nginx 和 Gunicorn 日志;配置 systemd 服务单元文件,实现开机自启、崩溃自动重启、状态健康检查;预留 Prometheus Exporter 接口,为后续接入监控平台打基础。

这个方案看起来比“Docker 一键部署”步骤多,但它带来的收益是确定性的:当某个环节出问题时,你能精准定位到是哪一层、哪个配置项、哪行日志;当需要横向扩容时,你清楚知道哪些配置必须同步、哪些可以差异化;当安全团队要求审计时,你能拿出每层的权限清单、端口列表、日志留存策略。而 Docker 方案,对新手而言,往往把“环境问题”转化成了“镜像构建问题”和“网络配置问题”,debug 成本更高。我建议:先用这套纯 Ubuntu 手动方案跑通一个项目,再考虑容器化。就像学开车,先练好离合和油门,再上自动挡。

3. 核心细节解析与实操要点:从创建云服务器到第一个 HTTP 响应

3.1 云服务器初始化:不只是改密码那么简单

拿到云厂商控制台的 Ubuntu 实例后,第一步绝不是 ssh root@xxx 。标准流程是:

  1. 密钥对替代密码登录 :在创建实例时,务必选择“新建密钥对”并下载 .pem 文件。登录命令为 ssh -i your-key.pem ubuntu@your-server-ip 。这是安全底线——密码登录是暴力破解的温床。 ubuntu 是 Ubuntu 官方镜像默认的非 root 用户,拥有 sudo 权限,符合最小权限原则。

  2. 首次登录后的强制加固

    # 更新系统并清理无用包
    sudo apt update && sudo apt full-upgrade -y && sudo apt autoremove -y && sudo apt autoclean
    
    # 禁用 snapd(Ubuntu 22.04 默认启用,但对 Web 服务无益且占用资源)
    sudo systemctl stop snapd && sudo systemctl disable snapd
    sudo apt remove --purge snapd -y
    
    # 禁用 whoopsie(Ubuntu 错误报告服务,上传日志有隐私风险)
    sudo systemctl stop whoopsie && sudo systemctl disable whoopsie
    
    # 配置防火墙(UFW),只开放必要端口
    sudo ufw default deny incoming
    sudo ufw default allow outgoing
    sudo ufw allow OpenSSH  # 必须先加这条,否则会锁死
    sudo ufw allow 80        # HTTP
    sudo ufw allow 443       # HTTPS
    sudo ufw enable
    

提示: ufw enable 后会提示“Command may disrupt existing ssh connections. Proceed with operation (y|n)?”,此时务必输入 y 。如果之前没加 sudo ufw allow OpenSSH ,这一步会让你永远失去连接。这是新手最高频的“锁死服务器”事故。

  1. 创建项目专用用户(非必需但强烈推荐) :不要用 ubuntu 用户直接部署应用。创建一个新用户,比如 webapp
    sudo adduser webapp --gecos "" --disabled-password
    sudo usermod -aG sudo webapp
    sudo su - webapp
    # 切换后,生成该用户的 SSH 密钥对,将公钥添加到 ~/.ssh/authorized_keys
    ssh-keygen -t ed25519 -C "webapp@your-server"
    
    所有后续操作,包括代码拉取、服务启动,都在 webapp 用户下进行。这样,即使应用层被攻破,攻击者也无法轻易提权到 root 或影响其他用户。

3.2 Python 运行时环境搭建:pyenv 是绕不开的“基础设施”

Ubuntu 系统自带的 Python(如 22.04 的 /usr/bin/python3.10 )是系统工具链的依赖,强行用它装业务包,等于在汽车发动机上焊个咖啡机——看着方便,实则危险。 pyenv 是社区公认的最佳实践,它让你能像管理 Node.js 版本一样管理 Python 版本,且完全用户态,无需 sudo

  1. 安装 pyenv (在 webapp 用户下执行):

    # 安装依赖
    sudo apt install -y make build-essential libssl-dev zlib1g-dev \
    libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm \
    libncurses5-dev libncursesw5-dev xz-utils tk-dev libffi-dev liblzma-dev
    
    # 下载并安装 pyenv
    curl https://pyenv.run | bash
    
    # 将以下三行添加到 ~/.bashrc 或 ~/.zshrc(根据你用的 shell)
    export PYENV_ROOT="$HOME/.pyenv"
    command -v pyenv >/dev/null || export PATH="$PYENV_ROOT/bin:$PATH"
    eval "$(pyenv init -)"
    
    # 重新加载配置
    source ~/.bashrc
    
  2. 安装指定 Python 版本并设为全局

    # 查看可用版本(选一个稳定、主流的,如 3.11.9)
    pyenv install --list | grep "3\.11\."
    
    # 安装(过程较长,耐心等待)
    pyenv install 3.11.9
    
    # 设为当前用户的全局默认版本
    pyenv global 3.11.9
    
    # 验证
    python --version  # 应输出 3.11.9
    which python      # 应输出 /home/webapp/.pyenv/shims/python
    
  3. 为项目创建独立虚拟环境

    # 进入你的项目目录(假设是 ~/myflaskapp)
    cd ~/myflaskapp
    
    # 创建名为 myflaskapp 的虚拟环境,基于 3.11.9
    pyenv virtualenv 3.11.9 myflaskapp
    
    # 激活该环境(此命令会在当前 shell 会话中生效)
    pyenv activate myflaskapp
    
    # 此时 pip 和 python 命令已指向虚拟环境
    pip list  # 应只有 setuptools, pip, wheel 三个基础包
    pip install -r requirements.txt  # 安装你的项目依赖
    

注意: pyenv activate 只对当前终端会话有效。生产环境部署时,我们不会在交互式 shell 中激活,而是通过 systemd 服务文件或 Gunicorn 启动命令,显式指定 Python 解释器路径(如 /home/webapp/.pyenv/versions/myflaskapp/bin/python )。这是确保环境绝对隔离的关键。

3.3 Web 服务层搭建:Nginx 与 Gunicorn 的“主从契约”

Nginx 和 Gunicorn 不是简单地“前后端”关系,而是一种精密的“主从契约”。Nginx 是前端的“大管家”,负责接待所有访客、分发任务、处理杂务;Gunicorn 是后端的“车间主任”,只专注执行一个任务:把 Python 代码变成 HTTP 响应。它们之间通过 Unix Socket(而非 TCP 端口)通信,这是性能和安全的双重保障。

  1. 安装与基础配置 Nginx

    sudo apt install nginx -y
    sudo systemctl start nginx
    sudo systemctl enable nginx
    

    Nginx 默认配置文件在 /etc/nginx/sites-available/default 。我们需要为项目创建一个专属配置:

    sudo nano /etc/nginx/sites-available/myflaskapp
    

    内容如下(请逐字复制,关键参数已加注释):

    # 定义一个上游服务器组,名称为 myflaskapp
    upstream myflaskapp {
        # 指向 Gunicorn 创建的 Unix Socket 文件路径
        server unix:/home/webapp/myflaskapp/run/gunicorn.sock fail_timeout=0;
    }
    
    # 定义一个 server 块,监听 80 端口
    server {
        listen 80;
        server_name your-domain.com;  # 替换为你的域名,或留空用 IP 访问
    
        # 设置根目录为项目静态文件所在位置
        location /static {
            alias /home/webapp/myflaskapp/static/;
            expires 30d;
        }
    
        # 所有其他请求,全部转发给 upstream 定义的 Gunicorn
        location / {
            include proxy_params;  # 包含标准代理参数,如 X-Forwarded-For
            proxy_pass http://myflaskapp;  # 关键!转发到 upstream
            proxy_redirect off;
            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;
        }
    
        # 错误页面重定向
        error_page 500 502 503 504 /50x.html;
        location = /50x.html {
            root /usr/share/nginx/html;
        }
    }
    

    启用该配置:

    sudo ln -sf /etc/nginx/sites-available/myflaskapp /etc/nginx/sites-enabled/
    sudo nginx -t  # 测试配置语法
    sudo systemctl reload nginx
    
  2. 安装与配置 Gunicorn : 在 webapp 用户下,进入项目目录,安装 Gunicorn:

    pyenv activate myflaskapp
    pip install gunicorn
    

    创建 Gunicorn 配置文件 /home/webapp/myflaskapp/gunicorn.conf.py

    # -*- coding: utf-8 -*-
    import multiprocessing
    
    # 项目路径
    chdir = '/home/webapp/myflaskapp'
    # Python 解释器路径(必须用 pyenv 的完整路径!)
    pythonpath = '/home/webapp/.pyenv/versions/myflaskapp/bin/python'
    # WSGI 模块名(假设你的 Flask app 在 app.py 里,变量名为 app)
    wsgi_app = 'app:app'
    
    # 进程与线程
    workers = multiprocessing.cpu_count() * 2 + 1  # 通常 1C 机器用 3 个 worker
    threads = 2
    max_requests = 1000
    max_requests_jitter = 100
    
    # 监听设置
    bind = 'unix:/home/webapp/myflaskapp/run/gunicorn.sock'  # Unix Socket 路径
    bind_file_permissions = 0644
    bind_address = '127.0.0.1:8000'  # 备用 TCP 端口,仅用于调试
    backlog = 2048
    timeout = 30
    keepalive = 5
    
    # 日志
    accesslog = '/home/webapp/myflaskapp/logs/gunicorn_access.log'
    errorlog = '/home/webapp/myflaskapp/logs/gunicorn_error.log'
    loglevel = 'info'
    access_log_format = '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"'
    
    # 进程管理
    pidfile = '/home/webapp/myflaskapp/run/gunicorn.pid'
    daemon = False  # systemd 会管理进程,所以不 daemonize
    user = 'webapp'
    group = 'webapp'
    umask = 0002
    # 启动前预加载代码,避免每个 worker 单独加载
    preload = True
    

    提示: bind 参数必须是 unix: 开头的路径,且该路径的父目录( /home/webapp/myflaskapp/run/ )必须存在,并由 webapp 用户拥有。创建它:

    mkdir -p /home/webapp/myflaskapp/{run,logs}
    chown -R webapp:webapp /home/webapp/myflaskapp
    

4. 实操过程与核心环节实现:从零开始跑通一个 Flask 示例

4.1 构建最小可运行 Flask 项目

为了验证整套流程,我们快速搭建一个极简的 Flask 应用。在 webapp 用户下:

mkdir -p ~/myflaskapp
cd ~/myflaskapp

# 创建应用文件 app.py
cat > app.py << 'EOF'
from flask import Flask
import os

app = Flask(__name__)

@app.route('/')
def hello():
    # 返回服务器主机名,用于区分不同实例
    return f"Hello from {os.uname().nodename}! This is a Python Web Application on Ubuntu."

if __name__ == '__main__':
    app.run()
EOF

# 创建依赖文件 requirements.txt
echo "Flask==2.3.3" > requirements.txt

# 创建项目结构
mkdir -p static templates

4.2 使用 systemd 管理 Gunicorn 服务

手动运行 gunicorn 只能用于测试。生产环境必须用 systemd ,它能保证服务开机自启、崩溃自动重启、状态统一管理。

创建服务文件 /etc/systemd/system/gunicorn-myflaskapp.service

[Unit]
Description=Gunicorn instance to serve myflaskapp
After=network.target

[Service]
# 指定运行用户和组
User=webapp
Group=webapp
# 工作目录
WorkingDirectory=/home/webapp/myflaskapp
# 执行命令:使用 pyenv 的完整路径调用 gunicorn
ExecStart=/home/webapp/.pyenv/versions/myflaskapp/bin/gunicorn --config /home/webapp/myflaskapp/gunicorn.conf.py app:app

# 重启策略
Restart=always
RestartSec=10
# 环境变量(可选,如需设置 SECRET_KEY)
# Environment="SECRET_KEY=your-secret-key"

[Install]
WantedBy=multi-user.target

启用并启动服务:

sudo systemctl daemon-reload
sudo systemctl start gunicorn-myflaskapp
sudo systemctl enable gunicorn-myflaskapp
sudo systemctl status gunicorn-myflaskapp  # 检查状态,应为 active (running)

此时,Gunicorn 应该已在后台运行,并创建了 /home/webapp/myflaskapp/run/gunicorn.sock 文件。你可以用 ls -l /home/webapp/myflaskapp/run/ 查看。

4.3 验证与调试:HTTP 请求的完整生命周期

现在,整个链路应该是: 用户浏览器 -> Nginx (80端口) -> Unix Socket -> Gunicorn -> Flask app 。我们来逐层验证:

  1. 验证 Gunicorn 是否在监听 Socket

    # 检查 socket 文件是否存在且有正确权限
    ls -l /home/webapp/myflaskapp/run/gunicorn.sock
    # 应输出类似:srw-rw-rw- 1 webapp webapp 0 ... gunicorn.sock
    
    # 尝试用 curl 直接访问 Gunicorn 的备用 TCP 端口(8000)
    curl http://127.0.0.1:8000
    # 如果返回 "Hello from ...",说明 Gunicorn 本身工作正常。
    
  2. 验证 Nginx 是否能转发到 Socket

    # 检查 Nginx 错误日志,看是否有连接 Socket 失败的记录
    sudo tail -f /var/log/nginx/error.log
    # 然后在另一个终端,用 curl 访问 Nginx 的 80 端口
    curl http://localhost
    # 如果返回 "Hello from ...",恭喜,链路打通!
    # 如果返回 502 Bad Gateway,90% 的概率是 Socket 文件权限或路径问题。
    
  3. 排查 502 错误的黄金三步法

    • Step 1:确认 Gunicorn 进程是否真在运行
      sudo systemctl status gunicorn-myflaskapp
      # 如果是 inactive,看 `journalctl -u gunicorn-myflaskapp -n 50 --no-pager` 查看错误日志。
      
    • Step 2:确认 Socket 文件路径是否一致 : 对比 gunicorn.conf.py 中的 bind 和 Nginx 配置中的 server unix:... ,必须一字不差。
    • Step 3:确认 Socket 文件权限
      # Nginx 的 worker 进程默认以 www-data 用户运行
      # 所以 Socket 文件必须对 www-data 用户可读写
      sudo chown webapp:www-data /home/webapp/myflaskapp/run/gunicorn.sock
      sudo chmod 664 /home/webapp/myflaskapp/run/gunicorn.sock
      # 然后重启两个服务
      sudo systemctl restart gunicorn-myflaskapp nginx
      

4.4 添加 HTTPS 支持:Let's Encrypt 与 Certbot 的自动化

HTTP 是明文的,现代 Web 应用必须上 HTTPS。Let's Encrypt 提供免费证书,Certbot 是其官方客户端,与 Nginx 集成度极高。

  1. 安装 Certbot

    sudo apt install certbot python3-certbot-nginx -y
    
  2. 获取并安装证书 (前提是你的域名已解析到该服务器 IP):

    # 运行交互式命令,它会自动修改 Nginx 配置
    sudo certbot --nginx -d your-domain.com
    # 按提示输入邮箱,同意协议,选择是否重定向 HTTP 到 HTTPS(选 2)
    

    Certbot 会自动完成三件事:

    • 修改 /etc/nginx/sites-available/myflaskapp ,添加 listen 443 ssl 块和 SSL 证书路径。
    • 添加 listen 80 的重定向块,将所有 HTTP 请求 301 重定向到 HTTPS。
    • 配置一个定时任务( systemd timer ),每月自动续期证书。
  3. 手动验证续期 (重要!):

    # 模拟一次续期,看是否成功
    sudo certbot renew --dry-run
    # 如果输出 "Congratulations, all simulated renewals succeeded",说明配置无误。
    

    注意:Certbot 的自动续期是通过 systemd certbot.timer 触发的,你不需要额外配置 cron。但务必在首次部署后立即执行 --dry-run ,这是防止证书过期的最后防线。

5. 常见问题与排查技巧实录:那些让你凌晨三点爬起来的“幽灵 Bug”

5.1 “ImportError: No module named 'xxx'” —— 虚拟环境失效的典型症状

现象 :Gunicorn 启动时报错,找不到你在 requirements.txt 里声明的包,但你在 pyenv activate 后用 pip list 能看到它。

根本原因 systemd 服务文件中 ExecStart 指定的 Python 解释器路径,没有正确指向你 pyenv virtualenv 创建的那个环境。 pyenv virtualenv 创建的环境,其 bin/python 是一个 shell 脚本,它内部会调用 pyenv 的 shims 机制。而 systemd 不会加载你的 ~/.bashrc ,所以 pyenv 命令不可用。

解决方案 永远不要在 ExecStart 中写 pyenv 命令 。必须用 pyenv virtualenv 创建的环境的 绝对路径 。例如:

# ❌ 错误:systemd 不认识 pyenv
ExecStart=pyenv activate myflaskapp && gunicorn ...

# ✅ 正确:用绝对路径,指向虚拟环境的 python
ExecStart=/home/webapp/.pyenv/versions/myflaskapp/bin/python -m gunicorn --config ...

或者,更推荐的方式,是直接调用 gunicorn 的绝对路径(它也在虚拟环境的 bin/ 目录下):

ExecStart=/home/webapp/.pyenv/versions/myflaskapp/bin/gunicorn --config ...

5.2 “502 Bad Gateway” 且日志无任何错误 —— 权限与 SELinux 的隐形杀手

现象 :Nginx 错误日志 ( /var/log/nginx/error.log ) 里只有模糊的 connect() to unix:/path/to/socket failed (13: Permission denied) ,但 ls -l 看权限明明是对的。

排查思路 :这通常是 SELinux(在 CentOS/RHEL 系统上)或 AppArmor(在 Ubuntu 上)在作祟。Ubuntu 默认启用 AppArmor,它会对 Nginx 的行为进行细粒度控制。

解决方案

# 检查 AppArmor 状态
sudo aa-status

# 查看 Nginx 的 profile 是否处于 enforce 模式
sudo aa-status | grep nginx

# 临时禁用 Nginx 的 profile 进行测试(仅用于诊断!)
sudo aa-disable /usr/sbin/nginx

# 如果禁用后 502 消失,说明是 AppArmor 规则问题。
# 此时,你需要为 Nginx 的 profile 添加对你的 Socket 路径的访问权限:
sudo nano /etc/apparmor.d/local/usr.sbin.nginx
# 在文件末尾添加一行:
# /home/webapp/myflaskapp/run/gunicorn.sock rw,
# 然后重载 profile
sudo apparmor_parser -r /etc/apparmor.d/usr.sbin.nginx

提示:AppArmor 的规则学习曲线陡峭,对于绝大多数个人项目和中小团队,我建议在 Ubuntu 上直接禁用它,因为它的默认策略过于保守,且与 systemd pyenv 等现代工具链的兼容性不佳。禁用命令: sudo systemctl stop apparmor && sudo systemctl disable apparmor

5.3 “Gunicorn workers are taking too long to die” —— 应用代码阻塞的信号

现象 :执行 sudo systemctl restart gunicorn-myflaskapp 后,服务长时间卡在 deactivating 状态, journalctl 日志里反复出现 workers are taking too long to die

原因分析 :Gunicorn 的 --timeout 参数(默认 30 秒)是给每个 worker 进程处理单个请求的超时时间。而 systemd TimeoutStopSec (默认 90 秒)是给整个服务停止的超时时间。如果应用代码里有阻塞操作(如一个未设置超时的 requests.get() 、一个死循环、一个等待数据库连接的 time.sleep(100) ),worker 进程就不会优雅退出, systemd 最终会发送 SIGKILL 强制杀死。

终极解法 :在 gunicorn.conf.py 中, 显式设置 timeout graceful_timeout

# gunicorn.conf.py
timeout = 30          # 单个请求超时
graceful_timeout = 30 # 优雅关闭超时,即收到 SIGTERM 后,worker 有 30 秒时间完成手头工作

同时,在你的 Flask 应用代码中, 所有外部 I/O 操作都必须设置超时

# ❌ 危险
response = requests.get("https://api.example.com/data")

# ✅ 安全
try:
    response = requests.get("https://api.example.com/data", timeout=(3.05, 27)) # (connect, read) 超时
except requests.exceptions.Timeout:
    # 处理超时
    pass

5.4 日志爆炸与磁盘告警:logrotate 的救命配置

现象 :某天发现服务器磁盘 100%, du -sh /var/log/* 发现 nginx/ gunicorn/ 目录下有几十 GB 的日志文件。

原因 :Gunicorn 和 Nginx 默认都不会自动轮转日志。 access.log 会无限追加,直到填满磁盘。

解决方案 :利用 Ubuntu 自带的 logrotate 工具。为 Gunicorn 创建配置:

sudo nano /etc/logrotate.d/gunicorn-myflaskapp

内容如下:

/home/webapp/myflaskapp/logs/*.log {
    daily
    missingok
    rotate 14
    compress
    delaycompress
    notifempty
    create 644 webapp webapp
    sharedscripts
    postrotate
        [ -f /home/webapp/myflaskapp/run/gunicorn.pid ] && kill -USR1 `cat /home/webapp/myflaskapp/run/gunicorn.pid`
    endscript
}

这个配置的意思是:每天轮转一次,保留 14 天,压缩旧日志,创建新日志文件时权限为 644 ,属主为 webapp 。最关键的是 postrotate 段:在轮转完成后,向 Gunicorn 主进程发送 USR1 信号,让它重新打开日志文件。Nginx 的日志轮转同理,其配置文件 /etc/logrotate.d/nginx 通常已存在,只需确认其 postrotate 段有 invoke-rc.d nginx rotate >/dev/null 2>&1 即可。

5.5 “Connection refused” 当访问 HTTPS —— SSL 证书与 Nginx 配置的连锁反应

现象 :HTTP ( http://your-domain.com ) 访问正常,但 HTTPS ( https://your-domain.com ) 报 ERR_CONNECTION_REFUSED ,而不是常见的证书错误。

排查链条

  1. sudo ss -tlnp | grep :443 —— 看是否有进程在监听 443 端口。如果没有,说明 Nginx 根本没加载 HTTPS 配置。
  2. sudo nginx -t —— 检查语法。Certbot 有时会因权限问题,在修改 Nginx 配置后留下语法错误。
  3. sudo cat /etc/nginx/sites-available/myflaskapp | grep -A 10 "listen 443" —— 确认 Certbot 确实添加了 listen 443 ssl 块,并且 ssl_certificate ssl_certificate_key 的路径是正确的、文件存在、权限为 644
  4. sudo ls -l /etc/letsencrypt/live/your-domain.com/ —— 确认证书文件是软链接,且指向的 fullchain.pem privkey.pem 存在。

高频陷阱 :Certbot 在续期后,会更新 /etc/letsencrypt/live/your-domain.com/ 下的软链接,但如果你在 Nginx 配置里硬编码了 /etc/letsencrypt/archive/... 的绝对路径,那么续期后 Nginx 就会找不到证书,导致启动失败,从而 443 端口无人监听。 永远使用 /etc/letsencrypt/live/your-domain.com/fullchain.pem 这种路径 ,因为 live 目录下的软链接会自动指向最新的证书。

6. 运维与扩展建议:让这套方案陪你走得更远

这套 Ubuntu + Python + Nginx + Gunicorn 的方案,不是一个“一次性部署”,而是一个可持续演进的运维基座。我最后分享几个在实际项目中沉淀下来的、能显著提升长期稳定性的建议。

首先, 把所有配置变成代码 /etc/nginx/sites-available/myflaskapp /etc/systemd/system/gunicorn-myflaskapp.service /home/webapp/myflaskapp/gunicorn.conf.py ,这些文件都应该和你的应用代码一起,放在同一个 Git 仓库里。每次服务器重建,你只需要 git clone ,然后运行一个简单的 setup.sh 脚本,就能自动完成所有配置的部署。这个脚本的核心逻辑就是 cp ln -sf 。这听起来很土,但它比任何复杂的 CI/CD 工具都可靠,因为你完全掌控每一步。

其次, 拥抱 systemd 的健康检查能力 systemd 不只是一个启动器,它还是一个强大的健康守护者。你可以在 gunicorn-myflaskapp.service [Service] 段里,加入:

# 每 30 秒,用 curl 检查应用是否返回 200
HealthCheckIntervalSec=30
HealthCheckStartSec=30
HealthCheckBurst=3
HealthCheckSuccess=1
HealthCheckFailure=3
ExecStartPost=/bin/sh -c 'curl -f http://127.0.0.1:8000/health || exit 1'

当然,这需要你的 Flask 应用提供一个 /health 端点,它只做最轻量的检查(如数据库连接池 ping)。当 systemd 连续三次健康检查失败,它会自动重启服务。这比等用户投诉再处理,要主动得多。

最后, 为未来的容器化铺路 。虽然我们现在用的是纯 Ubuntu 方案,但它的结构已经天然适配 Docker。你的项目目录结构( app.py , requirements.txt , gunicorn.conf.py )就是完美的 Dockerfile 构建上下文。当你决定上容器时, Dockerfile 可以写得极其简洁:

FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD ["gunicorn", "--config", "gunicorn.conf.py", "app:app"]

你会发现,你之前在 Ubuntu 上调试的所有参数( workers , timeout , bind ),都可以无缝迁移到容器里。这证明了: 好的基础架构设计,其价值不在于它有多炫酷,而在于它有多“可迁移”

我个人在实际操作中的体会是,技术选型没有银弹,只有“此刻最合适”。Ubuntu 云服务器方案,胜在透明、可控、学习成本低、社区资源丰富。它可能不是最前沿的,但它能让你把精力聚焦在业务逻辑上,而不是和各种抽象层搏斗。当你能把一个 Flask 应用,从零开始,稳稳当当地部署在 Ubuntu 云服务器上,并让它持续运行数月而不宕机,你就已经掌握了 Web 开发中最核心、也最被低估的一课: 工程化落地的能力 。这比写出一百行炫技的代码,更有价值。

更多推荐