使用Devpi与Nginx构建高可用PyPI镜像源
Published in:2025-09-17 |
Words: 4.9k | Reading time: 24min | reading:

使用Devpi与Nginx构建高可用PyPI镜像源

前言:为什么你的pip install又超时了?

对于每一位Python开发者来说,pip install 无疑是最常用的命令之一。然而,团队开发中,我们常常会遇到这些痛点:

  • 速度瓶颈:团队几十号人,CI/CD流水线上百个任务,全都从国外的官方PyPI源拉取包,下载速度慢得令人抓狂,严重影响开发和部署效率。
  • 单点故障:官方PyPI偶尔会抖动,或者公司出口网络出现问题,导致所有依赖安装失败,研发流程瞬间停摆。
  • 安全与合规:无法对团队使用的第三方包进行统一管理和安全审计。
  • 私有包托管:团队内部开发的公共库无处安放,只能通过Git仓库等原始方式共享。

为了彻底解决这些问题,我们需要一个私有的、高速的、且永不宕机的Python包仓库。今天,我将手把手带你使用业界最强大的开源工具 devpiNginx,构建一个具备自动故障转移能力的高可用PyPI镜像源。

最终目标:无论清华源、阿里源还是其他任何一个镜像挂了,我们的 pip install 依然能丝滑流畅地运行,对开发者完全透明!

1
2
3
4
5
6
7
8
9
10
11
12
+-----------+     +-------------------+     +-------------------------+     +--------------------------+
| | | | | | | Tsinghua Mirror |
| 开发者 | --> | devpi 服务器 | --> | Nginx 反向代理 | --> | (Upstream 1) |
| (pip) | | (缓存/私有包) | | (处理上游故障转移) | +--------------------------+
| | | | | | | Aliyun Mirror |
+-----------+ +-------------------+ +-------------------------+ --> | (Upstream 2) |
+--------------------------+
| USTC Mirror |
->| (Upstream 3) |
+--------------------------+
| ... and so on |
+--------------------------+

第一章:核心武器介绍 - Devpi 与 Nginx

在开始之前,我们先了解一下两位主角:

  1. Devpi: 一个功能完备的Python包服务器。它不仅仅是 pypi.org 的一个缓存代理,更是一个强大的私有包仓库和版本发布管理工具。我们将主要利用它的缓存镜像功能。

  2. Nginx: 一款高性能的HTTP和反向代理服务器。在这里,它将扮演我们镜像源的“智能调度员”和“故障哨兵”,负责监测上游镜像源的健康状况,并在某个源出现问题时,自动将流量切换到健康的备用源上。

第二章:基础建设 - 搭建Devpi缓存服务器

首先,我们需要一个能稳定运行的devpi服务。

步骤 1: 准备服务器与安装Devpi

在一台Linux服务器(本教程以Ubuntu为例)上,确保已安装Python和pip。

1
2
3
4
5
6
7
8
9
# 更新并安装依赖
sudo apt update
sudo apt install -y python3 python3-pip

# 安装 devpi-server
pip3 install devpi-server

devpi-init

步骤 2: 初始化Devpi

这个一次性操作会为你创建数据目录和 root 超级管理员。

1
devpi-init

在提示中,为 root 用户设置一个安全的密码,并牢牢记住它。

步骤 3: 让Devpi在后台稳定运行

为了生产环境的稳定,我们将 devpi 配置成一个 systemd 系统服务,实现开机自启和后台运行。

  1. 创建服务文件:

    1
    sudo nano /etc/systemd/system/devpi.service
  2. 粘贴以下配置 (注意修改 UserExecStart 中的路径为你自己的):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
(venv) czq2@czq2:~/clip-video$ sudo cat /etc/systemd/system/devpi.service
[Unit]
Description=devpi-server daemon
After=network.target

[Service]
Type=simple
User=czq2
Group=czq2
ExecStart=/home/czq2/clip-video/venv/bin/devpi-server --host=0.0.0.0 --port=3141
Restart=on-failure
RestartSec=5s

[Install]
WantedBy=multi-user.target
  • --host=0.0.0.0 确保局域网内的其他机器可以访问。
  1. 启动并验证服务:
    1
    2
    3
    4
    sudo systemctl daemon-reload
    sudo systemctl enable devpi # 开机自启
    sudo systemctl start devpi # 启动
    sudo systemctl status devpi # 检查状态
    看到绿色的 active (running),恭喜你,基础的 devpi 镜像服务已经成功运行在 http://<你的服务器IP>:3141 上了!
  • 成功输出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
(venv) czq2@czq2:~/clip-video$ sudo systemctl start devpi
(venv) czq2@czq2:~/clip-video$ sudo systemctl status devpi
● devpi.service - devpi-server daemon
Loaded: loaded (/etc/systemd/system/devpi.service; enabled; preset: enabled)
Active: active (running) since Wed 2025-09-17 07:19:23 UTC; 3s ago
Main PID: 1619813 (devpi-server)
Tasks: 54 (limit: 309293)
Memory: 41.4M (peak: 553.5M)
CPU: 15.733s
CGroup: /system.slice/devpi.service
└─1619813 /home/czq2/clip-video/venv/bin/python3 /home/czq2/clip-video/venv/bin/devpi-server --host=0.0.0.0 --p>

Sep 17 07:19:23 czq2 devpi-server[1619813]: 2025-09-17 07:19:23,634 WARNI NOCTX No secret file provided, creating a new rand>
Sep 17 07:19:25 czq2 devpi-server[1619813]: 2025-09-17 07:19:25,757 INFO [ASYN] Starting asyncio event loop
Sep 17 07:19:25 czq2 devpi-server[1619813]: 2025-09-17 07:19:25,777 INFO NOCTX devpi-server version: 6.17.0
Sep 17 07:19:25 czq2 devpi-server[1619813]: 2025-09-17 07:19:25,777 INFO NOCTX serverdir: /home/czq2/.devpi/server
Sep 17 07:19:25 czq2 devpi-server[1619813]: 2025-09-17 07:19:25,777 INFO NOCTX uuid: 0c7aa3d0d1874fd596c8936cd007d305
Sep 17 07:19:25 czq2 devpi-server[1619813]: 2025-09-17 07:19:25,777 INFO NOCTX serving at url: http://0.0.0.0:3141 (might b>
Sep 17 07:19:25 czq2 devpi-server[1619813]: 2025-09-17 07:19:25,777 INFO NOCTX using 50 threads
Sep 17 07:19:25 czq2 devpi-server[1619813]: 2025-09-17 07:19:25,777 INFO NOCTX bug tracker: https://github.com/devpi/devpi/>
Sep 17 07:19:25 czq2 devpi-server[1619813]: 2025-09-17 07:19:25,777 INFO NOCTX Hit Ctrl-C to quit.
Sep 17 07:19:25 czq2 devpi-server[1619813]: 2025-09-17 07:19:25,785 INFO Serving on http://0.0.0.0:3141
  • 错误分析
1
sudo journalctl -u devpi.service -n 50 --no-pager

第三章:构建护城河 - Nginx高可用反向代理

现在,我们要为 devpi 构建一个坚固的前哨站,让它不再直接依赖任何单一的上游源。

步骤 1: 安装 Nginx

1
2
sudo apt update
sudo apt install -y nginx

步骤 2: 编写Nginx高可用配置

这是实现自动故障转移的核心

  1. 创建Nginx配置文件:

    1
    sudo nano /etc/nginx/sites-available/pypi-proxy
  2. 粘贴以下魔法配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

# /etc/nginx/sites-available/pypi-proxy

## 定义一个上游服务器组,这次全部使用 HTTPS

upstream pypi_mirrors_https {
# 【核心修改】直接指向 HTTPS 域名,端口 443 是默认的,可以不写
server pypi.tuna.tsinghua.edu.cn:443;
server mirrors.aliyun.com:443;
server pypi.mirrors.ustc.edu.cn:443;
server mirrors.cloud.tencent.com:443;
server repo.huaweicloud.com:443;
}

server {
listen 8888;
server_name localhost;

location / {
# 【核心修改】将请求代理到我们新的 HTTPS 服务器组
proxy_pass https://pypi_mirrors_https;

# 故障转移的配置保持不变
proxy_next_upstream error timeout http_500 http_502 http_503 http_504;

proxy_connect_timeout 10s;

# 【重要】当代理到 HTTPS 上游时,必须正确设置 Host 头
# 这里的 $host 会是上游服务器组的名称,为了正确性,我们应该传递原始请求的Host
# 或者直接硬编码为 pypi.tuna.tsinghua.edu.cn 等,但传递原始Host更灵活
proxy_set_header Host $proxy_host; # 使用 $proxy_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;

# (可选,但推荐)增加 SSL 相关的代理设置
proxy_ssl_server_name on; # 这会让Nginx在TLS握手中发送正确的服务器名称(SNI)
proxy_ssl_session_reuse on; # 启用SSL会话复用以提高性能
}
}
  1. 启用配置并重启Nginx:
    1
    2
    3
    sudo ln -s /etc/nginx/sites-available/pypi-proxy /etc/nginx/sites-enabled/
    sudo nginx -t # 测试配置语法
    sudo systemctl restart nginx # 重启Nginx
    现在,一个强大的、能自动切换源的反向代理已经在 http://192.168.10.158:8088 上为你待命。

第四章:合体!将Devpi接入Nginx代理

万事俱备,只差最后一步:让 devpi 通过我们的 Nginx “护城河”去访问世界。

  1. 停止Devpi服务:

    1
    sudo systemctl stop devpi
  2. 修改Devpi的启动参数:

    1
    sudo nano /etc/systemd/system/devpi.service
  3. ExecStart 行的末尾,添加 --mirror-url 参数,指向我们的Nginx代理:

    1
    2
    3
    # ...
    ExecStart=/home/your_username/.local/bin/devpi-server --host=0.0.0.0 --port=3141 --mirror-url http://192.168.10.158:8088/pypi
    # ...
  4. 重新加载并启动Devpi:

    1
    2
    sudo systemctl daemon-reload
    sudo systemctl start devpi

第五章:享受成果 - 配置客户端

现在,让团队的所有开发者和CI/CD服务器都享受到这个稳定高速的镜像源吧!

在每一台客户端机器上,创建或修改 ~/.pip/pip.conf (macOS/Linux) 或 %APPDATA%\pip\pip.ini (Windows) 文件:

1
2
3
[global]
index-url = http://192.168.10.158:3141/root/pypi/
trusted-host = 192.168.10.158
  • index-url 指向我们搭建的 devpi 服务。
  • trusted-host 告诉 pip 这是一个可信任的HTTP源。

使用docker compose 搭建 devpi

  • docker-comose.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# version 属性已过时,可以移除
# version: "2.3"

services:
# 服务1:高可用 Nginx 反向代理
nginx-proxy:
image: nginx:latest
container_name: devpi-nginx-proxy
volumes:
# 将我们的 stream 配置文件挂载到容器中
- ./nginx_conf/nginx.conf:/etc/nginx/nginx.conf:ro
# 这个服务只在内部使用,不需要暴露端口到宿主机
restart: always

# 服务2:Devpi 服务器
devpi-lib:
container_name: devpi-lib
image: lowinli98/devpi:v0.2
expose:
- 7104
ports:
- "7104:7104"
environment:
- DEVPISERVER_HOST=0.0.0.0
- DEVPISERVER_PORT=7104
- DEVPISERVER_ROOT_PASSWORD=111
- DEVPISERVER_USER=czq
- DEVPISERVER_PASSWORD=111
- DEVPISERVER_MIRROR_INDEX=pypi
- DEVPISERVER_LIB_INDEX=devpi
# --- 【核心修改】---
# 将上游镜像源指向我们内部的 nginx-proxy 服务的 443 端口
- SOURCE_MIRROR_URL=http://nginx-proxy
restart: always
volumes:
- ./volume:/var/lib/devpi
# 【重要】确保 devpi 服务依赖于 nginx-proxy 服务
depends_on:
- nginx-proxy
  • nignx conf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# nginx_conf/nginx.conf (HTTP 代理版本)

events {}

http {
# 定义高可用的 PyPI 镜像上游服务器池
upstream pypi_mirrors_https {
server pypi.tuna.tsinghua.edu.cn:443;
server mirrors.aliyun.com:443;
server pypi.mirrors.ustc.edu.cn:443;
server mirrors.cloud.tencent.com:443;
server pypi.douban.com:443;
}

server {
# 监听容器内部的 80 端口,接收来自 devpi-lib 的 HTTP 请求
listen 80;

location / {
# 将请求代理到 HTTPS 上游
proxy_pass https://pypi_mirrors_https;

# --- 代理核心配置 ---
proxy_next_upstream error timeout http_502 http_503 http_504;
proxy_connect_timeout 10s;

# --- 关键的请求头与 SSL/TLS 配置 ---
proxy_set_header Host $proxy_host;
proxy_ssl_server_name on;
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_ssl_session_reuse on;
}
}
}
  • docker run
1
sudo docker-compose up -d --build

自建devpi镜像

  • Dockerfile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# Dockerfile (最终稳定版)

# 使用一个官方的、轻量级的 Python 3.9 基础镜像
FROM python:3.9-slim

# 设置容器内的工作目录
WORKDIR /app

# 【构建时网络优化】
# 将 pip 的默认源设置为高速的镜像源
RUN pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple

# --- 【核心 Bug 修复 & 版本锁定】 ---
# 安装应用程序及其依赖,并锁定所有核心组件的版本
# devpi-server 6.9.x 是一个非常稳定的系列
RUN pip install --no-cache-dir --upgrade pip && \
pip install --no-cache-dir \
"devpi-server==6.9.2" \
"devpi-web==4.1.0" \
"httpx==0.22.0"

# 创建数据卷挂载点
RUN mkdir /var/lib/devpi

# 【权限修复】
# 将数据目录的所有者更改为非 root 用户 (UID 1000)
RUN chown -R 1000:1000 /var/lib/devpi

# 【安全实践】
# 将容器的默认运行用户切换为非 root 用户
USER 1000

# 声明容器将要监听的端口
EXPOSE 7104

# 定义容器启动时要执行的默认命令
CMD ["devpi-server", "--serverdir", "/var/lib/devpi", "--host", "0.0.0.0", "--port", "7104"]
  • docker-compose.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# docker-compose.yml (最终、决定性版本)

services:
nginx-proxy:
image: nginx:latest
container_name: devpi-nginx-proxy
environment:
- http_proxy=http://192.168.12.80:7890
- https_proxy=http://192.168.12.80:7890
- HTTP_PROXY=http://192.168.12.80:7890
- HTTPS_PROXY=http://192.168.12.80:7890
volumes:
- ./nginx_conf/nginx.conf:/etc/nginx/nginx.conf:ro
restart: always

devpi-lib:
container_name: devpi-lib
# --- 【核心修改】---
# 不再使用旧的 image,而是使用 build 指令
build: . # 表示使用当前目录下的 Dockerfile 来构建镜像
ports:
- "7104:7104"
# 【重要】我们不再需要旧镜像的环境变量来自动化配置
# 我们将在启动后,手动进行一次性配置,这更可靠

environment:
- http_proxy=http://192.168.12.80:7890
- https_proxy=http://192.168.12.80:7890
- HTTP_PROXY=http://192.168.12.80:7890
- HTTPS_PROXY=http://192.168.12.80:7890
# environment:
# - ... (可以全部删除)
restart: always
volumes:
# 数据卷依然需要,用于持久化 devpi 的数据
- ./volume:/var/lib/devpi
depends_on:
- nginx-proxy

    1. 在你的客户端电脑上,安装 devpi-client:
    1
    2
    sudo chown -R czq2:czq2 ./volume # 更改目录权限
    pip install devpi-client
    1. 登录到你的新 devpi 服务器:
    1
    2
    3
    4
    5
    6
    7
    # 指向服务器
    devpi use http://<你的服务器IP>:7104

    # 首次登录 root 用户,devpi-server 会自动创建,无需密码
    # 然后会强制你设置新密码
    devpi login root --password=""
    # (按照提示输入你的新 root 密码)
    1. 【关键】配置 root/pypi 镜像:
      默认的 root/pypi 指向 pypi.org。我们需要将它修改为指向我们内部的 nginx-proxy
    1
    2
    # 确保你已登录 root
    devpi index root/pypi mirror_url=http://nginx-proxy
    • 注意: 这里的 http://nginx-proxydevpi-lib 容器内部访问 nginx-proxy 容器的地址。devpi-server 在收到这个配置后,它内部发起的请求就会走这条路。
    1. 验证:
      在你的客户端电脑上,配置好 pip 指向 http://<服务器IP>:7104/root/pypi/,然后执行 pip install numpy

成功日志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
(.venv) czq2@czq2:~/pypi-docker$ sudo docker logs devpi-lib
2025-09-22 02:56:36,728 INFO [req12] POST /+login
2025-09-22 02:57:08,329 INFO [req13] GET /root/pypi
2025-09-22 02:57:08,339 INFO [req14] PATCH /root/pypi
2025-09-22 02:57:08,342 INFO [req14] [Wtx13] modified index root/pypi: {'type': 'mirror', 'volatile': False, 'title': 'PyPI', 'mirror_url': 'http://mirrors.aliyun.com/pypi/simple/', 'mirror_web_url_fmt': 'https://pypi.org/project/{name}/'}
2025-09-22 02:57:08,345 INFO [req14] [Wtx13] fswriter14: committed at 14
2025-09-22 02:57:08,351 INFO [req15] GET /root/pypi
2025-09-22 02:57:25,788 INFO [req16] GET /root/pypi/opencv-python/
2025-09-22 02:57:40,843 INFO [req17] GET /root/pypi/opencv-python/
2025-09-22 02:57:41,058 INFO [req18] GET /root
2025-09-22 02:57:41,533 INFO [req19] GET /+static-4.1.0/favicon.ico
2025-09-22 02:57:43,732 INFO [Wtx14] fswriter15: committed at 15
2025-09-22 02:57:43,885 INFO [NOTI] [Rtx15] indexing 'root/pypi' mirror with 658356 projects
2025-09-22 02:57:44,305 INFO [IDX] Indexer queue size ~ 2
2025-09-22 02:57:46,680 INFO [req20] GET /root
2025-09-22 02:57:47,303 INFO [IDX] Committing 2500 new documents to search index.
2025-09-22 02:57:47,410 INFO [req21] GET /root/pypi
2025-09-22 02:57:48,180 INFO [req22] GET /root/pypi/+simple/
2025-09-22 02:57:48,183 INFO [req22] starting +simple
2025-09-22 02:57:52,640 INFO [req23] GET /root/pypi/+simple/
2025-09-22 02:57:52,643 INFO [req23] starting +simple
2025-09-22 02:58:00,856 INFO [IDX] Indexer queue size ~ 23
2025-09-22 02:58:03,872 INFO [IDX] Committing 2500 new documents to search index.
2025-09-22 02:58:13,593 INFO [IDX] Indexer queue size ~ 43
2025-09-22 02:58:16,637 INFO [IDX] Committing 2500 new documents to search index.
2025-09-22 02:58:24,550 INFO [IDX] Indexer queue size ~ 63
2025-09-22 02:58:27,063 INFO [req24] GET /root/dev/+simple/
2025-09-22 02:58:27,068 INFO [req24] starting +simple
2025-09-22 02:58:31,043 INFO [IDX] Committing 2500 new documents to search index.
2025-09-22 02:58:36,905 INFO [IDX] Indexer queue size ~ 84
2025-09-22 02:58:40,109 INFO [IDX] Committing 2500 new documents to search index.
2025-09-22 02:58:46,493 INFO [IDX] Indexer queue size ~ 105
2025-09-22 02:58:49,808 INFO [IDX] Committing 2500 new documents to search index.
2025-09-22 02:58:57,171 INFO [req25] GET /root/pypi/opencv-python/
2025-09-22 02:58:57,452 INFO [req26] GET /root/pypi/+f/092/c16da4c5a163a/opencv_python-4.12.0.88-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl
2025-09-22 02:58:57,703 INFO [req26] [Rtx15] reading remote: URL('http://mirrors.aliyun.com/pypi/packages/68/1f/795e7f4aa2eacc59afa4fb61a2e35e510d06414dd5a802b51a012d691b37/opencv_python-4.12.0.88-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl'), target root/pypi/+f/092/c16da4c5a163a/opencv_python-4.12.0.88-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl
2025-09-22 02:58:58,698 INFO [IDX] Indexer queue size ~ 125
2025-09-22 02:59:02,452 INFO [IDX] Committing 2500 new documents to search index.
2025-09-22 02:59:13,134 INFO [Wtx15] fswriter16: committed at 16
2025-09-22 02:59:13,406 INFO [req27] GET /root/pypi/numpy/
2025-09-22 02:59:16,152 INFO [IDX] Indexer queue size ~ 144
2025-09-22 02:59:20,559 INFO [IDX] Committing 2500 new documents to search index.
2025-09-22 02:59:22,506 INFO [Wtx16] fswriter17: committed at 17
2025-09-22 02:59:22,974 INFO [req28] GET /root/pypi/+f/fd8/3c01228a68873/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
2025-09-22 02:59:23,201 INFO [req28] [Rtx17] reading remote: URL('http://mirrors.aliyun.com/pypi/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl'), target root/pypi/+f/fd8/3c01228a68873/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
2025-09-22 02:59:25,317 INFO [Wtx17] fswriter18: committed at 18
2025-09-22 02:59:29,854 INFO [IDX] Indexer queue size ~ 157
2025-09-22 02:59:32,438 INFO [req29] GET /czq/devpi
2025-09-22 02:59:33,214 INFO [req30] GET /czq/devpi/+simple/
2025-09-22 02:59:33,219 INFO [req30] starting +simple
2025-09-22 02:59:33,222 WARNI [req30] [Rtx18] Index czq/devpi refers to non-existing base czq/pypi.
2025-09-22 02:59:33,419 INFO [IDX] Committing 2500 new documents to search index.
2025-09-22 02:59:35,333 INFO [req31] GET /root/dev
2025-09-22 02:59:36,071 INFO [req32] GET /root/dev/+simple/
2025-09-22 02:59:36,073 INFO [req32] starting +simple
2025-09-22 02:59:45,543 INFO [IDX] Indexer queue size ~ 183
2025-09-22 02:59:49,092 INFO [IDX] Committing 2500 new documents to search index.
2025-09-22 02:59:59,191 INFO [IDX] Indexer queue size ~ 204
2025-09-22 02:59:59,375 INFO [req33] GET /
2025-09-22 03:00:02,409 INFO [req34] GET /+search
2025-09-22 03:00:03,190 INFO [IDX] Committing 2500 new documents to search index.
2025-09-22 03:00:12,782 INFO [IDX] Indexer queue size ~ 223
2025-09-22 03:00:16,424 INFO [IDX] Committing 2500 new documents to search index.
2025-09-22 03:00:22,925 INFO [IDX] Indexer queue size ~ 243

python 搭建高可用

  • python flask
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# ha_proxy.py
import requests
from flask import Flask, Response, request

# --- 高可用镜像源列表 ---
# 将按照这个顺序尝试。可以随意增删或调整顺序。
UPSTREAM_MIRRORS = [
"https://pypi.tuna.tsinghua.edu.cn/simple",
"https://mirrors.aliyun.com/pypi/simple",
"https://pypi.mirrors.ustc.edu.cn/simple",
"https://mirrors.cloud.tencent.com/pypi/simple",
]

# 连接和读取的超时时间(秒)
TIMEOUT = 10

app = Flask(__name__)

@app.route('/<path:path>')
def proxy(path):
"""
接收来自 devpi-server 的所有请求,并尝试从上游镜像列表中获取。
"""
for base_url in UPSTREAM_MIRRORS:
url = f"{base_url}/{path}"
try:
print(f"HA-PROXY: Attempting to fetch from {url}")

# `requests` 会自动使用 HTTP_PROXY/HTTPS_PROXY 环境变量
response = requests.get(url, timeout=TIMEOUT, stream=True)

# 如果请求成功 (例如 200 OK)
if response.status_code == 200:
print(f"HA-PROXY: Success from {url}")
# 使用流式响应,避免大文件占用过多内存
return Response(response.iter_content(chunk_size=8192),
status=response.status_code,
content_type=response.headers['Content-Type'])

# 如果是 404,说明这个源没有这个包,立即尝试下一个
elif response.status_code == 404:
print(f"HA-PROXY: Got 404 from {url}, trying next mirror.")
continue # 继续下一个循环

# 其他客户端错误,也尝试下一个
else:
print(f"HA-PROXY: Got client error {response.status_code} from {url}, trying next mirror.")
continue

except requests.exceptions.RequestException as e:
# 捕获所有网络层面的错误 (如超时、连接失败)
print(f"HA-PROXY: Failed to connect to {base_url}. Error: {e}")
continue # 继续下一个循环

# 如果所有镜像源都尝试失败
print(f"HA-PROXY: All upstream mirrors failed for path: {path}")
return Response("All upstream mirrors failed.", status=502)

if __name__ == '__main__':
# 监听在 8000 端口,只接受来自容器内部的连接
app.run(host='127.0.0.1', port=8000)
  • supervisord.conf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
; supervisord.conf (最终生产版)

[supervisord]
nodaemon=true

[program:devpi]
command=devpi-server --serverdir /var/lib/devpi --host 0.0.0.0 --port 7104
user=devpiuser ; <--- 修改这里
autostart=true
autorestart=true
stderr_logfile=/var/log/supervisor/devpi_err.log
stdout_logfile=/var/log/supervisor/devpi_out.log

[program:ha_proxy]
command=python3 /app/ha_proxy.py
user=devpiuser ; <--- 修改这里
autostart=true
autorestart=true
stderr_logfile=/var/log/supervisor/ha_proxy_err.log
stdout_logfile=/var/log/supervisor/ha_proxy_out.log

同步更新dockerfile和docker-compose.yml

  • Dockerfile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# Dockerfile (最终生产版)

FROM python:3.9-slim

RUN sed -i 's/deb.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list.d/debian.sources

WORKDIR /app

RUN pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple

# --- 【关键修复】创建一个专用的非 root 用户 ---
# 创建一个名为 devpiuser 的系统组和用户,并指定其 ID 为 1000
RUN adduser \
--system \
--group \
--uid 1000 \
--no-create-home \
--disabled-password \
devpiuser

# --- 安装所有依赖 ---
RUN apt-get update && apt-get install -y supervisor && \
pip install --no-cache-dir --upgrade pip && \
pip install --no-cache-dir \
"devpi-server==6.9.2" \
"devpi-web==4.1.0" \
"httpx==0.22.0" \
"flask" \
"requests" && \
rm -rf /var/lib/apt/lists/*

# --- 复制配置文件和脚本 ---
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
COPY ha_proxy.py /app/ha_proxy.py

# --- 权限修复 ---
# 使用新创建的用户名 devpiuser 来设置权限
RUN mkdir -p /var/log/supervisor
RUN mkdir /var/lib/devpi
RUN chown -R devpiuser:devpiuser /var/lib/devpi
RUN chown -R devpiuser:devpiuser /var/log/supervisor

# 容器将以 root 启动,由 supervisor 负责降权
# USER devpiuser (这里不需要,supervisor 会处理)

EXPOSE 7104

CMD ["/usr/bin/supervisord"]
  • docker-compose.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# docker-compose.yml (与最终高可用版 Dockerfile 配合使用)
version: '3.7'

services:
devpi-lib:
build: .
container_name: devpi-lib
ports:
- "7104:7104"
volumes:
- devpi-data:/var/lib/devpi
restart: always
environment:
# 代理配置依然需要,ha_proxy.py 会使用它
- http_proxy=http://192.168.12.80:7890
- https_proxy=http://192.168.12.80:7890
- HTTP_PROXY=http://192.168.12.80:7890
- HTTPS_PROXY=http://192.168.12.80:7890

volumes:
devpi-data:
  • docker build
1
2

sudo docker-compose up -d --build --force-recreate
  • mirror set
1
2
3
4
devpi login root --password=""
# 将 mirror_url 指向我们内部的 HA 代理服务
devpi index root/pypi mirror_url=http://127.0.0.1:8000/
exit

See

Prev:
实现高可用 MySQL 集群
Next:
使用 Docker Compose 快速部署 Shadowsocks 代理服务教程