Как собрать своё зеркало PyPI на nginx за вечер (и не зависеть от блокировок pypi.org)

от автора

Зачем это вообще нужно

Вчера pypi.org несколько часов был недоступен из российских сетей. Для кого-то это «подождём», а для CI/CD, прода и просто рабочего дня — это вставший pip install и красные сборки.

Причина системная: pypi.org и хранилище пакетов files.pythonhosted.org живут на CDN Fastly, у которого нет точек присутствия в России и доступ к которому уже не раз ограничивался. Вчерашняя недоступность — не первая и почти наверняка не последняя.

Хорошая новость: чтобы застраховаться, не нужно зеркалировать весь PyPI (это терабайты и постоянная синхронизация). Достаточно поднять лёгкий реверс-прокси на nginx. В этом гайде соберём такой с нуля — с кешированием и прозрачным переключением для pip.

Не хотите хостить сами? Есть уже готовое зеркало — pypi.depkit.ru. Оно работает на российских IP, имеет большой объём кеша под пакеты и отдаёт их очень быстро. Можно просто подставить его в index-url (как — в конце статьи) и пропустить всю настройку. Дальше — для тех, кому интересно поднять своё.

Как это устроено

pip работает с PyPI в два шага:

  1. Берёт индекс /simple/<пакет>/ — небольшую HTML-страницу со списком всех файлов пакета и ссылками на них.

  2. По ссылкам из индекса скачивает сами файлы (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;      }  }

Логика:

Подставьте свой 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/