Зачем это вообще нужно
Вчера pypi.org несколько часов был недоступен из российских сетей. Для кого-то это «подождём», а для CI/CD, прода и просто рабочего дня — это вставший pip install и красные сборки.
Причина системная: pypi.org и хранилище пакетов files.pythonhosted.org живут на CDN Fastly, у которого нет точек присутствия в России и доступ к которому уже не раз ограничивался. Вчерашняя недоступность — не первая и почти наверняка не последняя.
Хорошая новость: чтобы застраховаться, не нужно зеркалировать весь PyPI (это терабайты и постоянная синхронизация). Достаточно поднять лёгкий реверс-прокси на nginx. В этом гайде соберём такой с нуля — с кешированием и прозрачным переключением для pip.
Не хотите хостить сами? Есть уже готовое зеркало — pypi.depkit.ru. Оно работает на российских IP, имеет большой объём кеша под пакеты и отдаёт их очень быстро. Можно просто подставить его в index-url (как — в конце статьи) и пропустить всю настройку. Дальше — для тех, кому интересно поднять своё.
Как это устроено
pip работает с PyPI в два шага:
-
Берёт индекс /simple/<пакет>/ — небольшую HTML-страницу со списком всех файлов пакета и ссылками на них.
-
По ссылкам из индекса скачивает сами файлы (wheels и sdists) — они лежат на files.pythonhosted.org.
Идея зеркала: проксируем индекс с pypi.org, но на лету переписываем в HTML ссылки на файлы так, чтобы они вели на наш домен. Тогда и индекс, и файлы pip тянет через нас. Хранить ничего заранее не нужно — файлы проксируются (и кешируются) по запросу.
Переписывание делает директива nginx sub_filter — построчная замена в теле ответа.
Шаг 1. Базовый конфиг
Минимальный рабочий вариант — один 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;
-
sub_filter меняет в ней все https://files.pythonhosted.org/ на https://pypi.example.com/files/;
-
pip идёт за файлами уже на наш /files/;
-
location /files/ срезает префикс /files и проксирует на files.pythonhosted.org.
Подставьте свой server_name в обоих местах (в server_name и в строке sub_filter) и не забудьте сертификат — pip ходит только по HTTPS.
Шаг 2. Грабли, которые стоят пары часов отладки
-
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 — наш, на хранилище его быть не должно, поэтому срезаем перед проксированием.
Шаг 3. Добавляем кеширование
Главный смысл своего зеркала — чтобы популярные пакеты не дёргались с 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. Оно:
-
работает на российских 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 уже работает на российских IP, с большим кешем и быстро. Вчерашняя блокировка лишний раз показала: такой риск дешевле закрыть заранее, чем чинить сборки в момент аварии.
ссылка на оригинал статьи https://habr.com/ru/articles/1043976/