Как настроить собственный Docker Registry и Pull Proxy

Для чего и чтобы что.

Иногда так бывает, что ты делаешь себе потихоньку что-то одно, а потом звезды сходятся в одну им, звездам, известную конфигурацию и ты бросаешь всё и начинаешь заниматься совсем дрегим делом.

Так случилось и со мной, а толчком этому послужила новая политика Docker Hub - не более 100 анонимных и не более 200 авторизованных пуллов в шесть часов. Сначала мы не придали этому большого значения, но количество проектов росло, количество деплоев на каждом - тоже и, в итоге, мы уткнулись в этот лимит. Можно было примотать какого-нибудь пользователя и жить до следующего лимита или же достать кошелек, но в кошельке мухи дохлые да дух святой и боле ничего, так что пришлось пилить нормальное решение.

Плюсом легло то, что я давно мечтал о собственном, сугубо локальном registry для хранения наших предсобранных образов - так две задачи объединились в одну.

Нам был нужен локальный docker-registry, доступный из локальной сети по внутреннему доменному имени через https без авторизации. К тому же этот registry должен не только хранить и отдавать наши собственные образы, но и проксировать запросы к Docker Hub без всяких дополнительных танцев.

Проще говоря, если условимся, что адрес будет registry.local, то все должно работать так:

docker pull registry.local/redis

отдает redis:latest из собственного кэша, если он там есть или же из docker hub, если в кэше не нашлось,

docker push registry.local/my-own-image

заливает образ в registry и, разумеется,

docker pull registry.local/my-own-image

этот образ отдает. При этом никаких действий на клиентских машинах производиться не должно.

Проблема

Для начала было решено использовать стандартный docker registry в режиме Registry as a pull through cache, но вот беда - registry не умеет одновременно работать и как, собственно, registry и как кэш - тут либо одно либо другое. Простой вариант - посадить два registry на соседние порты и пользоваться в таком вот режиме, но так, как любит говорить наш руководитель, только чужие для хищников пишут, а мы всё ж для людей стараемся. А значит, что точка входа должна быть одна, а там где одна точка входа на несколько сервисов, то там у нас что?

nginx

Куда ж без nginx? Но о нем мы поговорим чуть позже, а сначала - план!

Значит, первым на очереди на случай пуша образа у нас стоит обычный локальный registry, а на pull нам потребуется запросить образ в локальном registry, если нет - перенаправить запрос в кеш, ну а если и кеш не найдет ни у себя ни в вышестоящем registry, то ответить “Увы”. План прост как три рубля!

Но тут полезли подводные камушки.

library

Сперва надо было убедиться, что registry в режиме кэша работает как от него ожидается, так что на скорую руку я соорудил кэш:

version: '3'
services:
  cache:
    restart: always
    image: registry:2
    environment:
      REGISTRY_PROXY_REMOTEURL: https://registry-1.docker.io
    volumes:
      - /DATA/registry:/var/lib/registry

После этого я запросил образ редиса и получил 404. WTF, подумал я и полез а логи. Однако, выяснилось, что действительно, на registry-1.docker.io нет никакого образа redis. Зато есть образ library/redis. И вообще, как я потом уже нашел

docker pull ubuntu instructs docker to pull an image named ubuntu from the official Docker Hub. This is simply a shortcut for the longer docker pull docker.io/library/ubuntu command

отсюда

Проще говоря, все официальные образы хранятся у пользователя library, а docker hub это просто шорткатит.

Ну раз у докера есть такой шорткат, то и нам такой заиметь придется, а тут уже без nginx никак.

теперь действительно nginx

План наш немного дополнился - если на последнем шаге кэш ничего не нашел, то есть ненулевая вероятность, что пользователь запрашивает официальный образ, а значит, надо поискать в library. Штош, в первом приближении собираем что-то такое:

nginx.conf

upstream registry {
    server registry:5000;
}
upstream cache {
    server cache:5000;
}
client_max_body_size 10G;

server {
  listen 80 default_server;
  server_name registry.local;

  location / {
    proxy_pass http://registry;
    error_page 404 = @cache;
  }

  location @cache {
    proxy_pass http://cache;
    error_page 404 = @library;
  }

  location @library {
    rewrite /v2/(.*) /v2/library/$1 break;
    proxy_pass http://cache;
  }
}

Но на запрос редиса всё еще прилетает 404. Странно, не так ли? Очередной приступ гугления принес следующую информацию: по умолчанию nginx делает только один фолбэк на ошибку, а дальнейшие игнорирует. Почему - стоит Сысоева спросить, но есть способ это поправить:

recursive_error_pages  on;
proxy_intercept_errors on;  

И тут-то все заработало и тут-то… А нет, не работет. Ещё одним камнем оказалось то, что на отсутствующий образ docker hub вполне может отдать и 404 и 500. Пришлось добавить обработку и этой ошибки. Все, образы пуллятся, все счастливы, открываем шампанское! Но кажется, что что-то позабыли… Точно, пуш забыли! А пуш-то и сломался.

По причине, мне неизвестной, при пуше образа последний POST-запрос должен получить 404 вкачестве подтверждения, что образ запушен успешно. А на 404 у нас что? Правильно, редирект. Штош, отловим и это поведение:

    if ($request_method = GET ) {
        error_page 404 = @cache;
    }

Рабочий вариант

Вот такой в итоге получается рабочий вариант конфига:

upstream registry {
    server registry:5000;
}
upstream cache {
    server cache:5000;
}
client_max_body_size 10G;

server {
  listen 80 default_server;
  server_name registry.local;
  recursive_error_pages  on;
  proxy_intercept_errors on;

  location / {
    proxy_pass http://registry;
    if ($request_method = GET ) {
        error_page 404 = @cache;
    }
  }

  location @cache {
    proxy_pass http://cache;
    error_page 404 500 = @library;
  }

  location @library {
    rewrite /v2/(.*) /v2/library/$1 break;
    proxy_pass http://cache;
  }
}

Ну и docker-compose.yml для завершенности картины:

version: '3'
services:
  web:
    restart: always
    image: nginx
    ports:
      - 80:80
    volumes:
        - ./nginx.conf:/etc/nginx/conf.d/registry.nginx.conf

  cache:
    restart: always
    image: registry:2
    environment:
      REGISTRY_PROXY_REMOTEURL: https://registry-1.docker.io
    volumes:
      - /DATA/registry:/var/lib/registry

  registry:
    restart: always
    image: registry:2
    volumes:
      - /DATA/registry:/var/lib/registry

Понятное дело, надо чтобы в локальной сети этот сервер откликался на registry.local ну и я опустил историю с ssl просто потому что она довольно банальна и не имеет отношения к предмету статьи.

Вот в такой вот конфигурации все стало работать так, как и планировалось. Надеюсь, этот опыт будет вам полезен, ну и буду благодарен, если кто-нибудь расскажет мне про трюк с ошибкой 404 после пуша докер-образа.

comments powered by Disqus