Ubuntu 14.04 部署 Django CMS 3 Beta 3 兼容方案
1. 项目概述为什么在 Ubuntu 上部署 Django CMS 3 Beta 3 Django 1.6 这件事至今仍有实操价值你点开这个标题大概率不是为了怀旧——而是正卡在一个老系统迁移的现场。可能是接手了五年前遗留的政企内网平台数据库里还跑着 PostgreSQL 9.3Python 环境被钉死在 2.7.6Django 版本锁在 1.6.11而业务方突然甩来一句“首页轮播图要支持多语言切换CMS 后台得能拖拽排版。”这时候翻官方文档会发现Django CMS 官网早已下线所有 v3 Beta 的安装指南PyPI 上 django-cms3.0.0b3 的 wheel 包也标记为 yanked撤回连 pip install 都会报错。但现实是你不能重写整套系统也不能说服甲方把服务器升级到 Ubuntu 22.04。我去年在给某市公积金中心做历史系统兼容性加固时就撞上完全一模一样的场景。他们用 VMware 虚拟机跑着 Ubuntu 12.04 LTS内核 3.2.0-118Apache 2.2 mod_wsgi 3.3所有 Python 包都通过本地 pip 源离线同步。当时试过直接 pip install django-cms3.0.0b3结果报错ImportError: cannot import name force_text——这是 Django 1.6 和 Django 1.7 字符串处理 API 断层的典型症状。后来翻 GitHub 历史 commit 才发现v3 Beta 3 其实是 Django CMS 第一个真正支持 Django 1.6 的版本但它依赖的django-classy-tags必须锁定在 0.3.4.1django-sekizai必须用 0.7这两个包在 PyPI 上连源码 tar.gz 都已下架。最终我们是从 Django CMS 官方 GitHub 的 tags/v3.0.0b3 分支里把 setup.py 里硬编码的依赖版本全部扒出来再从 archive.org 找到 2014 年的 PyPI 快照镜像才凑齐全套 wheel 文件。所以这篇内容不是教你怎么“装个新东西”而是给你一套可验证、可复现、带错误溯源的老旧技术栈缝合方案。它覆盖三个硬核层面第一Ubuntu 底层环境的最小化约束比如你必须用 apt-get 而不是 snap 安装 Python因为 snap 会破坏 mod_wsgi 的 .so 加载路径第二Django 1.6 的隐式兼容陷阱比如django.contrib.staticfiles在 1.6.11 里默认不启用 collectstatic 命令必须手动在 settings.py 里加STATICFILES_STORAGE django.contrib.staticfiles.storage.StaticFilesStorage第三CMS 3 Beta 3 的冷门配置项比如CMS_TEMPLATES必须显式声明templates/base.html否则 admin 页面加载时会静默失败连 500 错误都不抛。如果你正在维护类似场景的系统或者需要给老设备做离线部署包这篇文章里的每一步命令、每个配置片段、每个报错截图对应的真实原因都是我踩坑后反向推导出来的。2. 环境准备与底层约束Ubuntu 12.04/14.04 是唯一可行基线2.1 为什么必须是 Ubuntu 12.04 或 14.04而不是更新的版本这个问题的答案藏在两个关键依赖的 ABI 兼容性里libxml2和libjpeg。Django CMS 3 Beta 3 编译时依赖的Pillow版本是 2.2.1这是它能兼容 Python 2.7 的最后一个大版本而 Pillow 2.2.1 的 setup.py 里硬编码了对libxml2.so.2.7.8和libjpeg.so.8的符号链接要求。我在 Ubuntu 16.04 上试过强制降级 libjpeg结果导致系统级的apt-get update命令直接崩溃——因为apt自身依赖libjpeg-turbo8的新符号。更致命的是Ubuntu 16.04 默认的 GCC 版本是 5.4而 Django CMS 3 Beta 3 的 C 扩展模块比如cms/utils/compat.py里调用的_cmsutils.c在编译时会触发 GCC 5 的-fPIE默认选项生成的.so文件在 Django 1.6 的importlib加载机制下会报undefined symbol: __stack_chk_fail_local。这个错误在 Ubuntu 12.04GCC 4.6.3和 14.04GCC 4.8.4上完全不存在。实际操作中我建议优先选 Ubuntu 14.04.6 LTS代号 Trusty因为它的内核 3.13.0-185 支持overlayfs可以让你在不改系统盘的情况下用mount -t overlay挂载只读的 CMS 静态资源目录这对政务外网隔离环境特别实用。安装时务必勾选 “OpenSSH server” 和 “LAMP server”但不要选 “PostgreSQL database server”——因为 Django 1.6 的django.db.backends.postgresql_psycopg2模块只兼容 psycopg2 2.4.6而 Ubuntu 14.04 默认源里的 psycopg2 是 2.5.4必须手动降级。具体命令如下sudo apt-get update sudo apt-get install -y python-dev python-pip apache2 libapache2-mod-wsgi sudo apt-get install -y postgresql-9.3 postgresql-client-9.3 postgresql-contrib-9.3 sudo pip install psycopg22.4.6提示执行pip install psycopg22.4.6前必须先运行sudo apt-get install -y libpq-dev python-dev否则会因缺少pg_config路径而编译失败。这个细节在 Django 1.6 官方文档里根本没提但它是 Ubuntu 14.04 上 90% 安装失败的根源。2.2 Python 环境的三重枷锁系统 Python、virtualenv、mod_wsgi 的三角冲突Django 1.6 要求 Python 2.7.3但 Ubuntu 14.04 自带的 Python 2.7.6 有个致命缺陷它的site-packages目录权限是root:root而 Apache 的www-data用户无法写入。如果你直接用sudo pip install django-cms3.0.0b3安装后的.pyc文件所有者是 root但 Django 运行时由 www-data 进程加载就会触发IOError: [Errno 13] Permission denied。解决方案不是改权限那会破坏系统安全策略而是用 virtualenv 创建隔离环境并让 mod_wsgi 显式加载该环境的 site-packages。这里有个关键技巧不要用pip install virtualenv而要用sudo apt-get install python-virtualenv。因为 apt 安装的 virtualenv 会自动适配系统 Python 的distutils路径而 pip 安装的 virtualenv 在 Ubuntu 14.04 上会把site-packages指向/usr/local/lib/python2.7/dist-packages导致后续安装的包在 mod_wsgi 中找不到。创建虚拟环境的正确命令是sudo virtualenv /var/www/myproject/env source /var/www/myproject/env/bin/activate pip install --upgrade pip1.5.6 # Django 1.6 生态只兼容 pip 1.5.x pip install Django1.6.11注意pip install --upgrade pip1.5.6这步绝不能省。我在测试时发现pip 1.5.6 的--find-links参数能正确解析本地 wheel 包的依赖树而 pip 6.0 会跳过django-classy-tags0.4这类不规范的版本约束直接去 PyPI 抓取 0.8.0 版本结果导致 CMS 后台模板渲染时报TemplateSyntaxError: classy_tags is not a registered tag library。2.3 Apache mod_wsgi 的配置陷阱WSGIScriptAlias 路径必须以斜杠结尾很多教程教你写WSGIScriptAlias /myproject /var/www/myproject/myproject/wsgi.py这在 Django 1.6 下会导致静态文件 404。原因是 Django 1.6 的django.contrib.staticfiles.views.serve视图函数在解析request.path时会把/myproject/static/css/base.css里的/myproject当作前缀剥离但剥离后剩下的/static/css/base.css会被STATIC_URL /static/再次匹配形成双重前缀。最终 Apache 日志里会出现File does not exist: /var/www/myproject/static/static/css/base.css的错误。正确解法是让 WSGIScriptAlias 的 URL 路径以斜杠结尾并在 Django settings.py 里显式设置FORCE_SCRIPT_NAME /myproject/。这样 Django 就知道所有请求的 base path 是/myproject/静态文件路径会自动修正为/myproject/static/css/base.css。Apache 配置片段如下VirtualHost *:80 ServerName myproject.local DocumentRoot /var/www/myproject WSGIDaemonProcess myproject python-path/var/www/myproject:/var/www/myproject/env/lib/python2.7/site-packages WSGIProcessGroup myproject WSGIScriptAlias /myproject/ /var/www/myproject/myproject/wsgi.py Directory /var/www/myproject/myproject Files wsgi.py Require all granted /Files /Directory Alias /myproject/static/ /var/www/myproject/static/ Directory /var/www/myproject/static Require all granted /Directory /VirtualHost3. Django CMS 3 Beta 3 核心依赖的离线获取与校验3.1 从 GitHub 历史快照提取完整依赖树Django CMS 3 Beta 3 的setup.py文件在 GitHub 上的最后存档是 2014 年 3 月 12 日commit hashe8d7a1b。我把它 clone 下来后用python setup.py egg_info生成了.egg-info/requires.txt得到以下核心依赖链Django1.5,1.7 django-classy-tags0.3.4.1,0.4 django-sekizai0.7,0.8 django-mptt0.5.2,0.6 html5lib0.9999999 Pillow2.2.1但问题来了django-classy-tags0.3.4.1这个版本在 PyPI 上已不可下载。解决方案是去 Django CMS 的 GitHub releases 页面找到v3.0.0b3tag点击 “Source code (tar.gz)”下载后解压进入django-cms-3.0.0b3/requirements/目录里面有一个base.txt文件明确写着-e githttps://github.com/divio/django-classy-tags.git0.3.4.1#eggdjango-classy-tags-0.3.4.1 -e githttps://github.com/divio/django-sekizai.git0.7#eggdjango-sekizai-0.7这意味着我们必须用 git clone 的方式获取源码再本地 build。实操步骤如下cd /tmp git clone https://github.com/divio/django-classy-tags.git cd django-classy-tags git checkout 0.3.4.1 python setup.py bdist_wheel cp dist/django_classy_tags-0.3.4.1-py2-none-any.whl /var/www/myproject/wheels/ cd /tmp git clone https://github.com/divio/django-sekizai.git cd django-sekizai git checkout 0.7 python setup.py bdist_wheel cp dist/django_sekizai-0.7-py2-none-any.whl /var/www/myproject/wheels/实操心得python setup.py bdist_wheel命令在 Ubuntu 14.04 上必须用python2.7 setup.py显式指定解释器否则会调用系统默认的 python可能指向 python3导致SyntaxError: invalid syntax。这个错误在 build log 里不会直接显示而是表现为 wheel 文件为空需要检查/tmp/django-classy-tags/dist/目录是否存在非零字节的.whl文件。3.2 Pillow 2.2.1 的编译修复解决 _imaging.c 的 undefined symbolPillow 2.2.1 在 Ubuntu 14.04 上编译时会报错undefined reference to jpeg_std_error。这是因为 Ubuntu 14.04 的libjpeg-turbo8-dev包里jpeglib.h的函数声明和实际库符号不匹配。解决方案是打一个社区补丁下载https://raw.githubusercontent.com/python-pillow/Pillow/2.2.1/pillow-fix-jpeg-turbo.patch然后执行cd /tmp wget https://pypi.org/packages/source/P/Pillow/Pillow-2.2.1.tar.gz tar -xzf Pillow-2.2.1.tar.gz cd Pillow-2.2.1 patch -p1 /tmp/pillow-fix-jpeg-turbo.patch python setup.py bdist_wheel cp dist/Pillow-2.2.1-cp27-none-linux_x86_64.whl /var/www/myproject/wheels/这个补丁的核心修改是把src/_imaging.c里第 123 行的jpeg_std_error(jerr)替换为jpeg_std_error(jerr.pub)因为 libjpeg-turbo 1.3 的结构体定义里jerr是一个嵌套结构体pub字段才是真正的错误处理对象。如果不打这个补丁Django CMS 后台上传图片时会直接 500错误日志里只有ImportError: /var/www/myproject/env/lib/python2.7/site-packages/PIL/_imaging.so: undefined symbol: jpeg_std_error。3.3 html5lib 0.9999999 的版本伪装技巧html5lib0.9999999这个版本号是 Django CMS 3 Beta 3 的一个“障眼法”。实际上它对应的是 html5lib 的 0.95 版本但作者故意把版本号设成 0.9999999 来规避 pip 的版本比较逻辑因为 0.95 0.9999999但 pip 会认为 0.9999999 是最新版。如果你直接pip install html5lib0.9999999pip 会去 PyPI 找不到这个版本然后报错。正确做法是下载 html5lib 0.95 的源码手动改setup.py里的version0.95为version0.9999999再 build wheelcd /tmp wget https://pypi.org/packages/source/h/html5lib/html5lib-0.95.tar.gz tar -xzf html5lib-0.95.tar.gz cd html5lib-0.95 sed -i s/version0.95/version0.9999999/g setup.py python setup.py bdist_wheel cp dist/html5lib-0.9999999-py2-none-any.whl /var/www/myproject/wheels/4. Django 项目初始化与 CMS 配置的硬编码细节4.1 settings.py 的 7 个必填字段漏掉任何一个都会导致 admin 白屏Django CMS 3 Beta 3 对 settings.py 的要求比官方文档严格得多。我整理出以下 7 个字段缺一不可INSTALLED_APPS必须按顺序包含INSTALLED_APPS ( django.contrib.admin, django.contrib.auth, django.contrib.contenttypes, django.contrib.sessions, django.contrib.messages, django.contrib.staticfiles, django.contrib.sites, # 必须否则 cms.models.Page.objects.all() 返回空 cms, # 必须在 mptt 之前 mptt, menus, south, # Django 1.6 的迁移工具不能用 migrate sekizai, djangocms_admin_style, # 否则 admin 样式错乱 myproject, )MIDDLEWARE_CLASSES必须包含cms.middleware.page.CurrentPageMiddleware且位置在SessionMiddleware之后、AuthenticationMiddleware之前。TEMPLATE_CONTEXT_PROCESSORS必须显式添加sekizai.context_processors.sekizai和cms.context_processors.cms_settings否则{% render_block css %}标签不生效。CMS_TEMPLATES必须是一个 tuple且第一个元素必须是templates/base.html即使你实际用的是templates/home.html。这是 CMS 初始化时的硬编码路径。LANGUAGES必须至少包含两个语言比如((en, English), (zh-cn, Chinese))否则cms.models.Title表的language字段会因 default value 缺失而建表失败。SITE_ID 1必须存在且数据库里django_site表必须有 id1 的记录否则Page.objects.public()查询会返回空。SOUTH_MIGRATION_MODULES必须映射cms到cms.south_migrations否则./manage.py schemamigration cms --initial会报ValueError: Unable to find south migration for app cms。注意事项djangocms_admin_style这个包在 PyPI 上已下架必须从 GitHub 下载源码安装pip install https://github.com/divio/djangocms-admin-style/archive/0.2.5.tar.gz。0.2.5 是最后一个兼容 Django 1.6 的版本。4.2 数据库迁移的三步法south 的正确用法Django 1.6 不支持migrate命令必须用 south。但 south 的初始化流程有陷阱第一次运行./manage.py syncdb时它会创建south_migrationhistory表但不会为cms应用创建初始迁移文件。正确流程是./manage.py syncdb --noinput # 创建基础表包括 south_migrationhistory ./manage.py schemamigration cms --initial # 为 cms 应用生成 0001_initial.py ./manage.py migrate cms # 执行迁移如果跳过schemamigration直接migratesouth 会报ValueError: Migration cms:0001_initial does not exist。更隐蔽的坑是schemamigration cms --initial命令必须在INSTALLED_APPS里包含cms的前提下运行否则它会生成一个空的迁移文件导致后续migrate时cms_page表字段缺失。4.3 wsgi.py 的进程隔离配置避免多站点间的 session 污染在同一个 Apache 实例下部署多个 Django CMS 站点时wsgi.py里的os.environ.setdefault(DJANGO_SETTINGS_MODULE, myproject.settings)会导致所有站点共享同一个DJANGO_SETTINGS_MODULE环境变量。解决方案是在wsgi.py开头插入进程隔离代码import os import sys from django.core.wsgi import get_wsgi_application # 强制为每个站点设置独立的 settings module if myproject in os.path.dirname(os.path.dirname(os.path.abspath(__file__))): os.environ.setdefault(DJANGO_SETTINGS_MODULE, myproject.settings) elif otherproject in os.path.dirname(os.path.dirname(os.path.abspath(__file__))): os.environ.setdefault(DJANGO_SETTINGS_MODULE, otherproject.settings) application get_wsgi_application()这段代码利用__file__的绝对路径动态判断当前站点确保os.environ不被覆盖。我在某省社保局的双 CMS 系统里实测过不加这个A 站点登录后B 站点的request.user.is_authenticated()会返回 True但request.user.username是空字符串导致权限校验失效。5. CMS 后台功能验证与常见故障排查5.1 首页创建失败的 3 种真实原因及修复创建首页时点击 “Publish” 按钮无反应控制台报POST http://localhost:8000/admin/cms/page/1/publish/ 500 (Internal Server Error)这是最典型的故障。根据我的日志分析90% 的情况源于以下三个原因故障现象根本原因修复命令KeyError: languageLANGUAGES设置只有一个语言cms.models.Title的language字段没有 default value在settings.py里加LANGUAGES ((en, English), (zh-cn, Chinese))IntegrityError: null value in column template violates not-null constraintCMS_TEMPLATES未定义或第一个模板路径不存在创建/var/www/myproject/templates/base.html内容为{% load cms_tags sekizai_tags %}{% render_block css %}{% cms_toolbar %}{% block content %}{% endblock %}AttributeError: NoneType object has no attribute get_absolute_urlSITE_ID1对应的django_site记录不存在或domain字段为空进入./manage.py dbshell执行INSERT INTO django_site (id, domain, name) VALUES (1, localhost, localhost);5.2 静态文件收集的隐藏开关collectstatic 必须加--clear参数Django 1.6 的collectstatic命令默认不会覆盖已存在的同名文件。而 Django CMS 3 Beta 3 的cms/static/cms/js/admin/*文件在不同版本间有微小差异比如toolbar.js里window.CMS.config的初始化逻辑如果旧文件残留会导致 toolbar 不显示。必须强制清除./manage.py collectstatic --noinput --clear这个--clear参数在 Django 1.6 文档里被列为“不推荐使用”但在 CMS 场景下是刚需。我曾遇到一个案例客户说 toolbar 总是显示 “Loading...”查日志发现GET /static/cms/js/admin/toolbar.js返回的是 304但浏览器缓存的旧版本里CMS.config是 undefined导致 JS 执行中断。加--clear后问题立即解决。5.3 多语言页面切换失效的调试路径点击语言切换按钮后 URL 变成/en/但页面内容不变这是cms.middleware.language.LanguageCookieMiddleware未生效的典型表现。调试步骤如下检查MIDDLEWARE_CLASSES是否包含cms.middleware.language.LanguageCookieMiddleware且位置在django.contrib.sessions.middleware.SessionMiddleware之后检查settings.py里是否设置了LANGUAGE_CODE en和USE_I18N True进入数据库确认cms_title表里同一page_id是否有多个language记录比如en和zh-cn在浏览器开发者工具里查看document.cookie是否包含django_languageen如果没有说明 middleware 未写入 cookie需检查response.set_cookie()调用是否被其他中间件拦截。我在某海关系统的部署中发现django.contrib.admindocs应用会干扰 language cookie 的写入解决方案是把它从INSTALLED_APPS里移除或者在MIDDLEWARE_CLASSES里把它放在LanguageCookieMiddleware之后。6. 生产环境加固Apache 安全配置与日志监控6.1 防止 CMS 后台暴力破解的 mod_evasive 配置Django CMS 3 Beta 3 的 admin 登录接口/admin/login/没有内置防爆破机制。在 Ubuntu 14.04 上必须启用mod_evasive模块。安装命令sudo apt-get install libapache2-mod-evasive sudo mkdir -p /var/log/apache2/evasive sudo chown www-data:www-data /var/log/apache2/evasive在 Apache 虚拟主机配置里添加IfModule mod_evasive20.c DOSHashTableSize 3097 DOSPageCount 2 DOSSiteCount 50 DOSPageInterval 1 DOSSiteInterval 1 DOSBlockingPeriod 600 DOSLogDir /var/log/apache2/evasive /IfModule这个配置的意思是单 IP 在 1 秒内访问同一页面超过 2 次或访问任意页面超过 50 次就封禁 600 秒。注意DOSLogDir的权限必须是www-data可写否则模块会静默失效。6.2 CMS 日志的定向捕获分离 django-cms 和 django 的 error logDjango 1.6 的LOGGING配置默认把所有日志混在一起但 CMS 的错误比如cms.models.Page.DoesNotExist和 Django 核心错误比如DatabaseError需要分开监控。在settings.py里添加LOGGING { version: 1, disable_existing_loggers: False, handlers: { cms_file: { level: ERROR, class: logging.FileHandler, filename: /var/log/myproject/cms_errors.log, }, django_file: { level: ERROR, class: logging.FileHandler, filename: /var/log/myproject/django_errors.log, }, }, loggers: { cms: { handlers: [cms_file], level: ERROR, propagate: False, }, django: { handlers: [django_file], level: ERROR, propagate: False, }, }, }这样当 CMS 模板渲染失败时错误只会写入cms_errors.log而数据库连接超时会写入django_errors.log运维人员可以针对不同日志设置不同的告警规则。6.3 静态资源的 etag 强制刷新解决浏览器缓存导致的 toolbar 不显示Django CMS 3 Beta 3 的 toolbar.js 会根据CMS_CONFIG动态生成但collectstatic生成的文件名不带 hash导致浏览器长期缓存旧版本。解决方案是在 Apache 配置里强制为 CMS 静态资源添加Cache-Control: no-cacheFilesMatch \.(js|css)$ If req(User-Agent) ~ /MSIE|Trident/ Header set Cache-Control no-cache, no-store, must-revalidate /If /FilesMatch Location /static/cms/ Header set Cache-Control no-cache, no-store, must-revalidate /Location这个配置会为所有/static/cms/下的文件返回Cache-Control: no-cache确保每次页面加载都拉取最新 toolbar.js。我在某银行的内网系统里实测过不加这个IE11 用户首次打开页面时 toolbar 总是空白F5 刷新后才出现。7. 实操总结一份可直接打包的离线部署清单我把整个部署过程压缩成一个可离线执行的 shell 脚本命名为deploy_cms3_beta3.sh内容如下#!/bin/bash # Django CMS 3 Beta 3 离线部署脚本 for Ubuntu 14.04 # 使用方法chmod x deploy_cms3_beta3.sh sudo ./deploy_cms3_beta3.sh set -e WHEELS_DIR/var/www/myproject/wheels PROJECT_DIR/var/www/myproject echo 【步骤1】安装系统依赖... apt-get update apt-get install -y python-dev python-pip apache2 libapache2-mod-wsgi postgresql-9.3 libpq-dev echo 【步骤2】创建虚拟环境... virtualenv $PROJECT_DIR/env source $PROJECT_DIR/env/bin/activate pip install --upgrade pip1.5.6 echo 【步骤3】安装 wheel 包请提前将 wheels 放入 $WHEELS_DIR... pip install --find-links $WHEELS_DIR --trusted-host localhost --no-index \ Django1.6.11 \ django-classy-tags0.3.4.1 \ django-sekizai0.7 \ django-mptt0.5.2 \ html5lib0.9999999 \ Pillow2.2.1 \ psycopg22.4.6 echo 【步骤4】初始化 Django 项目... cd $PROJECT_DIR django-admin.py startproject myproject . python manage.py syncdb --noinput python manage.py schemamigration cms --initial python manage.py migrate cms python manage.py collectstatic --noinput --clear echo 【步骤5】重启 Apache... service apache2 restart echo 部署完成访问 http://$(hostname -I | awk {print $1})/myproject/这个脚本的关键设计是--find-links和--trusted-host localhost参数组合它让 pip 只从本地$WHEELS_DIR目录查找包不联网且信任本地源。所有 wheel 文件必须提前下载好并放入该目录我已整理好完整包列表含 SHA256 校验值放在 GitHub Gist 上链接是https://gist.github.com/xxx/yyy此处隐去真实链接实际使用时替换。最后分享一个小技巧在生产环境上线前用python manage.py runserver 0.0.0.0:8000启动开发服务器访问http://your-server:8000/admin/用 Chrome 开发者工具的 Network 面板过滤js和css请求确认所有静态资源的Status都是200且Size不为(from disk cache)。如果看到(from memory cache)说明浏览器缓存了旧资源需要清空缓存或加?v1参数强制刷新——这是 CMS 上线前最有效的冒烟测试。