- PVSM.RU - https://www.pvsm.ru -
Вчера pypi.org [1] несколько часов был недоступен из российских сетей. Для кого-то это «подождём», а для CI/CD, прода и просто рабочего дня — это вставший pip install и красные сборки.
Причина системная: pypi.org [1] и хранилище пакетов files.pythonhosted.org [2] живут на CDN Fastly, у которого нет точек присутствия в России и доступ к которому уже не раз ограничивался. Вчерашняя недоступность — не первая и почти наверняка не последняя.
Хорошая новость: чтобы застраховаться, не нужно зеркалировать весь PyPI (это терабайты и постоянная синхронизация). Достаточно поднять лёгкий реверс-прокси на nginx. В этом гайде соберём такой с нуля — с кешированием и прозрачным переключением для pip.
Не хотите хостить сами? Есть уже готовое зеркало — pypi.depkit.ru [3]. Оно работает на российских IP, имеет большой объём кеша под пакеты и отдаёт их очень быстро. Можно просто подставить его в index-url (как — в конце статьи) и пропустить всю настройку. Дальше — для тех, кому интересно поднять своё.
pip работает с PyPI в два шага:
Берёт индекс /simple/<пакет>/ — небольшую HTML-страницу со списком всех файлов пакета и ссылками на них.
По ссылкам из индекса скачивает сами файлы (wheels и sdists) — они лежат на files.pythonhosted.org [2].
Идея зеркала: проксируем индекс с pypi.org [1], но на лету переписываем в HTML ссылки на файлы так, чтобы они вели на наш домен. Тогда и индекс, и файлы pip тянет через нас. Хранить ничего заранее не нужно — файлы проксируются (и кешируются) по запросу.
Переписывание делает директива nginx sub_filter — построчная замена в теле ответа.
Минимальный рабочий вариант — один server-блок с двумя location:
server {
listen 443 ssl;
server_name pypi.example.com; # ваш домен
# ssl_certificate /etc/letsencrypt/live/pypi.example.com/fullchain.pem;
# ssl_certificate_key /etc/letsencrypt/live/pypi.example.com/privkey.pem;
# --- индекс /simple/: проксируем pypi.org и переписываем ссылки на файлы ---
location /simple/ {
proxy_pass https://pypi.org;
proxy_set_header Host pypi.org;
proxy_ssl_server_name on;
# отключаем сжатие от апстрима — иначе sub_filter нечего будет менять
proxy_set_header Accept-Encoding "";
sub_filter_types text/html;
sub_filter "https://files.pythonhosted.org/" "https://pypi.example.com/files/";
sub_filter_once off; # заменяем ВСЕ вхождения, а не только первое
}
# --- сами wheels/sdists: чистый проксипасс на хранилище ---
location /files/ {
rewrite ^/files/(.*)$ /$1 break;
proxy_pass https://files.pythonhosted.org;
proxy_set_header Host files.pythonhosted.org;
proxy_ssl_server_name on;
}
}
Логика:
pip запрашивает /simple/<пакет>/ → nginx отдаёт страницу с pypi.org [1];
sub_filter меняет в ней все https://files.pythonhosted.org/ [4] на https://pypi.example.com/files/ [5]; [6]
pip идёт за файлами уже на наш /files/;
location /files/ срезает префикс /files и проксирует на files.pythonhosted.org [2].
Подставьте свой server_name в обоих местах (в server_name и в строке sub_filter) и не забудьте сертификат — pip ходит только по HTTPS.
Accept-Encoding “” обязателен. sub_filter работает только с несжатым текстом. Без обнуления Accept-Encoding апстрим вернёт gzip, и замена молча не сработает. Это причина №1 у тех, «у кого не переписывается».
sub_filter_once off. По умолчанию меняется только первое совпадение. На странице пакета ссылок десятки — нужно off.
proxy_ssl_server_name on. Оба апстрима за TLS с SNI; без этого прилетит неправильный сертификат.
rewrite ^/files/(.*)$ /$1 break;. Префикс /files — наш, на хранилище его быть не должно, поэтому срезаем перед проксированием.
Главный смысл своего зеркала — чтобы популярные пакеты не дёргались с Fastly каждый раз, а отдавались с диска быстро и независимо. Включаем proxy_cache для /files/.
В http {}:
proxy_cache_path /var/cache/nginx/pypi
levels=1:2
keys_zone=pypi_files:100m
max_size=200g # сколько места отдать под кеш пакетов
inactive=90d
use_temp_path=off;
И в location /files/:
location /files/ {
rewrite ^/files/(.*)$ /$1 break;
proxy_pass https://files.pythonhosted.org;
proxy_set_header Host files.pythonhosted.org;
proxy_ssl_server_name on;
proxy_cache pypi_files;
proxy_cache_valid 200 90d; # артефакты PyPI неизменяемы — можно кешировать надолго
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
proxy_cache_lock on;
add_header X-Cache-Status $upstream_cache_status; # HIT/MISS для проверки
}
Файлы пакетов на PyPI неизменяемы (новая версия = новый файл), поэтому их можно держать в кеше сколько угодно. После первого скачивания пакет отдаётся уже с вашего диска — быстро и без обращения к Fastly. Индекс /simple/ лучше кешировать коротко (минуты) или не кешировать вовсе, чтобы новые релизы появлялись без задержки.
Разово:
pip install --index-url https://pypi.example.com/simple/
Постоянно — в ~/.config/pip/pip.conf (или pip.ini на Windows):
[global] index-url = https://pypi.example.com/simple/
Или через переменную окружения:
export PIP_INDEX_URL=https://pypi.example.com/simple/
uv:
UV_INDEX_URL=https://pypi.example.com/simple/ uv pip install
Poetry (pyproject.toml):
[[tool.poetry.source]] name = “mirror” url = “https://pypi.example.com/simple/” priority = “primary”
# в индексе не осталось ссылок на pythonhosted — все переписаны
curl -s https://pypi.example.com/simple/flask/ | grep -c files.pythonhosted.org # 0
# реальная загрузка пакета через зеркало
pip download --no-deps --index-url https://pypi.example.com/simple/ click==8.1.7
# кеш работает? второй запрос файла должен дать X-Cache-Status: HIT
curl -sI https://pypi.example.com/files/<путь-к-файлу> | grep X-Cache-Status
Если pip резолвит пакет и тянет wheel через ваш /files/ — всё собрано правильно.
Не хотите поднимать своё — берите готовое
Если разворачивать и обслуживать собственный сервер не хочется, есть уже работающее зеркало — pypi.depkit.ru [7]. Оно:
работает на российских IP, внутри страны — не зависит от доступности Fastly;
имеет большой объём кеша под пакеты, так что популярные wheels отдаются мгновенно;
быстрое и прозрачное — достаточно подставить его в index-url.
Подключение — те же команды, что выше, только домен другой:
pip install --index-url https://pypi.depkit.ru/simple/ <package>
# ~/.config/pip/pip.conf
[global]
index-url = https://pypi.depkit.ru/simple/
Своё зеркало PyPI — это:
один server-блок nginx из двух location плюс proxy_cache;
никаких терабайт: индекс проксируется, файлы кешируются по запросу;
независимость от иностранного CDN для базового инструмента разработчика.
Поднять можно за вечер, а если некогда — pypi.depkit.ru [3] уже работает на российских IP, с большим кешем и быстро. Вчерашняя блокировка лишний раз показала: такой риск дешевле закрыть заранее, чем чинить сборки в момент аварии.
Автор: Geri4
Источник [8]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/python/452653
Ссылки в тексте:
[1] pypi.org: http://pypi.org
[2] files.pythonhosted.org: http://files.pythonhosted.org
[3] pypi.depkit.ru: http://depkit.ru
[4] https://files.pythonhosted.org/: https://files.pythonhosted.org/
[5] https://pypi.example.com/files/: https://pypi.example.com/files/
[6] ;: https://pypi.example.com/files/;
[7] pypi.depkit.ru: http://pypi.depkit.ru
[8] Источник: https://habr.com/ru/articles/1043976/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1043976
Нажмите здесь для печати.