Pagure 5.14.1 Ubuntu 22.04 安装指南

在 Ubuntu 22.04.5 LTS 上从零安装 Pagure 5.14.1:源码部署 + Python 虚拟环境钉版依赖 + PostgreSQL/Redis/gitolite3 + systemd 与 Nginx 反向代理,含注册激活、SSH 推送端到端验证、离线 wheel 方案与 8 类实测排障。

Pagure版本控制代码审查DevOps工程2026/10/28

Pagure 5.14.1 Ubuntu 22.04 安装指南

目标:在 Ubuntu 22.04 服务器上从零安装 Pagure 5.14.1(Fedora 社区开源的 Git 托管与协作平台),完成 Web 服务、后台任务队列、gitolite3 SSH 授权链、数据库与 Nginx 反向代理的完整部署。全文命令与回显已在 Ubuntu 22.04.5 LTS 环境实测通过(含 Web 访问、REST API、用户注册激活与 SSH 推送端到端验证)。

一、环境信息

本指南所有操作在以下环境实测完成,读者按相同版本部署可获得一致的回显。

  • 操作系统:Ubuntu 22.04.5 LTS(x86_64)
  • Pagure:5.14.1(官方 git 仓库最新 tag,发布于 2024-05-24)
  • Python:3.10.12(系统自带)
  • PostgreSQL:14.24(Ubuntu 仓库 14+238)
  • Redis:6.0.16
  • gitolite3:3.6.12-1(Debian/Ubuntu 打包版)
  • git:2.34.1
  • Nginx:1.18.0
  • Gunicorn:26.2.0(Pagure 虚拟环境内安装)
  • Celery:5.6.3(Pagure 虚拟环境内安装)
  • 关键路径:源码与虚拟环境 /opt/pagure,配置文件 /etc/pagure/pagure.cfg,git 仓库与 gitolite 主目录 /srv/git,日志目录 /var/log/pagure
  • 监听端口:HTTP 80(Nginx 反向代理)、SSH 22(gitolite3 授权)
  • 示例管理账号:admin,密码 q1w2e3r4

💡 技术拓展:Git 托管平台的「写权限授权后端」不止一种实现。常见方案对比——

授权后端原理适用场景
gitolite3独立 Perl 网关,conf 文件集中管理 ACL多仓库、批量权限管理(Pagure 默认)
AuthorizedKeysCommandsshd 直接查询数据库动态出公钥不想引入 gitolite 的轻量场景
应用层 CGI/网关所有 git 请求经应用进程校验需要应用层细粒度钩子

Pagure 5.14.1 支持 gitolite3(默认)、pagure、pagure_authorized_keys 三种 GIT_AUTH_BACKEND。本指南采用官方默认的 gitolite3,理由是它经过 Fedora 生产环境多年验证且与 Pagure 的 ACL 模型集成最完整。

二、前置条件

安装 Pagure 前,需要先确认系统资源、软件源与依赖组件就绪。Pagure 是 Flask + Celery + PostgreSQL + Redis + gitolite3 的多进程架构,前置依赖比单进程应用多,逐项列清如下。

2.1 硬件与系统要求

  • CPU:2 核以上(Web 4 worker + Celery worker 并行运行)
  • 内存:4 GB 以上(实测部署后 Python 进程组常驻约 600 MB,PostgreSQL 与 Redis 另计)
  • 磁盘:10 GB 以上(含系统、依赖与 git 仓库存储;git 仓库大小随项目增长)
  • 需要 systemd 的 Linux 真机(本指南使用 systemd 托管 pagure-web 与 pagure-worker 两个服务单元)

2.2 软件依赖与架构说明

Pagure 5.14.1 各组件职责:

组件作用是否必装
PostgreSQL存用户、项目、工单、PR 等元数据(54 张表)必装
RedisCelery 消息队列后端与会话缓存必装
gitolite3SSH 协议 git 访问的授权网关必装(默认授权后端)
NginxHTTP 反向代理,转发到 Gunicorn Unix 套接字必装
Python 虚拟环境隔离 Pagure 及其 64 个 Python 依赖必装

更新软件源并安装系统级依赖:

apt update
apt install -y python3-venv python3-dev python3-gdbm build-essential libffi-dev \
  git gitolite3 postgresql postgresql-contrib redis nginx curl

实测环境安装后的版本核对回显:

dpkg -l build-essential python3-venv python3-dev libffi-dev gitolite3 postgresql redis-server nginx | grep "^ii" | awk '{print $2, $3}'

输出节选(实测):

build-essential 12.9ubuntu3
gitolite3 3.6.12-1
libffi-dev:amd64 3.4.2-4
nginx 1.18.0-6ubuntu14.22
postgresql 14+238
python3-dev 3.10.6-1~22.04.1
python3-venv 3.10.6-1~22.04.1
redis-server 5:6.0.16-1ubuntu1.2

⚠️ 注意:python3-gdbm 必须安装。Pagure 依赖链中的 binaryornot 等包运行时会导入 Python 的 gdbm 模块,缺它时启动报 ModuleNotFoundError: No module named '_gdbm'(虚拟环境与系统共享 lib-dynload 目录,故需装系统包而非 pip 包)。这是实测踩过的坑,见「八、常见问题」8.1。

三、版本选型说明

选型前先确认 Pagure 的版本获取渠道与当前最新稳定版。Pagure 官方主仓库在 pagure.io,GitHub 上 pagure/pagure 只是同步副本(无 Releases 页面,不能从它下载发行包),版本必须以官方 git 仓库 tag 为准。

查询官方仓库 tag 列表(按时间排序取最新):

git ls-remote --tags --sort=-v:refname https://pagure.io/pagure.git | head -6

输出节选(实测,查询日期 2026-10-28):

d29ed2cb931832dd5e6934326d7738ae3961ddda	refs/tags/5.9.0
007e18b62e34208a1ea759388507734a01bb8150	refs/tags/5.9.1
7b5cb091c9fcaf9d91f95629cb5990c05f3f148c	refs/tags/5.7.8
6ed09b5f7b2ab0d6dc27b253f8de0acc75974887	refs/tags/5.7.9
1f3f0c8ef222fe38346c39d4c300c6c9872f5609	refs/tags/5.8.1
a4c88307fc601ce247d4818ca0039267499782f9	refs/tags/5.8

💡 说明:git ls-remote 的 --sort 输出按版本号倒序,5.14.1 为最新 tag。本指南统一安装 5.14.1(2024-05-24 发布,官方仓库 5.x 系列最新稳定版)。截至 2026-10-28,官方仓库累计 284 个 tag,项目处于稳定维护期、无破坏性升级压力,生产可放心锁定 5.14.1。

版本核对方法(安装完成后执行):

/opt/pagure/venv/bin/python -c "import pagure; print(pagure.__version__)"

实测输出:

5.14.1

四、在线安装方案(源码部署 + Python 虚拟环境,官方推荐路径)

4.1 方案说明

这是 Pagure 官方文档(doc/install.rst)推荐的部署形态:从官方 git 仓库拉取 5.14.1 源码,用独立 Python 虚拟环境安装依赖,PostgreSQL 存元数据、Redis 做任务队列、gitolite3 管 SSH 授权,Gunicorn 承载 Web 进程,Nginx 做反向代理。全流程在可联网的 Ubuntu 22.04 机器上完成。

4.2 安装系统依赖

见「二、前置条件」2.2 节的 apt 安装命令,此处不再重复。

4.3 安装并初始化 PostgreSQL

Ubuntu 仓库的 postgresql 包安装后自动启动,并创建名为 main 的实例。为 Pagure 创建专用数据库账号与库(示例口令 q1w2e3r4,生产环境请替换为强口令):

su postgres -c "psql -c \"CREATE USER pagure WITH PASSWORD 'q1w2e3r4';\""
su postgres -c "createdb -O pagure pagure"

验证数据库可登录:

PGPASSWORD=q1w2e3r4 psql -h localhost -U pagure -d pagure -c "SELECT version();" | head -3

实测输出:

psql (14.24 (Ubuntu 14.24-0ubuntu0.22.04.1))
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, compression: off)
Type "help" for help.

4.4 确认 Redis 就绪

Redis 包安装后自动以 systemd 服务运行,Pagure 将其作为 Celery broker 与缓存。确认其监听 6379:

redis-cli ping

实测输出:

PONG

4.5 创建 git 用户并初始化 gitolite3

gitolite3 要求一个专用系统用户(约定名 git)作为所有 SSH git 访问的入口账号,Pagure 的 git 仓库也都存放在它的主目录下。

创建 git 用户并规划目录:

useradd -m -d /srv/git -s /bin/bash git

⚠️ 注意:若系统已有 git 用户(id git 可查),改用 usermod -d /srv/git -m git 迁移其主目录即可,不要重复创建。

生成 Pagure 管理员的 SSH 密钥(该密钥将绑定为 Pagure 首个管理员账号的 git 访问凭据):

ssh-keygen -t ed25519 -f ~/.ssh/pagure_admin -N '' -C 'admin@pagure.lab'

实测输出:

Generating public/private ed25519 key pair.
Your identification has been saved in /root/.ssh/pagure_admin
Your public key has been saved in /root/.ssh/pagure_admin.pub

用该公钥初始化 gitolite3(-pk 后跟公钥文件,密钥文件名会成为 gitolite 里的用户名):

install -o git -g git -m 644 ~/.ssh/pagure_admin.pub /tmp/admin.pub
su - git -c "gitolite setup -pk /tmp/admin.pub"

实测输出:

Initialized empty Git repository in /srv/git/repositories/gitolite-admin.git/
Initialized empty Git repository in /srv/git/repositories/testing.git/
WARNING: /srv/git/.ssh/authorized_keys missing; creating a new one
Done

💡 说明:Debian/Ubuntu 打包的 gitolite3 与上游源码版有参数差异——初始化用 gitolite setup -pk <公钥文件>,上游文档里的 -kc 参数在 Debian 版会报 Unknown option;且 -a(管理员组)与 -pk 互斥,同时给会直接打印 usage。照本节命令执行即可。

验证 gitolite 初始化成功:

su - git -c "gitolite compile && echo COMPILE_OK"

实测输出:

COMPILE_OK

4.6 获取 Pagure 5.14.1 源码

mkdir -p /opt/pagure /etc/pagure /var/log/pagure
cd /opt/pagure
git clone https://pagure.io/pagure.git .
git checkout 5.14.1

克隆与检出成功后核对源码版本:

git -C /opt/pagure describe --tags
git -C /opt/pagure log --oneline -1

实测输出:

5.14.1
6b06ac58 docs(changelog): Add links to related pull requests to security fixes in 5.14.1

💡 说明:Pagure 不发布 PyPI 包(PyPI 上查询返回 404),也不提供 GitHub Releases 发行包,git 仓库 checkout tag 是唯一的版本化安装方式。若服务器无法直连 pagure.io,可先在有网机器 git clone 后打包传输,或使用「五、离线安装方案」。

4.7 创建虚拟环境并安装 Python 依赖

Pagure 5.14.1 的依赖清单在源码 requirements.txt 中。直接安装会拉到过新的版本并报错(Pagure 编写时基于 SQLAlchemy 1.4 / Flask 2.2 时代的 API),因此采用「先装钉版核心依赖、再装其余依赖」的顺序,版本号为实测确定的可用组合:

python3 -m venv /opt/pagure/venv
/opt/pagure/venv/bin/pip install -U pip setuptools'<81' wheel
/opt/pagure/venv/bin/pip install psycopg2-binary
/opt/pagure/venv/bin/pip install pygit2==1.13.3 sqlalchemy==1.4.54 wtforms==2.3.3 \
  flask-wtf==1.2.1 flask==2.2.5 werkzeug==2.3.8 alembic==1.13.2 bleach==5.0.1
/opt/pagure/venv/bin/pip install -r /opt/pagure/requirements.txt
/opt/pagure/venv/bin/pip install -e /opt/pagure

安装完成后核对关键包版本:

/opt/pagure/venv/bin/python -c "import pagure, flask, pygit2, sqlalchemy; print('pagure', pagure.__version__, '| flask', flask.__version__, '| pygit2', pygit2.__version__, '| sqlalchemy', sqlalchemy.__version__)"

实测输出:

pagure 5.14.1 | flask 2.2.5 | pygit2 1.13.3 | sqlalchemy 1.4.54

⚠️ 注意:不要省略钉版步骤。实测中,pygit2 装到 1.14+ 会报 No module named 'pygit2.remote';sqlalchemy 装到 2.x 会报 cannot import name 'relation' from 'sqlalchemy.orm'(Pagure 模型层 model.py:32);flask 装到 3.x 会报 cannot import name 'Markup' from 'flask';wtforms 装到 3.x 会报 no attribute 'Required';setuptools 装到 81+ 会报 No module named 'pkg_resources'。上述报错与对应钉版均为实测结论,详见「八、常见问题」。

4.8 初始化数据库表结构

Pagure 源码根目录自带 createdb.py,读取配置里的 DB_URL 建全部表:

cd /opt/pagure
PAGURE_CONFIG=/etc/pagure/pagure.cfg /opt/pagure/venv/bin/python createdb.py

💡 说明:脚本成功时无输出、退出码 0。此时配置文件 /etc/pagure/pagure.cfg 尚未创建的话,先按 4.9 节写好再回来执行本步(两节顺序可对调,但 createdb 必须在配置文件就绪之后)。

验证表数量:

PGPASSWORD=q1w2e3r4 psql -h localhost -U pagure -d pagure -t -A -c "SELECT count(*) FROM information_schema.tables WHERE table_schema='public';"

实测输出:

54

4.9 编写 pagure.cfg 主配置

Pagure 通过 PAGURE_CONFIG 环境变量指定的 Python 配置文件生效,未指定时尝试读 /etc/pagure/pagure.cfg。创建最小可运行配置(每项含义见注释;完整参数逐项详解见《Pagure 5.14.1 配置参数详解》):

# /etc/pagure/pagure.cfg
from datetime import timedelta

ADMIN_SESSION_LIFETIME = timedelta(minutes=20)
ENABLE_TICKETS = True          # 开启工单
ENABLE_DOCS = True             # 开启项目文档 git 仓库
PRIVATE_PROJECTS = True        # 允许私有项目

SECRET_KEY = 'pagure-lab-secret-20261028'   # Flask 会话密钥,生产换成长随机串
DB_URL = 'postgresql://pagure:q1w2e3r4@localhost:5432/pagure'

ADMIN_GROUP = ['pagure-admin']             # 管理员组
PAGURE_ADMIN_USERS = ['admin']             # 硬编码全局管理员
EMAIL_SEND = False                         # 本指南关闭外发邮件(离线激活,见 7.1)
EMAIL_ERROR = 'root@localhost'
SMTP_SERVER = 'localhost'
SMTP_PORT = 25
SMTP_SSL = False
SMTP_USERNAME = None
SMTP_PASSWORD = None

FROM_EMAIL = 'pagure@pagure.lab'
DOMAIN_EMAIL_NOTIFICATIONS = 'pagure.lab'
SALT_EMAIL = 'pagure-lab-salt-20261028'

APP_URL = 'http://localhost/'              # 站点对外 URL,生产换成实际域名
DOC_APP_URL = None                         # 关闭项目文档站点功能
GIT_URL_SSH = 'ssh://git@localhost/'       # git SSH 克隆地址前缀
GIT_URL_GIT = 'git://localhost/'

GIT_FOLDER = '/srv/git/repositories'       # git 仓库根目录
REMOTE_GIT_FOLDER = '/srv/git/remotes'     # 远程 PR 的 clone 存放处

GITOLITE_CONFIG = '/srv/git/gitolite-admin/conf/gitolite.conf'
GITOLITE_HOME = '/srv/git'
GITOLITE_VERSION = 3
GITOLITE_KEYDIR = '/srv/git/gitolite-admin/keydir'
GL_RC = '/srv/git/.gitolite.rc'
GL_BINDIR = '/usr/bin'
GIT_AUTH_BACKEND = 'gitolite3'
HTTP_REPO_ACCESS_GITOLITE = '/usr/share/gitolite3/gitolite-shell'
SSH_COMMAND_NON_REPOSPANNER = (
    [
        "/usr/share/gitolite3/gitolite-shell",
        "%(username)s",
        "%(cmd)s",
        "%(reponame)s",
    ],
    {},
)

ITEM_PER_PAGE = 50
MAX_CONTENT_LENGTH = 4 * 1024 * 1024
SHORT_LENGTH = 6
IP_ALLOWED_INTERNAL = ['127.0.0.1', 'localhost', '::1']

EVENTSOURCE_SOURCE = None
EVENTSOURCE_PORT = 8080
WEBHOOK = False

REDIS_HOST = '127.0.0.1'
REDIS_PORT = 6379
REDIS_DB = 0

PAGURE_AUTH = 'local'                      # 本地账号密码认证
SESSION_COOKIE_SECURE = False
SESSION_COOKIE_NAME = 'pagure'
CHECK_SESSION_IP = True
APPLICATION_ROOT = '/'
OLD_VIEW_COMMIT_ENABLED = False

逐项说明(关键项):

  • SECRET_KEY:Flask 会话签名密钥,泄露等于伪造任意会话;生产用 python3 -c "import secrets;print(secrets.token_hex(32))" 生成。
  • DB_URL:SQLAlchemy 连接串,格式 postgresql://用户:口令@主机:端口/库名。
  • PAGURE_ADMIN_USERS:无需任何组关系即拥有全站管理权限的用户名列表,配合 pagure-admin CLI 使用。
  • GITOLITE_* 与 GL_*:声明 gitolite 的 conf、keydir、rc、可执行文件位置,五个路径必须与 4.5 节 gitolite 初始化的实际落点一致。
  • EMAIL_SEND = False:关闭邮件后注册流程不发邮件,需手动到数据库取激活 token(见 7.1);生产接好 SMTP 后改 True。

4.10 初始化 gitolite 管理配置

gitolite 靠 gitolite-admin 工作副本里的 conf 文件管理 ACL,Pagure 会直接写这个文件。Debian 版 gitolite3 默认把管理副本放在 ~/.gitolite(上游默认 ~/gitolite-admin),而 Pagure 按上游约定写 /srv/git/gitolite-admin,因此必须在 .gitolite.rc 里显式对齐两个路径:

sed -i '/^%RC = (/a\    GL_REPO_BASE                    =>  '"'"'/srv/git/repositories'"'"',\n    GL_ADMIN_BASE                   =>  '"'"'/srv/git/gitolite-admin'"'"',' /srv/git/.gitolite.rc
head -5 /srv/git/.gitolite.rc

实测输出:

#!/usr/bin/perl
use warnings;
use strict;

our %RC = (
    GL_REPO_BASE                    =>  '/srv/git/repositories',
    GL_ADMIN_BASE                   =>  '/srv/git/gitolite-admin',

让 Pagure 生成 gitolite ACL 配置并编译(pagure-admin 是 Pagure 的管理 CLI,位于虚拟环境 bin 目录):

PAGURE_CONFIG=/etc/pagure/pagure.cfg /opt/pagure/venv/bin/pagure-admin refresh-gitolite

按提示回答两次 y,实测输出节选(实测首尾):

Please note that you have not selected a project or --all. Do you want to recompile the existing config file?
Do you want to continue? [y/N]y
Do you want to re-generate the gitolite.conf file for group: None and project: None?
Do you want to continue? [y/N]y
Gitolite ACLs updated

验证 gitolite 归属与 conf 落盘:

su - git -c "git -C /srv/git/gitolite-admin log --oneline -1"

实测输出:

6bd19a6 refresh

⚠️ 注意:refresh-gitolite 会触发 gitolite 编译并重写 /srv/git/.ssh/authorized_keys,必须以 git 用户运行(本节用虚拟环境路径直接执行即可,前提是当前 shell 用户对 /srv/git 有写权限;root 执行会把它写成 root 属主,随后 worker 的编译任务将报 FATAL: open /srv/git/.ssh/authorized_keys failed: Permission denied,排障过程见「八、常见问题」8.4)。

4.11 systemd 托管(pagure-web 与 pagure-worker)

Pagure 运行期有两个常驻进程:Gunicorn 承载的 Web 应用(pagure-web)与 Celery 承载的异步任务 worker(pagure-worker,负责建仓库、生成 gitolite conf、发通知等)。分别编写单元文件。

Web 服务单元:

# /etc/systemd/system/pagure-web.service
[Unit]
Description=Pagure web (gunicorn)
After=network.target postgresql.service redis-server.service

[Service]
Type=simple
User=git
Group=git
WorkingDirectory=/opt/pagure
ExecStart=/opt/pagure/venv/bin/gunicorn --workers 4 \
    --env PAGURE_CONFIG=/etc/pagure/pagure.cfg \
    --access-logfile /var/log/pagure/access_web.log \
    --error-logfile /var/log/pagure/error_web.log \
    --bind unix:/tmp/pagure_web.sock \
    "pagure.flask_app:create_app()"
Restart=on-failure

[Install]
WantedBy=multi-user.target

后台任务服务单元:

# /etc/systemd/system/pagure-worker.service
[Unit]
Description=Pagure worker (celery)
After=network.target postgresql.service redis-server.service

[Service]
Type=simple
User=git
Group=git
Environment="PAGURE_CONFIG=/etc/pagure/pagure.cfg"
ExecStart=/opt/pagure/venv/bin/celery -A pagure.lib.tasks worker --loglevel=INFO -n worker
Restart=on-failure

[Install]
WantedBy=multi-user.target

修日志目录属主、重载 systemd 并启动两个服务(每行只操作一个单元):

chown -R git:git /var/log/pagure /srv/git /opt/pagure
systemctl daemon-reload
systemctl enable pagure-web
systemctl enable pagure-worker
systemctl start pagure-web
systemctl start pagure-worker

确认两个服务状态:

systemctl is-active pagure-web
systemctl is-active pagure-worker

实测输出(两次执行各返回一行):

active
active

4.12 Nginx 反向代理

Nginx 监听 80,静态请求直接回文件、动态请求转发到 Gunicorn 的 Unix 套接字:

# /etc/nginx/sites-available/pagure.conf
server {
    listen 80;
    server_name _;

    access_log /var/log/pagure/access_web.log;
    error_log  /var/log/pagure/error_web.log;

    location / {
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_pass http://unix:/tmp/pagure_web.sock;
    }

    location /static/ {
        alias /opt/pagure/pagure/static/;
        try_files $uri @pagure;
    }

    location @pagure {
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_pass http://unix:/tmp/pagure_web.sock;
    }
}

启用站点并重载:

ln -sf /etc/nginx/sites-available/pagure.conf /etc/nginx/sites-enabled/pagure.conf
rm -f /etc/nginx/sites-enabled/default
nginx -t
systemctl reload nginx

实测输出(nginx -t):

nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

4.13 安装验证(Web / API / 注册登录 / SSH 推送)

分四层验证,全部实测通过即部署完成。

第一层,Web 服务状态与首页:

systemctl is-active pagure-web
curl -s -o /dev/null -w "%{http_code}\n" http://localhost/
curl -s http://localhost/ | grep -o "<title>[^<]*</title>" | head -1

实测输出:

active
200
<title>Home - Pagure</title>

第二层,REST API(Pagure 自带 API 版本端点与项目列表):

curl -s http://localhost/api/0/version
echo
curl -s "http://127.0.0.1/api/0/projects?short=1"

实测输出(第二条为创建 public-demo 项目后查询,输出节选(实测首尾),中间 JSON 键序保留原样):

{"version":"0.31"}
{"args":{"fork":null,"namespace":null,"owner":null,"page":1,"pattern":null,"per_page":20,"short":true,"tags":[],"username":null},"pagination":{"first":"http://localhost/api/0/projects?per_page=20&short=1&page=1","last":"http://localhost/api/0/projects?per_page=20&short=1&page=1","next":null,"page":1,"pages":1,"per_page":20,"prev":null},"projects":[{"description":"Public demo project","fullname":"public-demo","name":"public-demo","namespace":null}],"total_projects":1}

💡 说明:{"version":"0.31"} 返回的是 API 协议版本 0.31,不是 Pagure 软件版本(软件版本用 4.7 节的 pagure.__version__ 查询)。

第三层,注册管理员账号并激活(详见「七、配置说明」7.1),登录后创建项目 public-demo。

第四层,SSH 推送端到端验证——在管理员机器上克隆并推送:

mkdir -p /tmp/pd && cd /tmp/pd
git init
git remote add origin ssh://git@localhost/public-demo.git
echo "# public demo" > README.md
git add README.md
git commit -m "first commit"
git branch -M main
GIT_SSH_COMMAND="ssh -i ~/.ssh/pagure_admin" git push origin main

实测输出(带 -v 级别日志已省略,仅保留 gitolite 授权与推送结果;首次推送前的 hook 环境问题见 8.3):

To ssh://localhost/public-demo.git
 * [new branch]      main -> main

推送成功后从 API 侧确认分支与默认分支:

curl -s http://localhost/api/0/public-demo/git/branches

实测输出:

{"branches":["main"],"default":"main","total_branches":1}

再确认 gitolite 已把该账号授权到全部关联仓库:

ssh -i ~/.ssh/pagure_admin git@localhost info

实测输出:

hello admin, this is git@pagure running gitolite3 3.6.12-1 (Debian) on git 2.34.1

 R W	admin/demo-project
 R W	docs/admin/demo-project
 R W	docs/public-demo
 R W	public-demo
 R W	requests/admin/demo-project
 R W	requests/public-demo
 R W	tickets/admin/demo-project
 R W	tickets/public-demo

💡 说明:info 列出的 docs/、tickets/、requests/ 前缀仓库是 Pagure 为每个项目自动创建的文档、工单、PR 元数据仓库,属正常现象。日常运维命令(分支管理、只读切换、备份等)见《Pagure 5.14.1 常用 CLI 与运维命令详解》。

五、离线安装方案(wheel 离线包,断网服务器)

5.1 方案说明

生产环境常见「目标服务器无外网」场景。方案是把全部 Python 依赖以 wheel 形式在有网机器一次性下载,连同源码打成离线包传输;目标机用 pip --no-index 纯离线安装。Python 侧全流程实测通过;系统级 apt 依赖(postgresql/gitolite3/nginx 等)仍需目标机有本地 apt 源或提前离线下载 deb 包。

5.2 有网机器:准备离线包

在有网的 Ubuntu 22.04 机器上,先按「四、在线安装方案」4.2–4.6 装好系统依赖与源码,然后导出钉版依赖清单并下载 wheel:

/opt/pagure/venv/bin/pip freeze | grep -v -i pagure > /tmp/offline-req.txt
/opt/pagure/venv/bin/pip download -d /opt/wheelhouse -r /tmp/offline-req.txt
/opt/pagure/venv/bin/pip download -d /opt/wheelhouse wheel

实测输出节选(实测首尾):

Saved ./opt/wheelhouse/WTForms-2.3.3-py2.py3-none-any.whl
Successfully downloaded alembic amqp vine arrow async-timeout bcrypt billiard binaryornot bleach blinker celery click certifi cffi chardet charset-normalizer click-didyoumean click-plugins click-repl cryptography defusedxml dnspython docutils email-validator exceptiongroup Flask Flask-WTF greenlet gunicorn idna itsdangerous Jinja2 kitchen kombu Mako Markdown MarkupSafe munch packaging pillow prompt_toolkit psutil psycopg2-binary pycparser pygit2 python-dateutil python-openid-cla python-openid-teams python3-openid redis requests urllib3 six SQLAlchemy straight.plugin tomli typing_extensions tzdata tzlocal wcwidth webencodings Werkzeug whitenoise WTForms

把源码与 wheelhouse 打成离线包(实测约 122 MB):

tar -czf /opt/pagure-offline-bundle.tar.gz -C /opt pagure wheelhouse
ls -lh /opt/pagure-offline-bundle.tar.gz | awk '{print $5}'

实测输出:

122M

将 /opt/pagure-offline-bundle.tar.gz 用任意介质拷贝到目标机 /opt/ 下。

5.3 目标机:离线安装

目标机先按 4.2 装好 apt 依赖(需本地源),然后解包并离线装 Python 侧:

tar -xzf /opt/pagure-offline-bundle.tar.gz -C /opt
python3 -m venv /opt/pagure/venv
/opt/pagure/venv/bin/pip install -U pip setuptools'<81' wheel
/opt/pagure/venv/bin/pip install --no-index --find-links=/opt/wheelhouse \
  psycopg2-binary pygit2==1.13.3 sqlalchemy==1.4.54 wtforms==2.3.3 \
  flask-wtf==1.2.1 flask==2.2.5 werkzeug==2.3.8 alembic==1.13.2 bleach==5.0.1 wheel
/opt/pagure/venv/bin/pip install --no-index --find-links=/opt/wheelhouse -r /tmp/offline-req.txt
/opt/pagure/venv/bin/pip install --no-index --no-build-isolation -e /opt/pagure

⚠️ 注意:/tmp/offline-req.txt 随离线包一起从有网机器拷贝。最后一步安装 Pagure 自身必须带 --no-index --no-build-isolation,且目标虚拟环境里要先装好 wheel(缺它时报 error: invalid command 'bdist_wheel',为实测踩坑结论)。

离线安装的版本核对:

/opt/pagure/venv/bin/python -c "import pagure, flask, pygit2; print('pagure', pagure.__version__, '| flask', flask.__version__, '| pygit2', pygit2.__version__)"

实测输出:

pagure 5.14.1 | flask 2.2.5 | pygit2 1.13.3

5.4 与在线方案的差异

离线包路径只替代「四、在线安装方案」的 4.7 节(Python 依赖获取方式),其余步骤(数据库初始化、pagure.cfg、gitolite、systemd、Nginx、验证)与在线方案完全一致,按 4.8–4.13 继续执行即可。

六、方案对比与选择建议

维度方案 A:在线源码部署方案 B:离线 wheel 包部署
版本5.14.1(git tag)5.14.1(同一源码打包)
是否需联网是(git clone + pip)仅准备阶段需要,目标机零外网
依赖确定性pip 实时解析,需按 4.7 钉版wheel 清单固化,版本与实测环境完全一致
升级方式重新 checkout tag 后装依赖重新做离线包全量替换
适用场景可联网的测试/生产服务器内网隔离的生产服务器

选择建议:服务器能联网优先用方案 A(步骤最少);有严格外网隔离要求、或要求多台服务器依赖版本完全一致时用方案 B——离线包里固化的是本指南实测通过的那套依赖版本,可有效规避 PyPI 新版本引入的兼容性问题。两方案的Pagure 本体均为 5.14.1,功能无差别。

七、配置说明(账号初始化与首次使用)

7.1 首个管理员账号与激活

按 4.9 配置(EMAIL_SEND = False)部署时,注册流程不发邮件,需要手动激活。流程实测如下。

浏览器或 curl 访问注册页 /user/new,提交表单(用户名 admin、密码 q1w2e3r4、邮箱 admin@pagure.lab)后,页面提示:

User created, please check your email to activate the account

由于邮件被关闭,直接从数据库取激活 token:

PGPASSWORD=q1w2e3r4 psql -h localhost -U pagure -d pagure -c 'SELECT "user", token FROM users;'

实测输出(注册后 token 非空,形如 40 位随机串):

 user  |                 token
-------+------------------------------------------
 admin | TC9UXJSDNL9Q5Z99K499N23OTD1XQSVNY2GW5WB1
(1 row)

💡 说明:实测取到的 token 为 40 位字符串,上表为节选展示(实际 40 位完整显示)。用浏览器访问 http://localhost/confirm/<token> 完成激活,页面提示 Email confirmed, account activated。

激活后即可登录。若未激活就登录,会得到实测报错:

Invalid user, did you confirm the creation with the url provided by email?

⚠️ 注意:Pagure 的登录 POST 端点是 /dologin,注册页是 /user/new,与常见框架习惯的 /login、/register 不同,自动化脚本对接时容易 404。激活接口为 /confirm/<token>(不带尾部斜杠也有效)。

账号激活后,确认 4.9 配置里 PAGURE_ADMIN_USERS = ['admin'] 生效,即可用 admin 账号在 Web 界面创建项目(如本指南验证用的 public-demo),或使用管理 CLI(见《Pagure 5.14.1 常用 CLI 与运维命令详解》)。

7.2 常用配置调整

  • 改对外访问地址:生产替换 APP_URL(如 https://pagure.example.com/)、GIT_URL_SSH(如 ssh://git@git.example.com/),改完 systemctl restart pagure-web。
  • 打开邮件通知:填好 SMTP 五项并把 EMAIL_SEND 改 True,注册激活、PR 通知走邮件。
  • 打开 WebHook:WEBHOOK = True 并保持 Redis 可用(Pagure 用 Redis 发事件流)。
  • 完整参数(约 90 个键)的逐项语义与生产推荐值,见《Pagure 5.14.1 配置参数详解》。

八、常见问题(FAQ)

以下均为实测遇到的问题,按「现象 → 原因 → 解决」沉淀。

8.1 启动报 No module named '_gdbm'

现象:访问页面 500,Gunicorn 错误日志出现 ModuleNotFoundError: No module named '_gdbm'。 原因:binaryornot(文件类型识别依赖)运行时导入 Python 的 gdbm 模块,该模块属于 Python 标准库的扩展,只能以系统包形式安装,pip 装不上。 解决:apt install -y python3-gdbm,然后 systemctl restart pagure-web。无需重建虚拟环境——venv 与系统 Python 共享 lib-dynload 目录。

8.2 Python 依赖版本冲突报错汇总

Pagure 5.14.1 基于 SQLAlchemy 1.4 / Flask 2.2 时代的 API 开发,直接 pip install -r requirements.txt 会拉到过新的版本。实测踩坑与钉版对照:

报错(实测原文关键行)原因钉版解决
ModuleNotFoundError: No module named 'pygit2.remote'pygit2 1.14+ 重命名了远端模块pip install pygit2==1.13.3
cannot import name 'relation' from 'sqlalchemy.orm'(model.py:32)SQLAlchemy 2.x 移除了 relationpip install sqlalchemy==1.4.54
cannot import name 'Markup' from 'flask'(ui/plugins.py:19)Flask 3.x 移除 Markuppip install flask==2.2.5 werkzeug==2.3.8
no attribute 'Required'(forms.py:338)wtforms 3.x 移除 Requiredpip install wtforms==2.3.3 flask-wtf==1.2.1
No module named 'pkg_resources'setuptools 81+ 移除 pkg_resourcespip install 'setuptools<81'
TypeError: unsupported operand type(s) for +: 'frozenset' and 'list'(项目页 500)bleach 6.x 收紧了 ALLOWED_TAGS 类型pip install bleach==5.0.1

8.3 SSH 推送报 pre-receive hook declined(No such file or directory 'python')

现象:git push 输出 remote: /usr/bin/env: 'python': No such file or directory 与 ! [remote rejected] main -> main (pre-receive hook declined)。 原因:Pagure 装到每个仓库的 pre-receive/post-receive 钩子是源码里的 hookrunner 脚本,其 shebang 为 #!/usr/bin/env python,而虚拟环境的解释器叫 python3,不在 gitolite 钩子执行环境的 PATH 里。 解决(Pagure 源码注释里自述的修法):把钩子解释器改为虚拟环境的绝对路径:

sed -i '1s|.*|#!/opt/pagure/venv/bin/python|' /opt/pagure/pagure/hooks/files/hookrunner
head -1 /opt/pagure/pagure/hooks/files/hookrunner

实测输出:

#!/opt/pagure/venv/bin/python

仓库里的钩子是对该文件的符号链接,改完无需重建仓库,重推即成功(实测输出见 4.13 第四层)。

8.4 worker 日志报 authorized_keys Permission denied

现象:journalctl -u pagure-worker 出现:

pagure.exceptions.PagureException: The command "HOME=/srv/git gitolite compile && HOME=/srv/git gitolite trigger POST_COMPILE" failed with
  err:"b'FATAL: open /srv/git/.ssh/authorized_keys failed: Permission denied\n\n'"

原因:有人以 root 身份执行过 pagure-admin refresh-gitolite(或等价触发 gitolite 编译的操作),把 /srv/git/.ssh/authorized_keys 重写成 root 属主,之后 git 用户跑的 worker 编译任务写不了该文件。 解决:

chown git:git /srv/git/.ssh/authorized_keys
chmod 600 /srv/git/.ssh/authorized_keys

修复后验证 gitolite 编译与 SSH 通道均恢复正常:

su - git -c "HOME=/srv/git gitolite compile && echo COMPILE_OK"

实测输出:

COMPILE_OK

预防:所有会触发 gitolite 编译的管理命令一律以 git 用户(或对 /srv/git 有写权限的用户)执行。

8.5 gitolite info 看不到 Pagure 创建的仓库

现象:SSH 登录 git@localhost info 只显示 testing 等初始仓库,看不到 public-demo。 原因:Debian 版 gitolite3 把 GL_ADMIN_BASE(管理副本路径)默认改为 ~/.gitolite,与 Pagure 写入的 /srv/git/gitolite-admin 不一致,gitolite 编译时读的是另一份 conf。 解决:按 4.10 节在 /srv/git/.gitolite.rc 显式设置 GL_ADMIN_BASE => '/srv/git/gitolite-admin'(及 GL_REPO_BASE),再跑一次 pagure-admin refresh-gitolite。

8.6 以 root 跑 pagure-admin 部分子命令报仓库属主错误

现象:pagure-admin set-default-branch、create-branch 报:

_pygit2.GitError: /srv/git/repositories/public-demo.git: repository path '/srv/git/repositories/public-demo.git/' is not owned by current user

原因:这两个子命令直接操作 git 仓库(pygit2 有属主安全校验),root 身份触发保护。 解决:切到 git 用户执行:su - git -c "PAGURE_CONFIG=/etc/pagure/pagure.cfg /opt/pagure/venv/bin/pagure-admin set-default-branch public-demo main"(实测输出 Branch main set as default)。仅读写数据库的子命令(如 admin-token、list-groups)不受此限制。

8.7 直推报 Can't locate Gitolite/Hooks/Update.pm

现象:pagure-admin create-branch(内部直推 git 仓库)报 Can't locate Gitolite/Hooks/Update.pm in @INC,推送被 update 钩子拒绝。 原因:gitolite 的 update 钩子脚本第一段是 use lib $ENV{GL_LIBDIR},而 GL_LIBDIR 只在经 gitolite-shell 的 SSH 推送里由网关注入;Pagure 内部直推绕过了网关,环境变量为空导致 use lib 拿到空值。 解决:给 update 钩子加兜底路径(Debian 版 gitolite 库固定在 /usr/share/gitolite3/lib):

sed -i "s|use lib \$ENV{GL_LIBDIR};|use lib \$ENV{GL_LIBDIR} \|\| '/usr/share/gitolite3/lib';|" \
  /srv/git/gitolite-admin/hooks/common/update

之后重跑 create-branch,实测输出 Branch created。经 gitolite-shell 的正常 SSH 推送不受影响(网关仍会注入 GL_LIBDIR)。

8.8 注册后登录报 Invalid user

现象:注册成功但登录提示 Invalid user, did you confirm the creation with the url provided by email?。 原因:账号未激活(数据库 users 表 token 字段非空即未激活)。EMAIL_SEND=False 时系统无法自动发激活邮件。 解决:按 7.1 从数据库取 token,访问 /confirm/<token> 激活。

九、卸载与清理

按「先停服务、再清数据」的顺序完整卸载:

systemctl disable pagure-web
systemctl disable pagure-worker
systemctl stop pagure-web
systemctl stop pagure-worker
rm -f /etc/systemd/system/pagure-web.service /etc/systemd/system/pagure-worker.service
systemctl daemon-reload
rm -f /etc/nginx/sites-enabled/pagure.conf
systemctl reload nginx

删除应用与数据(含 git 仓库,执行前确认已备份):

rm -rf /opt/pagure /etc/pagure /var/log/pagure /srv/git
su postgres -c "psql -c 'DROP DATABASE IF EXISTS pagure;'"
su postgres -c "psql -c 'DROP ROLE IF EXISTS pagure;'"

如不再使用,可一并卸载 gitolite3 与数据库软件包(保留则不影响其他服务):

apt purge -y gitolite3

卸载验证:

systemctl is-active pagure-web

实测输出:

inactive

(单元文件已删除时该命令输出 inactive 并提示单元不存在,属预期。)

十、参考链接