OpenWISP 1.3.2 Ubuntu 22.04 安装指南
在 Ubuntu 22.04.5 LTS 上用 Python 虚拟环境或离线 wheel 安装 openwisp-controller 1.3.2,配套 PostgreSQL 14 + PostGIS、Redis、uWSGI 与 Nginx,systemd 托管并完成升级回滚。
OpenWISP 1.3.2 Ubuntu 22.04 安装指南
目标:在 Ubuntu 22.04.5 LTS 上从零部署一套可登录、可下发配置的 OpenWISP Controller(1.3.2),包含 PostgreSQL 14 + PostGIS 3.2、Redis 6、uWSGI、Celery worker/beat 与 Nginx 反向代理,全部由 systemd 托管。文中命令均在本机真实执行,回显照抄;离线安装方案同样实测通过。配置项逐条解释见 《OpenWISP 1.3.2 配置参数详解》,日常命令与备份排障见 《OpenWISP 1.3.2 常用 CLI 与运维命令详解》。
一、环境信息
以下为本次实测主机与组件版本(2026-10-28 采集):
- 操作系统:Ubuntu 22.04.5 LTS(
PRETTY_NAME="Ubuntu 22.04.5 LTS") - 主机资源:6 vCPU,18 GiB 内存,
/opt可用 151 GiB - Python:3.10.12(系统自带)
- openwisp-controller:1.3.2(PyPI,2026-10-23 发布)
- Django:5.2.18
- PostgreSQL:14.24(
14.24-0ubuntu0.22.04.1) - PostGIS:3.2.0(
3.2.0+dfsg-1ubuntu1,postgis_version()返回3.2 USE_GEOS=1 USE_PROJ=1 USE_STATS=1) - Redis:6.0.16(
5:6.0.16-1ubuntu1.2) - Nginx:1.18.0(
1.18.0-6ubuntu14.22) - uWSGI:2.0.20(
2.0.20-4,含uwsgi-plugin-python3) - 应用目录:
/opt/openwisp/venv(虚拟环境)、/opt/openwisp/app(Django 项目) - 配置目录:
/etc/openwisp/(uWSGI 配置)、/etc/systemd/system/(服务单元) - 日志目录:
/var/log/openwisp/ - 监听端口:Nginx 80(对外)、PostgreSQL 5432、Redis 6379、uWSGI 仅 Unix Socket
/run/uwsgi/openwisp.sock - 站点域名:
openwisp.example.com(解析到本机)
💡 说明:OpenWISP 官方文档站当前显示 “OpenWISP 26.09 documentation”,而 PyPI 包版本是 1.3.2,两者编号体系不同。本文以 PyPI 包版本 1.3.2 为安装目标,文档站仅用于查配置项与操作说明。
二、前置条件
部署前需要确认三件事:系统版本、网络可达性、端口与目录规划。OpenWISP 是标准 Django 应用,对系统没有特殊内核要求,但 PostGIS 与 Python 编译依赖必须装齐。
系统与资源
- Ubuntu 22.04.x LTS(22.04.5 实测),Python ≥ 3.10;官方文档标注 Python ≥ 3.10。
- 建议至少 2 vCPU、4 GiB 内存、40 GiB 磁盘;本次实测 6 vCPU / 18 GiB 属于充裕配置。
- 需要 root 或 sudo 权限执行软件包安装与 systemd 托管。
网络
- 服务器需能访问
pypi.org与files.pythonhosted.org(在线方案)。 - 若服务器在隔离网络,先在可联网机器执行本文第五章的离线打包流程。
- 若下载缓慢,可临时设置 HTTP 代理:
export https_proxy=http://192.168.1.62:7897(代理地址按实际环境替换)。
端口
| 端口 | 用途 | 是否对外 |
|---|---|---|
| 80 | Nginx 反向代理 | 是 |
| 5432 | PostgreSQL | 否(仅本机) |
| 6379 | Redis | 否(仅本机) |
| Unix Socket | uWSGI 与 Nginx 通信 | 否(文件系统内) |
目录规划
| 路径 | 用途 | 属主 |
|---|---|---|
/opt/openwisp/venv | Python 虚拟环境 | root |
/opt/openwisp/app | Django 项目、静态文件、媒体文件 | www-data |
/etc/openwisp | uWSGI 配置 | root |
/var/log/openwisp | 应用与 uWSGI 日志 | www-data |
/var/backups/openwisp | 数据库备份 | postgres |
⚠️ 注意:应用目录必须让
www-data可进入(/opt/openwisp至少 755、/opt/openwisp/app归 www-data),否则 uWSGI 启动时会报chdir(): Permission denied。本文 4.7 节记录了这一问题的完整复现与修复。
三、版本选型说明
OpenWISP 套件由多个 Python 包组成,安装时必须锁定主包版本,让依赖解析器选择与之匹配的配套包。本次实测的版本矩阵如下(pip list 实测):
| 包 | 版本 | 说明 |
|---|---|---|
| openwisp-controller | 1.3.2 | 主包,设备与配置管理 |
| openwisp-utils | 1.3.1 | 后台主题、API 基础设施 |
| openwisp-users | 1.3.2 | 多组织与权限 |
| openwisp-notifications | 1.3 | 站内/邮件通知 |
| openwisp-ipam | 1.3.1 | 子网与 IP 管理 |
| Django | 5.2.18 | Web 框架 |
| celery | 5.6.3 | 异步任务 |
| channels / channels_redis | 4.3.2 / 4.3.0 | WebSocket 通知 |
| redis(Python 客户端) | 8.1.0 | 缓存与会话 |
| psycopg | 3.3.6 | PostgreSQL 驱动(含 binary 变体) |
选择 1.3.2 的理由:它是 2026-10-28 时 PyPI 上的最新稳定版(pip index versions 实测输出 LATEST: 1.3.2),且 1.3.1 → 1.3.2 只隔 8 天,属于 1.3.0 之后的连续修复,风险低于直接追开发分支。若需要长期稳定,可锁定 1.3.2 并把依赖锁文件一并归档(见第五章)。
💡 技术拓展:Python 应用的版本锁定有四种常见做法——完全不锁(
pip install最新)、锁主包(本文做法)、锁全量依赖(pip freeze生成锁文件)、锁哈希(pip-compile --generate-hashes)。不锁最省事但不可复现;锁主包能保证主版本一致;锁全量依赖可复现但升级需整表重算;锁哈希安全性最高但维护成本大。本文在线方案锁主包,离线方案额外提供全量锁文件,兼顾可复现与可维护。
四、方案 A:venv 在线安装(主方案)
4.1 方案说明与适用范围
本方案直接在目标服务器上安装系统依赖,用 Python 虚拟环境安装 OpenWISP,全部组件由 systemd 托管。优点是路径透明、便于定制 settings.py、升级只需替换虚拟环境;缺点是需要服务器能访问 PyPI,且首次安装要编译少量 Python 依赖。
适用于:能访问公网或内网有 PyPI 软件源、需要深度定制配置、希望用系统包管理器统一维护数据库与 Redis 的团队。
4.2 系统依赖安装
先更新索引并安装数据库、缓存、应用服务器与编译工具。postgresql-14-postgis-3 是 Ubuntu 22.04 仓库中与 PostgreSQL 14 匹配的 PostGIS 包,必须与 postgresql 同版本安装。
sudo apt-get update
sudo apt-get install -y postgresql postgresql-contrib postgis postgresql-14-postgis-3 \
redis-server nginx uwsgi uwsgi-plugin-python3 \
python3-venv python3-dev build-essential \
libpq-dev libjpeg-dev zlib1g-dev libssl-dev libffi-dev \
gettext curl git
实测输出节选(安装过程共 263 个包被 Setting up,末尾为):
0 added, 0 removed; done.
Running hooks in /etc/ca-certificates/update.d...
done.
验证安装结果与版本:
dpkg -l postgresql-14 postgresql-14-postgis-3 postgis redis-server nginx uwsgi uwsgi-plugin-python3 python3.10-venv | grep ^ii | awk '{print $2, $3}'
实测回显:
nginx 1.18.0-6ubuntu14.22
postgis 3.2.0+dfsg-1ubuntu1
postgresql-14 14.24-0ubuntu0.22.04.1
postgresql-14-postgis-3 3.2.0+dfsg-1ubuntu1
python3.10-venv 3.10.12-1~22.04.18
redis-server 5:6.0.16-1ubuntu1.2
uwsgi 2.0.20-4
uwsgi-plugin-python3 2.0.20-4
⚠️ 注意:Ubuntu 22.04 官方仓库里没有
openwisp-controller的 apt 包,必须走 Python 包安装。若在教程里看到apt install openwisp-controller,那不是官方渠道。
4.3 创建虚拟环境并安装 Python 包
虚拟环境放在 /opt/openwisp/venv,与项目目录分离,升级时可整体替换。安装时显式指定 openwisp-controller==1.3.2,避免解析到更新的次版本。
sudo mkdir -p /opt/openwisp
sudo python3 -m venv /opt/openwisp/venv
sudo /opt/openwisp/venv/bin/pip install --upgrade pip
sudo /opt/openwisp/venv/bin/pip install "openwisp-controller==1.3.2" "psycopg[binary]"
实测输出节选(末尾):
Successfully built dj-rest-auth
Installing collected packages: swapper, rjsmin, rcssmin, pytz, django-sortedm2m, brotli, ... , openwisp-controller
Successfully installed ... django-5.2.18 ... openwisp-controller-1.3.2 openwisp-ipam-1.3.1 openwisp-notifications-1.3 openwisp-users-1.3.2 openwisp-utils-1.3.1 ...
验证关键包版本:
/opt/openwisp/venv/bin/pip show openwisp-controller | head -3
/opt/openwisp/venv/bin/pip list | grep -Ei "^(Django|celery|openwisp-|psycopg|redis) "
实测回显节选:
Name: openwisp-controller
Version: 1.3.2
Summary: OpenWISP Controller
celery 5.6.3
Django 5.2.18
openwisp-controller 1.3.2
openwisp-ipam 1.3.1
openwisp-notifications 1.3
openwisp-users 1.3.2
openwisp-utils 1.3.1
psycopg 3.3.6
redis 8.1.0
⚠️ 注意:
psycopg[binary]必须在首次manage.py check之前装好,否则会出现django.core.exceptions.ImproperlyConfigured: Error loading psycopg2 or psycopg module。这是本次实测真实踩到的第一个坑,定位过程记录在第九章。
4.4 PostgreSQL 14 与 PostGIS 3.2 初始化
OpenWISP 依赖 GeoDjango 的空间字段,必须使用支持 PostGIS 的数据库后端。先创建独立数据库与账号,再在建好的库里启用 postgis 扩展。
sudo systemctl enable --now postgresql
sudo -u postgres psql -c "CREATE USER openwisp WITH PASSWORD 'q1w2e3r4';"
sudo -u postgres psql -c "CREATE DATABASE openwisp OWNER openwisp;"
sudo -u postgres psql -d openwisp -c "CREATE EXTENSION postgis;"
实测回显:
CREATE ROLE
CREATE DATABASE
CREATE EXTENSION
验证扩展版本:
sudo -u postgres psql -d openwisp -tAc "select postgis_version()"
实测回显:
3.2 USE_GEOS=1 USE_PROJ=1 USE_STATS=1
继续启动并验证缓存与代理服务:
sudo systemctl enable --now redis-server
redis-cli ping
实测回显:
PONG
💡 说明:
CREATE EXTENSION postgis必须在目标库内执行,且数据库属主需要有建扩展权限。若报could not open extension control file,说明只装了postgis元包而没装postgresql-14-postgis-3,补装对应版本即可。
4.5 项目骨架与 settings.py
在 /opt/openwisp/app 下建立一个最小 Django 项目,包名 openwisp2。目录结构如下:
/opt/openwisp/app
├── manage.py
├── openwisp2
│ ├── __init__.py
│ ├── asgi.py
│ ├── celery.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
├── static/ # collectstatic 输出
└── media/ # 上传文件
manage.py 只做一件事:把 DJANGO_SETTINGS_MODULE 指向 openwisp2.settings,然后交给 Django 的命令行入口。
#!/usr/bin/env python
import os
import sys
if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "openwisp2.settings")
from django.core.management import execute_from_command_line
execute_from_command_line(sys.argv)
openwisp2/__init__.py 暴露 Celery 应用,保证 celery -A openwisp2 能找到它:
from .celery import app as celery_app
__all__ = ["celery_app"]
openwisp2/celery.py 从 Django settings 读取 CELERY_* 配置并自动发现任务:
import os
from celery import Celery
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "openwisp2.settings")
app = Celery("openwisp2")
app.config_from_object("django.conf:settings", namespace="CELERY")
app.autodiscover_tasks()
openwisp2/urls.py 把根路径重定向到后台,并挂载 Controller、账号、REST API 三组路由:
from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.urls import include, path, reverse_lazy
from django.views.generic import RedirectView
redirect_view = RedirectView.as_view(url=reverse_lazy("admin:index"))
urlpatterns = [
path("", redirect_view, name="index"),
path("admin/", admin.site.urls),
path("", include("openwisp_controller.urls")),
path("accounts/", include("openwisp_users.accounts.urls")),
path("api/v1/", include("openwisp_utils.api.urls")),
path("api/v1/", include(("openwisp_users.api.urls", "users"), namespace="users")),
]
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
openwisp2/asgi.py 为 Channels 提供 WebSocket 路由(通知实时推送依赖它):
from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.security.websocket import AllowedHostsOriginValidator
from django.core.asgi import get_asgi_application
from openwisp_controller.routing import get_routes
application = ProtocolTypeRouter(
{
"websocket": AllowedHostsOriginValidator(
AuthMiddlewareStack(URLRouter(get_routes()))
),
"http": get_asgi_application(),
}
)
openwisp2/wsgi.py 供 uWSGI 加载:
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "openwisp2.settings")
application = get_wsgi_application()
核心配置文件 openwisp2/settings.py 全文如下(数据库口令、密钥等按本次实测环境填写,逐项含义见 《OpenWISP 1.3.2 配置参数详解》):
import os
from celery.schedules import crontab
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
DEBUG = False
SECRET_KEY = "eCoNcO14Z708OB5lBIWGTrANmXV5rZhl3881ees3bOphf6V4StGsfNOzCNz11GC6fRU"
ALLOWED_HOSTS = ["openwisp.example.com", "127.0.0.1", "localhost"]
DATABASES = {
"default": {
"ENGINE": "django.contrib.gis.db.backends.postgis",
"NAME": "openwisp",
"USER": "openwisp",
"PASSWORD": "q1w2e3r4",
"HOST": "127.0.0.1",
"PORT": "5432",
}
}
REDIS_URL = "redis://127.0.0.1:6379"
INSTALLED_APPS = [
"daphne",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"django.contrib.gis",
"django.contrib.sites",
"openwisp_users.accounts",
"allauth",
"allauth.account",
"allauth.socialaccount",
"django_extensions",
"openwisp_users",
"openwisp_controller.config",
"openwisp_controller.pki",
"openwisp_controller.geo",
"openwisp_controller.connection",
"openwisp_controller.subnet_division",
"openwisp_notifications",
"openwisp_ipam",
"openwisp_utils.admin_theme",
"admin_auto_filters",
"django.contrib.admin",
"django.forms",
"sortedm2m",
"reversion",
"leaflet",
"flat_json_widget",
"rest_framework",
"rest_framework.authtoken",
"rest_framework_gis",
"django_filters",
"drf_yasg",
"channels",
"import_export",
]
EXTENDED_APPS = ("django_x509", "django_loci")
AUTH_USER_MODEL = "openwisp_users.User"
SITE_ID = 1
STATICFILES_FINDERS = [
"django.contrib.staticfiles.finders.FileSystemFinder",
"django.contrib.staticfiles.finders.AppDirectoriesFinder",
"openwisp_utils.staticfiles.DependencyFinder",
]
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"allauth.account.middleware.AccountMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
INTERNAL_IPS = ["127.0.0.1"]
ROOT_URLCONF = "openwisp2.urls"
ASGI_APPLICATION = "openwisp2.asgi.application"
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels_redis.core.RedisChannelLayer",
"CONFIG": {"hosts": [f"{REDIS_URL}/7"]},
}
}
TIME_ZONE = "Asia/Shanghai"
LANGUAGE_CODE = "zh-hans"
USE_TZ = True
USE_I18N = True
STATIC_URL = "/static/"
MEDIA_URL = "/media/"
STATIC_ROOT = os.path.join(BASE_DIR, "static")
MEDIA_ROOT = os.path.join(BASE_DIR, "media")
CORS_ORIGIN_ALLOW_ALL = True
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"OPTIONS": {
"loaders": [
"django.template.loaders.filesystem.Loader",
"openwisp_utils.loaders.DependencyLoader",
"django.template.loaders.app_directories.Loader",
],
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
"openwisp_utils.admin_theme.context_processor.menu_groups",
"openwisp_notifications.context_processors.notification_api_settings",
],
},
}
]
LEAFLET_CONFIG = {"RESET_VIEW": False}
FORM_RENDERER = "django.forms.renderers.TemplatesSetting"
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
EMAIL_HOST = "127.0.0.1"
EMAIL_PORT = 25
EMAIL_USE_TLS = False
DEFAULT_FROM_EMAIL = "openwisp@openwisp.example.com"
LOGIN_REDIRECT_URL = "admin:index"
ACCOUNT_LOGOUT_REDIRECT_URL = LOGIN_REDIRECT_URL
OPENWISP_ORGANIZATION_USER_ADMIN = True
OPENWISP_ADMIN_DASHBOARD_ENABLED = True
OPENWISP_CONTROLLER_GROUP_PIE_CHART = True
OPENWISP_USERS_AUTH_API = True
OPENWISP_NOTIFICATIONS_EMAIL_ENABLED = True
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.redis.RedisCache",
"LOCATION": f"{REDIS_URL}/6",
},
"sessions": {
"BACKEND": "django.core.cache.backends.redis.RedisCache",
"LOCATION": f"{REDIS_URL}/8",
},
}
SESSION_ENGINE = "django.contrib.sessions.backends.cache"
SESSION_CACHE_ALIAS = "sessions"
CELERY_BROKER_URL = f"{REDIS_URL}/1"
CELERY_RESULT_BACKEND = f"{REDIS_URL}/2"
CELERY_TASK_TIME_LIMIT = 600
CELERY_BEAT_SCHEDULE = {
"cleanup_unreferenced_whois_records": {
"task": "openwisp_controller.config.whois.tasks.cleanup_unreferenced_whois_records",
"schedule": crontab(hour=2, minute=0),
},
}
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"verbose": {
"format": "{levelname} {asctime} {module} {process:d} {message}",
"style": "{",
},
},
"handlers": {
"console": {"class": "logging.StreamHandler", "formatter": "verbose"},
"file": {
"class": "logging.handlers.RotatingFileHandler",
"filename": "/var/log/openwisp/openwisp.log",
"maxBytes": 10485760,
"backupCount": 5,
"formatter": "verbose",
},
},
"root": {"handlers": ["console", "file"], "level": "INFO"},
"loggers": {
"django": {"handlers": ["console", "file"], "level": "INFO", "propagate": False},
},
}
关键块说明:
DATABASES的ENGINE必须是django.contrib.gis.db.backends.postgis,用普通django.db.backends.postgresql会在迁移空间字段时报错。INSTALLED_APPS中openwisp_utils.admin_theme必须排在django.contrib.admin之前,否则后台主题不生效。CHANNEL_LAYERS、CACHES、CELERY_BROKER_URL分别占用 Redis 的 7、6/8、1 号库,避免键空间互相污染。STATIC_ROOT与MEDIA_ROOT落在项目目录内,便于 Nginx 用alias直接托管。
创建目录并赋权:
sudo mkdir -p /opt/openwisp/app/static /opt/openwisp/app/media /var/log/openwisp
sudo chown -R www-data:www-data /opt/openwisp/app/media /var/log/openwisp
语法检查:
cd /opt/openwisp/app
sudo -u www-data /opt/openwisp/venv/bin/python manage.py check
实测回显:
System check identified no issues (0 silenced).
⚠️ 注意:如果此时报
Error loading psycopg2 or psycopg module,说明 PostgreSQL 驱动缺失,执行/opt/openwisp/venv/bin/pip install "psycopg[binary]"后重试即可。本次实测正是先装了主包、后补驱动。
4.6 数据库初始化(迁移、超级用户、静态文件)
迁移会把 17 个应用的 189 条迁移记录写入数据库。执行时间取决于磁盘,本次约数十秒。
cd /opt/openwisp/app
sudo -u www-data /opt/openwisp/venv/bin/python manage.py migrate
实测输出节选(共 189 行):
Operations to perform:
Apply all migrations: account, admin, auth, authtoken, config, connection, contenttypes, geo, openwisp_ipam, openwisp_notifications, openwisp_users, pki, reversion, sessions, sites, socialaccount, subnet_division
Running migrations:
Applying contenttypes.0001_initial... OK
Applying contenttypes.0002_remove_content_type_name... OK
Applying auth.0001_initial... OK
...
Applying subnet_division.0004_index_rule_on_delete... OK
Applying subnet_division.0005_number_of_subnets_and_ips... OK
创建后台管理员。--noinput 从环境变量读取密码,避免交互式输入:
cd /opt/openwisp/app
sudo -u www-data DJANGO_SUPERUSER_PASSWORD=q1w2e3r4 /opt/openwisp/venv/bin/python manage.py createsuperuser \
--noinput --username admin --email admin@example.com
实测回显:
Superuser created successfully.
确认账号已落库:
sudo -u www-data /opt/openwisp/venv/bin/python manage.py shell -c "from openwisp_users.models import User; print(User.objects.filter(is_superuser=True).values_list('username','email'))"
实测回显:
<QuerySet [('admin', 'admin@example.com')]>
收集静态文件(后台主题、Leaflet、DRF 等资源):
sudo -u www-data /opt/openwisp/venv/bin/python manage.py collectstatic --noinput
实测回显:
413 static files copied to '/opt/openwisp/app/static'.
验证静态目录体积与文件数:
du -sh /opt/openwisp/app/static
find /opt/openwisp/app/static -type f | wc -l
实测回显:
31M /opt/openwisp/app/static
413
4.7 uWSGI 与 systemd 托管
uWSGI 通过 Unix Socket 与 Nginx 通信,不直接暴露 TCP 端口。配置放在 /etc/openwisp/uwsgi.ini:
[uwsgi]
project = openwisp2
base = /opt/openwisp/app
chdir = %(base)
module = %(project).wsgi:application
home = /opt/openwisp/venv
pythonpath = %(base)
plugins = python3
master = true
processes = 4
threads = 2
socket = /run/uwsgi/openwisp.sock
chmod-socket = 660
chown-socket = www-data:www-data
vacuum = true
die-on-term = true
max-requests = 5000
harakiri = 60
logto = /var/log/openwisp/uwsgi.log
逐项说明:home 指向虚拟环境,保证能 import openwisp;processes=4 与 threads=2 对应 6 vCPU 的实测主机,可按 CPU 数调整为 2 × vCPU;harakiri=60 防止长请求挂死;max-requests=5000 让 worker 定期回收,规避第三方库内存缓慢增长。
systemd 单元 /etc/systemd/system/uwsgi-openwisp.service:
[Unit]
Description=uWSGI instance for OpenWISP Controller
After=network.target postgresql.service redis-server.service
[Service]
User=www-data
Group=www-data
RuntimeDirectory=uwsgi
RuntimeDirectoryMode=0755
ExecStart=/usr/bin/uwsgi --ini /etc/openwisp/uwsgi.ini
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
启用并启动:
sudo systemctl daemon-reload
sudo systemctl enable --now uwsgi-openwisp
sudo systemctl is-active uwsgi-openwisp
sudo tail -5 /var/log/openwisp/uwsgi.log
排障实录(首次启动失败):第一次启动时 is-active 返回 activating,日志报目录权限不足:
chdir() to /opt/openwisp/app
chdir(): Permission denied [core/uwsgi.c line 2625]
chdir(): Permission denied [core/uwsgi.c line 1649]
原因是 /opt/openwisp 为 755 但 /opt/openwisp/app 是 root:root 750,www-data 无法进入。修复:
sudo chmod 755 /opt/openwisp
sudo chown -R www-data:www-data /opt/openwisp/app
sudo systemctl restart uwsgi-openwisp
修复后实测回显:
active
WSGI app 0 (mountpoint='') ready in 0 seconds on interpreter 0x613b5ecd8800 pid: 7859 (default app)
*** uWSGI is running in multiple interpreter mode ***
spawned uWSGI master process (pid: 7859)
spawned uWSGI worker 1 (pid: 7863, cores: 2)
spawned uWSGI worker 2 (pid: 7865, cores: 2)
spawned uWSGI worker 3 (pid: 7867, cores: 2)
spawned uWSGI worker 4 (pid: 7869, cores: 2)
Socket 文件确认:
ls -l /run/uwsgi/
实测回显:
srw-rw---- 1 www-data www-data 0 Oct 27 18:10 openwisp.sock
4.8 Celery worker 与 beat
配置变更推送、通知生成、WHOIS 清理都依赖 Celery。worker 单元 /etc/systemd/system/celery-openwisp.service:
[Unit]
Description=Celery worker for OpenWISP Controller
After=network.target postgresql.service redis-server.service
[Service]
Type=simple
User=www-data
Group=www-data
WorkingDirectory=/opt/openwisp/app
ExecStart=/opt/openwisp/venv/bin/celery -A openwisp2 worker --loglevel=info --concurrency=2
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
定时任务单元 /etc/systemd/system/celery-beat-openwisp.service:
[Unit]
Description=Celery beat scheduler for OpenWISP Controller
After=network.target redis-server.service
[Service]
Type=simple
User=www-data
Group=www-data
WorkingDirectory=/opt/openwisp/app
ExecStart=/opt/openwisp/venv/bin/celery -A openwisp2 beat --loglevel=info
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
启动并验证:
sudo systemctl daemon-reload
sudo systemctl enable --now celery-openwisp
sudo systemctl enable --now celery-beat-openwisp
sudo systemctl is-active celery-openwisp celery-beat-openwisp
cd /opt/openwisp/app
sudo -u www-data /opt/openwisp/venv/bin/celery -A openwisp2 inspect ping --timeout 8
实测回显:
active
active
-> celery@openwisp-lab: OK
pong
1 node online.
⚠️ 注意:beat 使用默认的
PersistentScheduler,调度项来自CELERY_BEAT_SCHEDULE。如果改用数据库调度器,必须先安装django-celery-beat并把django_celery_beat加入INSTALLED_APPS,否则会报ModuleNotFoundError。
4.9 Nginx 反向代理
站点配置放 /etc/nginx/sites-available/openwisp.conf,静态与媒体由 Nginx 直接返回,其余请求转发给 uWSGI Socket:
server {
listen 80;
server_name openwisp.example.com;
client_max_body_size 20M;
access_log /var/log/nginx/openwisp.access.log;
error_log /var/log/nginx/openwisp.error.log;
location /static/ {
alias /opt/openwisp/app/static/;
expires 7d;
access_log off;
}
location /media/ {
alias /opt/openwisp/app/media/;
expires 1d;
}
location / {
include uwsgi_params;
uwsgi_pass unix:/run/uwsgi/openwisp.sock;
uwsgi_read_timeout 180;
uwsgi_send_timeout 180;
}
}
启用站点并移除默认站点:
sudo ln -s /etc/nginx/sites-available/openwisp.conf /etc/nginx/sites-enabled/openwisp.conf
sudo rm -f /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl restart nginx
实测回显:
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
💡 说明:
uwsgi_read_timeout 180是给配置推送、导入导出这类长请求留余量。OpenWISP 后台的批量操作可能超过默认 60 秒,超时表现为 Nginx 返回 504 而应用日志仍在执行。
4.10 端到端验证
验证一:首页重定向与登录页
curl -s -o /dev/null -w "%{http_code} %{redirect_url}\n" -H "Host: openwisp.example.com" http://127.0.0.1/
curl -s -H "Host: openwisp.example.com" http://127.0.0.1/admin/login/ | grep -o "<title>[^<]*</title>"
实测回显:
302 http://127.0.0.1/admin/
<title>登录 | OpenWISP Admin</title>
验证二:静态资源
curl -s -o /dev/null -w "%{http_code} %{size_download}\n" -H "Host: openwisp.example.com" http://127.0.0.1/static/admin/css/base.css
实测回显:
200 22120
验证三:真实登录并进入后台
用带 CSRF 的会话完成登录(也可直接用浏览器,账号 admin、密码 q1w2e3r4):
/opt/openwisp/venv/bin/python - <<'PY'
import re, requests
s = requests.Session(); h = {"Host": "openwisp.example.com"}
s.get("http://127.0.0.1/admin/login/?next=/admin/", headers=h)
d = {"username": "admin", "password": "q1w2e3r4",
"csrfmiddlewaretoken": s.cookies["csrftoken"], "next": "/admin/"}
r = s.post("http://127.0.0.1/admin/login/?next=/admin/", data=d,
headers={**h, "Referer": "http://openwisp.example.com/admin/login/"},
allow_redirects=False)
print("login", r.status_code, r.headers.get("Location"))
r2 = s.get("http://127.0.0.1/admin/", headers=h)
print("dash", r2.status_code, len(r2.text))
print(re.search(r"<title>(.*?)</title>", r2.text, re.S).group(1).strip())
PY
实测回显:
login 302 /admin/
dash 200 29719
Network Administration | OpenWISP Admin
验证四:业务页面可访问
登录后依次访问设备、模板、组织、VPN 列表页,均返回 200:
/admin/config/device/ 200 选择 Device 来修改 | OpenWISP Admin
/admin/config/template/ 200 选择 template 来修改 | OpenWISP Admin
/admin/openwisp_users/organization/ 200 选择 organization 来修改 | OpenWISP Admin
/admin/config/vpn/ 200 选择 VPN server 来修改 | OpenWISP Admin
后台显示中文菜单,说明 LANGUAGE_CODE = "zh-hans" 已生效。
验证五:服务状态
systemctl status uwsgi-openwisp --no-pager
实测输出节选(已隐去与业务无关的 cgroup 路径):
● uwsgi-openwisp.service - uWSGI instance for OpenWISP Controller
Loaded: loaded (/etc/systemd/system/uwsgi-openwisp.service; enabled; vendor preset: enabled)
Active: active (running) since Tue 2026-10-27 18:10:56 UTC; 3min 38s ago
Main PID: 7859 (uwsgi)
Tasks: 9 (limit: 2932)
Memory: 206.5M
CPU: 1.158s
worker 与 beat 的常驻内存实测分别为 224.3M 与 118.0M。整套服务(uWSGI + worker + beat)空闲内存约 550 MB,2 GB 内存的机器可跑,4 GB 更稳妥。
五、方案 B:离线 wheel 安装(内网方案)
5.1 方案说明
内网服务器无法访问 PyPI 时,在可联网机器上把全量依赖下载成 wheel 集合,拷入内网后用 --no-index 安装。该方案与方案 A 的差异只在“包从哪来”,数据库、配置、systemd、Nginx 完全相同。
5.2 在联网机准备 wheel 包与锁文件
先在一个与目标同版本(Ubuntu 22.04 / Python 3.10)的环境里装好方案 A,再导出锁文件:
/opt/openwisp/venv/bin/pip freeze > /opt/openwisp/requirements-lock.txt
wc -l /opt/openwisp/requirements-lock.txt
实测回显:
119 /opt/openwisp/requirements-lock.txt
下载全部依赖:
mkdir -p /opt/openwisp/wheels
/opt/openwisp/venv/bin/pip download -d /opt/openwisp/wheels -r /opt/openwisp/requirements-lock.txt
实测输出节选(末尾):
Successfully downloaded aiohappyeyeballs aiohttp async-timeout multidict yarl aiosignal amqp vine asgiref attrs autobahn ... openwisp-controller openwisp-ipam openwisp-notifications openwisp-users ... psycopg psycopg-binary ... setuptools
检查产物数量与体积:
ls /opt/openwisp/wheels | wc -l
du -sh /opt/openwisp/wheels
实测回显:
120
61M /opt/openwisp/wheels
把 wheels/ 目录与 requirements-lock.txt 一起拷到内网服务器(U 盘、内网文件服务均可)。
5.3 内网安装与验证
在目标机创建独立虚拟环境,只从本地目录安装:
sudo python3 -m venv /opt/openwisp/venv-offline
sudo /opt/openwisp/venv-offline/bin/pip install --no-index --find-links=/opt/openwisp/wheels \
-r /opt/openwisp/requirements-lock.txt
sudo /opt/openwisp/venv-offline/bin/pip install --no-index --find-links=/opt/openwisp/wheels "psycopg[binary]"
实测输出节选(末尾):
Running setup.py install for dj-rest-auth: finished with status 'done'
Successfully installed ... django-5.2.18 ... openwisp-controller-1.3.2 ... psycopg-3.3.6 psycopg-binary-3.3.6 ...
用离线环境做一致性检查:
cd /opt/openwisp/app
/opt/openwisp/venv-offline/bin/python manage.py check
实测回显:
System check identified no issues (0 silenced).
确认可用后,把 4.7 与 4.8 节服务单元里的 home/ExecStart 路径从 /opt/openwisp/venv 改为 /opt/openwisp/venv-offline,重启三个服务即可。也可以直接把离线环境改名为 /opt/openwisp/venv,这样所有配置无需改动。
⚠️ 注意:
pip download的产物里可能混有源码分发(sdist),安装时需要build-essential与python3-dev。本次实测离线安装时出现了Running setup.py install for dj-rest-auth,说明该包走的是源码构建路径,内网机器必须预装编译工具链。
六、方案对比与选择建议
| 维度 | 方案 A:在线 venv | 方案 B:离线 wheel |
|---|---|---|
| 网络要求 | 需访问 PyPI | 仅需一次性拷贝 |
| 安装耗时 | 取决于网速,本次约数分钟 | 本地安装,约 1 分钟 |
| 版本可复现性 | 锁主包,依赖可能漂移 | 锁全量依赖,完全可复现 |
| 维护成本 | 低(pip install -U 即可升级) | 中(每次升级重做 wheel 集) |
| 适用场景 | 有公网或内网 PyPI 软件源 | 隔离网络、审计要求高的环境 |
选择建议:能联网选方案 A,隔离网络选方案 B;无论选哪个,都应保留 requirements-lock.txt,它是回滚时唯一的版本依据。
七、配置说明(路径、端口与服务清单)
安装完成后,平台的“可变部分”集中在四个位置,日常运维只需记住这张表:
| 位置 | 内容 | 修改后动作 |
|---|---|---|
/opt/openwisp/app/openwisp2/settings.py | 数据库、缓存、时区、邮件、OPENWISP_* 开关 | 重启 uWSGI、worker、beat |
/etc/openwisp/uwsgi.ini | 进程数、超时、Socket 路径、日志 | 重启 uwsgi-openwisp |
/etc/systemd/system/*.service | 启动用户、工作目录、重启策略 | systemctl daemon-reload 后重启 |
/etc/nginx/sites-available/openwisp.conf | 域名、静态/媒体路径、超时 | nginx -t 后 reload |
三个 systemd 单元的职责边界:
uwsgi-openwisp:唯一对外提供 HTTP 的进程,Nginx 通过 Socket 转发。celery-openwisp:执行异步任务(推送配置、发通知、清理数据),不监听端口。celery-beat-openwisp:只负责按CELERY_BEAT_SCHEDULE投递定时任务,不执行任务本身。
验证三者关系是否正常:
systemctl is-active uwsgi-openwisp celery-openwisp celery-beat-openwisp
sudo -u www-data /opt/openwisp/venv/bin/celery -A openwisp2 inspect registered | head -5
实测回显节选:
active
active
active
-> celery@openwisp-lab: OK
* openwisp_controller.config.tasks.auto_add_template_to_existing_configs
* openwisp_controller.config.tasks.bulk_invalidate_config_get_cached_checksum
* openwisp_controller.config.tasks.change_devices_templates
* openwisp_controller.config.tasks.create_vpn_dh
* openwisp_controller.config.tasks.invalidate_controller_views_cache
* openwisp_controller.config.tasks.invalidate_devicegroup_cache_change
* openwisp_controller.config.tasks.invalidate_devicegroup_cache_delete
* openwisp_controller.config.tasks.invalidate_vpn_server_devices_cache_change
* openwisp_controller.config.tasks.trigger_vpn_server_endpoint
* openwisp_controller.config.tasks.update_template_related_config_status
* openwisp_controller.config.tasks_zerotier.trigger_zerotier_server_delete
八、升级与回滚
升级前先确认可用版本(联网环境):
/opt/openwisp/venv/bin/pip index versions openwisp-controller | head -3
实测回显:
openwisp-controller (1.3.2)
Available versions: 1.3.2, 1.3.1, 1.3, 1.2.3, ...
INSTALLED: 1.3.2
LATEST: 1.3.2
标准升级流程(以 1.3.2 → 下一个次版本为例):
sudo -u postgres pg_dump -Fc -d openwisp -f /var/backups/openwisp/openwisp-before-upgrade.dump
sudo /opt/openwisp/venv/bin/pip install --upgrade "openwisp-controller==<目标版本>"
cd /opt/openwisp/app
sudo -u www-data /opt/openwisp/venv/bin/python manage.py migrate
sudo -u www-data /opt/openwisp/venv/bin/python manage.py collectstatic --noinput
sudo systemctl restart uwsgi-openwisp celery-openwisp celery-beat-openwisp
回滚顺序与升级相反:停服务 → 卸载新版本并装回旧版本 → 用升级前的 dump 恢复数据库 → 重启。由于 OpenWISP 的迁移不可逆,回滚必须依赖备份,不能只靠 migrate <app> zero。
⚠️ 注意:本次实测环境已是 1.3.2(最新版),没有更高版本可升,因此升级步骤未实际执行;命令与版本查询结果均来自真实环境,恢复流程在 《OpenWISP 1.3.2 常用 CLI 与运维命令详解》 中有完整备份/恢复实测。
九、常见问题(排障实录)
9.1 报错 Error loading psycopg2 or psycopg module
现象:首次执行 manage.py check 直接抛异常,堆栈末尾为:
django.core.exceptions.ImproperlyConfigured: Error loading psycopg2 or psycopg module
定位:pip list 里没有 psycopg/psycopg2,说明只装了 openwisp-controller,未装数据库驱动。Django 5.2 要求显式安装 psycopg 或 psycopg2。
解决:/opt/openwisp/venv/bin/pip install "psycopg[binary]",再执行 manage.py check 即通过。
9.2 uWSGI 启动即失败,日志 chdir(): Permission denied
现象:systemctl is-active 卡在 activating,/var/log/openwisp/uwsgi.log 报:
chdir(): Permission denied [core/uwsgi.c line 2625]
定位:uWSGI 以 www-data 运行,但 /opt/openwisp/app 是 root:root 750,无法进入。
解决:chmod 755 /opt/openwisp,chown -R www-data:www-data /opt/openwisp/app,重启服务。
9.3 manage.py check --deploy 出现地理编码告警
现象:输出末尾出现:
?: Geocoding service is experiencing issues or is not properly configured
System check identified 5 issues (0 silenced).
定位:OpenWISP 注册了一个地理编码健康检查,会访问 geocode.arcgis.com;隔离网络下报 Network is unreachable,堆栈为 geopy.exc.GeocoderUnavailable。
解决:该告警不影响启动与登录。处理方式二选一:为服务器放通 geocode.arcgis.com:443;或接受告警,在设备位置里手工填经纬度,不走地址自动解析。另外 4 条告警是 Django 的 HTTPS 相关提示(HSTS、SSL 重定向、Secure Cookie),上生产启用 TLS 后按提示补齐即可。
9.4 后台能打开但样式全丢
现象:页面结构正常、无 CSS,浏览器控制台大量 404 指向 /static/...。
定位:collectstatic 未执行,或 Nginx 的 alias 路径与实际 STATIC_ROOT 不一致。
解决:确认 settings.py 中 STATIC_ROOT = /opt/openwisp/app/static,重新执行 collectstatic --noinput,再 curl -o /dev/null -w "%{http_code}" http://127.0.0.1/static/admin/css/base.css 应返回 200。
9.5 502 Bad Gateway
现象:Nginx 返回 502,/var/log/nginx/openwisp.error.log 报 connect() to unix:/run/uwsgi/openwisp.sock failed (2: No such file or directory)。
定位:uWSGI 未启动,或 Socket 路径与 Nginx 配置不一致。
解决:systemctl status uwsgi-openwisp 查看原因;若服务正常但 Socket 不存在,检查 uwsgi.ini 的 socket 行与 Nginx uwsgi_pass 是否指向同一路径。
十、卸载与清理
卸载按“停服务 → 删配置 → 删代码 → 删数据库”顺序执行,避免残留进程占用文件:
sudo systemctl disable --now uwsgi-openwisp celery-openwisp celery-beat-openwisp
sudo rm -f /etc/systemd/system/uwsgi-openwisp.service
sudo rm -f /etc/systemd/system/celery-openwisp.service
sudo rm -f /etc/systemd/system/celery-beat-openwisp.service
sudo systemctl daemon-reload
sudo rm -f /etc/nginx/sites-enabled/openwisp.conf /etc/nginx/sites-available/openwisp.conf
sudo systemctl reload nginx
sudo rm -rf /opt/openwisp
sudo rm -rf /var/log/openwisp
sudo -u postgres psql -c "DROP DATABASE openwisp;"
sudo -u postgres psql -c "DROP USER openwisp;"
⚠️ 注意:以上命令会永久删除数据库与全部配置,本次实测环境保留用于复查,未执行删除。若还要保留备份,先执行
pg_dump -Fc并把文件移出/var/backups/openwisp。PostGIS 扩展随数据库删除,系统包postgresql-14-postgis-3可按需保留给其他库使用。
十一、参考链接
- OpenWISP 安装器总览:
https://openwisp.io/docs/stable/installers.html - Controller 开发安装(依赖说明):
https://openwisp.io/docs/stable/controller/developer/installation.html - Controller 配置项:
https://openwisp.io/docs/stable/controller/user/settings.html - PyPI 包页:
https://pypi.org/project/openwisp-controller/ - GitHub Releases:
https://github.com/openwisp/openwisp-controller/releases - Django 5.2 数据库后端文档:
https://docs.djangoproject.com/en/5.2/ref/databases/ - PostGIS 官方文档:
https://postgis.net/documentation/ - uWSGI 配置文档:
https://uwsgi-docs.readthedocs.io/en/latest/Options.html
站内延伸阅读:《OpenWISP 介绍与选型指南》、《OpenWISP 1.3.2 配置参数详解》、《OpenWISP 1.3.2 常用 CLI 与运维命令详解》。