Ubuntu 22.04 上从零部署 Mezzanine CMS:Gunicorn + Nginx 生产环境实战 1. 为什么要在 Ubuntu 22.04 上折腾 Mezzanine如果你正在找一个“能长期用、不折腾、后台清爽”的内容管理系统Mezzanine 大概率会进入你的候选名单。它基于 Django 构建自带后台管理、富文本编辑、页面层级、博客、表单、图库、SEO 字段甚至内置了电商模块的扩展接口。和 WordPress 那种“插件一装就崩”的体验不同Mezzanine 的哲学是核心功能足够全扩展靠 Django 生态稳定性和可控性都强很多。但问题也恰恰出在这里——它不是一个“下载即用”的 PHP 程序而是一个需要 Python 环境、Django 版本匹配、数据库配置、静态文件收集、生产级 WSGI 部署的完整 Django 项目。很多人在 Ubuntu 上装到一半就卡在psycopg2编译失败、Pillow缺库、collectstatic报错、Nginx 502 这些坑上。我自己第一次在 Ubuntu 22.04 上部署 Mezzanine 时光是把 PostgreSQL 和 Python 虚拟环境的权限理顺就花了两个多小时。这篇内容就是把我踩过的坑、验证过的步骤、以及生产环境里真正稳的配置方案完整梳理出来。目标很明确让你在一台干净的 Ubuntu 22.04 上从零把 Mezzanine CMS 跑起来并且能直接用于正式站点。不管你是刚接触 Django 的新手还是已经写过几个 Django 项目想找个成熟 CMS 复用的老手这套流程都能直接抄作业。提示本文所有操作均在 Ubuntu 22.04 LTS 上验证Python 版本为 3.10数据库使用 PostgreSQL 14Web 服务采用 Gunicorn Nginx 组合。如果你用的是其他 Ubuntu 版本大部分步骤通用但包名和默认 Python 版本需要微调。2. 环境准备与依赖选型背后的逻辑2.1 系统更新与基础工具链安装拿到一台新的 Ubuntu 22.04 服务器第一件事不是急着装 Python 包而是把系统源和基础编译工具补齐。Mezzanine 依赖的几个关键包——psycopg2、Pillow、lxml——都需要在安装时进行本地编译如果缺少gcc、libpq-dev、libjpeg-dev这些头文件pip 安装阶段就会直接报错退出。sudo apt update sudo apt upgrade -y sudo apt install -y build-essential libpq-dev libjpeg-dev zlib1g-dev \ libfreetype6-dev liblcms2-dev libwebp-dev libxml2-dev libxslt1-dev \ python3-dev python3-pip python3-venv git nginx postgresql postgresql-contrib这里有几个点值得展开说。build-essential提供了 gcc 和 make是编译 C 扩展的基础libpq-dev是 PostgreSQL 的客户端开发库psycopg2编译时必须链接它libjpeg-dev、zlib1g-dev、libfreetype6-dev这一串是 Pillow 处理图片格式所需的底层库少一个都可能在pip install Pillow时失败。我见过太多人只装python3-pip就开始pip install mezzanine结果卡在error: command gcc failed上反复重试。注意Ubuntu 22.04 默认的 Python 是 3.10Mezzanine 官方对 3.10 的支持在较新版本中已经稳定但如果你用的是 Mezzanine 4.x 早期版本建议升级到 5.x 或 6.x避免 Django 版本冲突。2.2 为什么用 PostgreSQL 而不是 SQLiteMezzanine 默认配置使用 SQLite本地开发确实方便但一旦上生产就会遇到并发写入锁、全文检索性能差、备份恢复麻烦等问题。PostgreSQL 在 Django 生态里是一等公民Mezzanine 的搜索模块、层级查询、事务处理在 PostgreSQL 下表现明显更好。安装完成后切换到 postgres 用户创建数据库和专用账号sudo -u postgres psql在 psql 交互界面中执行CREATE DATABASE mezzanine_db; CREATE USER mezzanine_user WITH PASSWORD your_strong_password; ALTER ROLE mezzanine_user SET client_encoding TO utf8; ALTER ROLE mezzanine_user SET default_transaction_isolation TO read committed; ALTER ROLE mezzanine_user SET timezone TO Asia/Shanghai; GRANT ALL PRIVILEGES ON DATABASE mezzanine_db TO mezzanine_user; \q这里把client_encoding设为 utf8 是为了避免中文内容写入时出现乱码default_transaction_isolation设为 read committed 是 Django 官方推荐的生产配置时区设置则影响后台时间显示和定时任务。密码不要用弱口令生产环境建议用 16 位以上随机字符串。2.3 Python 虚拟环境的隔离策略系统级 pip 安装在生产环境是大忌不同项目依赖冲突时会非常难排查。我习惯在/opt或用户家目录下创建虚拟环境路径清晰权限也好管理。sudo mkdir -p /opt/mezzanine sudo chown -R $USER:$USER /opt/mezzanine cd /opt/mezzanine python3 -m venv venv source venv/bin/activate pip install --upgrade pip setuptools wheel升级pip、setuptools、wheel这三件套是很多教程会忽略的一步。老版本的 pip 在解析 Mezzanine 的依赖树时可能选到不兼容的版本组合升级后能避免大量“依赖地狱”问题。虚拟环境激活后命令行提示符前会出现(venv)后续所有 pip 操作都在这个隔离环境中进行。3. Mezzanine 安装与项目初始化的完整实操3.1 安装 Mezzanine 及其核心依赖在虚拟环境激活状态下执行pip install mezzanine psycopg2-binary gunicorn这里我特意用了psycopg2-binary而不是psycopg2。两者的区别在于psycopg2需要本地编译依赖libpq-devpsycopg2-binary是预编译 wheel 包安装快、兼容性好适合大多数生产场景。只有在需要特定编译选项或调试数据库底层问题时才需要源码编译版本。安装完成后可以用pip list确认关键包版本pip list | grep -E Mezzanine|Django|psycopg2|Pillow正常输出应该类似包名版本示例Mezzanine6.0.0Django4.2.xpsycopg2-binary2.9.xPillow10.x如果 Django 版本和 Mezzanine 不匹配启动时会直接报ImportError或ImproperlyConfigured。Mezzanine 6.x 对应 Django 4.2Mezzanine 5.x 对应 Django 3.2这个对应关系必须严格对齐。3.2 创建 Mezzanine 项目骨架Mezzanine 提供了一个mezzanine-project命令来生成项目结构mezzanine-project myblog cd myblog生成的目录结构大致如下myblog/ ├── manage.py ├── myblog/ │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── static/ ├── templates/ └── requirements.txt这个结构和标准 Django 项目基本一致区别在于settings.py里已经预置了 Mezzanine 的INSTALLED_APPS、中间件、模板上下文处理器等配置。static和templates目录是 Mezzanine 主题覆盖的入口后续自定义样式和页面都从这里入手。3.3 数据库连接配置与 settings.py 关键修改打开myblog/settings.py找到DATABASES部分替换为 PostgreSQL 配置DATABASES { default: { ENGINE: django.db.backends.postgresql, NAME: mezzanine_db, USER: mezzanine_user, PASSWORD: your_strong_password, HOST: localhost, PORT: 5432, } }同时确认ALLOWED_HOSTS已经包含你的服务器 IP 或域名ALLOWED_HOSTS [your_domain.com, www.your_domain.com, 127.0.0.1]还有一个容易被忽略的配置是TIME_ZONE和LANGUAGE_CODE。中文站点建议LANGUAGE_CODE zh-hans TIME_ZONE Asia/Shanghai USE_I18N True USE_TZ TrueUSE_TZ True是 Django 的推荐设置数据库存 UTC 时间展示时按TIME_ZONE转换。如果你把它设为 False后期做定时发布、时区相关功能时会很痛苦。3.4 初始化数据库与创建超级用户配置完成后执行迁移python manage.py migrate这个命令会创建 Mezzanine 所需的全部表包括页面、博客、表单、图库、用户权限等。迁移过程中如果报django.db.utils.OperationalError: could not connect to server说明 PostgreSQL 服务没启动或账号密码不对先用sudo systemctl status postgresql检查服务状态。迁移成功后创建管理员账号python manage.py createsuperuser按提示输入用户名、邮箱、密码。这个账号就是 Mezzanine 后台的超级管理员拥有全部权限。接着收集静态文件python manage.py collectstaticMezzanine 自带的后台主题、富文本编辑器、图库脚本都依赖静态文件collectstatic会把它们统一复制到STATIC_ROOT指定的目录。如果这一步报FileNotFoundError检查STATIC_ROOT是否配置了绝对路径。3.5 开发模式快速验证在正式部署前先用 Django 自带的开发服务器验证一切正常python manage.py runserver 0.0.0.0:8000浏览器访问http://你的服务器IP:8000/admin能看到 Mezzanine 登录页就说明核心链路已经通了。登录后可以创建页面、发博客、传图片确认数据库读写、静态文件加载、后台功能都正常。提示开发服务器仅用于验证不要用于生产。它的并发处理能力极弱且不会自动处理静态文件缓存和 HTTPS。4. 生产级部署Gunicorn Nginx 配置详解4.1 Gunicorn 服务配置与系统守护开发服务器验证通过后下一步是用 Gunicorn 作为 WSGI 服务器。在项目根目录创建gunicorn.conf.pybind 127.0.0.1:8001 workers 3 worker_class sync timeout 120 keepalive 5 max_requests 1000 max_requests_jitter 100 accesslog /var/log/mezzanine/access.log errorlog /var/log/mezzanine/error.log loglevel infoworkers的数量有个经验公式CPU 核心数 * 2 1。如果是 1 核 2G 的小机器设 3 个 worker 足够4 核 8G 可以设 9 个。max_requests配合max_requests_jitter是为了防止内存泄漏累积每个 worker 处理 1000 次请求后自动重启jitter 让重启时间错开避免同时重启导致服务中断。创建 systemd 服务文件/etc/systemd/system/mezzanine.service[Unit] DescriptionMezzanine Gunicorn Service Afternetwork.target postgresql.service [Service] Userwww-data Groupwww-data WorkingDirectory/opt/mezzanine/myblog EnvironmentPATH/opt/mezzanine/venv/bin ExecStart/opt/mezzanine/venv/bin/gunicorn myblog.wsgi:application -c /opt/mezzanine/myblog/gunicorn.conf.py Restartalways RestartSec5 [Install] WantedBymulti-user.target这里把运行用户设为www-data是 Ubuntu 下 Nginx 的默认用户方便文件权限统一。Restartalways保证进程崩溃后自动拉起RestartSec5避免频繁重启拖垮系统。启动并设置开机自启sudo systemctl daemon-reload sudo systemctl start mezzanine sudo systemctl enable mezzanine sudo systemctl status mezzanine4.2 Nginx 反向代理与静态文件托管Nginx 负责两件事把动态请求转发给 Gunicorn直接托管静态文件和媒体文件。创建/etc/nginx/sites-available/mezzanineserver { listen 80; server_name your_domain.com www.your_domain.com; client_max_body_size 20M; location /static/ { alias /opt/mezzanine/myblog/static/; expires 30d; add_header Cache-Control public, immutable; } location /media/ { alias /opt/mezzanine/myblog/media/; expires 7d; } location / { proxy_pass http://127.0.0.1:8001; 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; } }client_max_body_size 20M是为了支持后台上传较大图片或附件默认 1M 很容易在上传时被 Nginx 直接拒绝并返回 413。静态文件设置 30 天缓存并加immutable是因为collectstatic后的文件名通常带哈希内容不会变可以放心让浏览器长期缓存。启用站点并重载 Nginxsudo ln -s /etc/nginx/sites-available/mezzanine /etc/nginx/sites-enabled/ sudo nginx -t sudo systemctl reload nginxnginx -t是必做步骤配置语法错误会导致 reload 失败甚至 Nginx 停机。4.3 文件权限与目录归属处理生产环境最常见的 502 错误来源就是权限。Gunicorn 以www-data运行就必须能读取项目代码、写入 media 目录和日志目录sudo chown -R www-data:www-data /opt/mezzanine/myblog/media sudo chown -R www-data:www-data /var/log/mezzanine sudo chmod -R 755 /opt/mezzanine/myblog项目代码目录保持 755media 和日志目录需要写权限。不要图省事直接chmod -R 777那会带来严重的安全隐患。5. 常见问题排查与避坑经验实录5.1 安装阶段高频报错速查报错信息根本原因解决方案gcc failed with exit status 1缺少编译工具或头文件安装build-essential和对应-dev包pg_config executable not found缺少libpq-devsudo apt install libpq-devNo module named PILPillow 未安装或编译失败补装图片库后重装 Pillowdjango.core.exceptions.ImproperlyConfiguredDjango 与 Mezzanine 版本不匹配按对应关系降级或升级relation ... does not exist未执行 migratepython manage.py migrate5.2 部署后 502 与静态文件 404 的排查思路502 的第一排查顺序是Gunicorn 进程是否存活 → 端口是否监听 → Nginx 配置的 upstream 地址是否一致 → 文件权限是否足够。用sudo systemctl status mezzanine看进程状态用ss -tlnp | grep 8001看端口监听用sudo tail -f /var/log/mezzanine/error.log看应用日志。静态文件 404 则通常是collectstatic没执行、STATIC_ROOT路径不对、或 Nginxalias路径末尾斜杠不匹配。Nginx 的alias指令对末尾斜杠非常敏感/static/对应/opt/.../static/两边要么都带斜杠要么都不带混用会导致路径拼接错误。5.3 我踩过的三个真实坑第一个坑是ALLOWED_HOSTS没加服务器 IP开发服务器能跑但 Gunicorn 启动后所有请求返回 400。Django 在DEBUGFalse时强制校验 Host 头这个报错不会在日志里写得很明显容易误判成 Nginx 问题。第二个坑是 media 目录权限。后台上传图片成功但前台显示 403。原因是 Gunicorn 以www-data写入的图片Nginx 读取时权限没问题但目录本身归属 root导致新文件创建失败。把整个 media 目录归属改成www-data后解决。第三个坑是 PostgreSQL 的peer认证。用sudo -u postgres psql能进但 Django 用密码连接时报Peer authentication failed。这是因为pg_hba.conf里 local 连接默认是 peer 认证需要改成 md5 或 scram-sha-256并重启 PostgreSQL。5.4 上线前的检查清单DEBUG False已设置ALLOWED_HOSTS包含所有访问域名和 IPSECRET_KEY已替换为随机长字符串且未提交到版本库数据库使用独立账号非 postgres 超级用户静态文件和 media 目录权限正确Gunicorn 和 Nginx 均设置开机自启日志目录存在且可写防火墙只开放 80/443 和必要的 SSH 端口这套流程我在三台不同配置的 Ubuntu 22.04 机器上跑过从 1 核 2G 的轻量云主机到 4 核 8G 的独立服务器核心步骤完全一致。唯一需要按机器调整的是 Gunicorn 的workers数量和 PostgreSQL 的共享缓冲区参数。Mezzanine 本身对资源要求不高真正吃内存的是 Pillow 处理大图时的临时占用如果站点图片多建议把client_max_body_size和 Pillow 的MAX_IMAGE_PIXELS一起调大避免上传高清图时被静默截断。