UMGUM.COM 

Docker Registry ( Разворачиваем минимально функциональный сервер репозиториев docker-образов из эталонной реализации "Docker Registry", с простой HTTP-аутентификацией и web-интерфейсом "Docker Registry UI" для просмотра мета-данных и удаления ненужного )

1 марта 2021

OS: "Linux Debian 9/10", "Linux Ubuntu Server 16/18/20 LTS".
Apps: "Nginx", "Let`s Encrypt", "Docker Registry", "Docker Registry UI", "Docker", "Docker Compose".

Задача: развернуть минимально функциональный сервер репозиториев docker-образов из эталонной реализации "Docker Registry", с простой HTTP-аутентификацией и web-интерфейсом "Docker Registry UI" для просмотра мета-данных и удаления ненужного.


Всё просто - последовательно делаем следующее:

1. Подготовка системного окружения (отдельная инструкция);
2. Установка системы контейнеризации "Docker" (отдельная инструкция);
3. Установка сопутствующего ПО и подготовка конфигурации;
4. Подготовка среды и запрос "Let`s Encrypt" SSL-сертификатов;
5. Подготовка конфигурации для фронтального web-сервера "Nginx";
6. Наладка запуска посредством "Docker Compose";
7. Настройка автозапуска "Docker Compose" посредством "Systemd";
8. Проверка функциональности docker-репозитория;
9. Удаление контейнеров из регистра посредством "docker API";
10. Автоматизация очистки от ненужных данных в "Docker Registry".


Подготовка несущей файловой структуры для приложения "Docker Registry".

Создаём в файловой системе несущей операционной системы директории для данных и журналов событий:

# mkdir -p /var/opt/docker-registry/data
# mkdir -p /var/opt/docker-registry/logs

В контейнере "Docker Registry" запускается к контексте суперпользователя, так что ограничение доступа к данным посторонним достаточно просто реализуется:

# chown -R root:root /var/opt/docker-registry
# chmod -R go-rwx /var/opt/docker-registry

Также готовим место для журналов событий web-сервиса управления содержимым "Docker Registry":

# mkdir -p /var/opt/docker-registry-ui/logs
# chown -R root:root /var/opt/docker-registry-ui
# chmod -R go-rwx /var/opt/docker-registry-ui

Подготовка среды и запрос "Let`s Encrypt" SSL-сертификатов.

Если имеется wildcard-сертификат, который используется для терминирования SSL/TLS-подключений, то этот этап можно пропустить. Для мелких же проектов наладка работы с автоматически запрашиваемыми и продлеваемыми сертификатами от "Let`s Encrypt" в последние годы стала стандартом.

Создаём в файловой системе несущего сервера директорию для сертификатов и генерируем DH-файл:

# mkdir -p /var/opt/letsencrypt
# openssl dhparam -out /var/opt/letsencrypt/dhparam-2048.pem 2048
# chown -R root:root /var/opt/letsencrypt && chmod -R o-rwx /var/opt/letsencrypt

Подготавливаем структуру для агента "certbot", запрашивающего и продлевающего сертификаты:

# mkdir -p /var/opt/certbot/nginx/etc/conf.d
# mkdir -p /var/opt/certbot/logs
# mkdir -p /var/opt/certbot/webroot/.well-known/acme-challenge

Внутри docker-контейнера Nginx запущен в контексте пользователя с UID:101 - даём право такому для чтения из директории "webroot" (процесс запроса и подтверждения владения web-ресурсом уже расписан на этом сайте):

# chown -R 101:root /var/opt/certbot && chmod -R o-rwx /var/opt/certbot

Опишем крайне простую конфигурацию сайта, предназначенного лишь для выдачи ключа валидации по запросу со стороны сервиса "Let`s Encrypt":

# vi /var/opt/certbot/nginx/etc/conf.d/dr.example.net.conf

server {
  listen      80;
  listen [::]:80;
  server_name dr.example.net;
  server_tokens off;
  location /.well-known/acme-challenge {
    root /var/www/letsencrypt;
  }
}

Считая, что никаких других web-сервисов на несущем сервере ещё нет, запускаем docker-контейнер с "Nginx", только лишь для первичного запроса SSL-сертификатов:

# docker run -d --rm --name letsencrypt-nginx -p 80:80 -v /var/opt/certbot/nginx/etc/conf.d:/etc/nginx/conf.d -v /var/opt/certbot/webroot:/var/www/letsencrypt nginx:latest

Воспользовавшись готовой сборкой docker-образа агента "certbot" запрашиваем SSL-сертификаты у "Let`s Encrypt" для "Docker Registry" FQDN:

# docker run --rm --name letsencrypt-certbot -v /var/opt/certbot/webroot:/var/www/letsencrypt -v /var/opt/letsencrypt:/etc/letsencrypt certbot/certbot certonly --webroot --noninteractive --agree-tos --no-eff-email --register-unsafely-without-email -w /var/www/letsencrypt -d dr.example.net >> /var/opt/certbot/logs/docker-output-certbot.log 2>&1

После успешного получения SSL-сертификатов останавливаем вспомогательный инстанс "Nginx":

# docker stop letsencrypt-nginx

Автоматизация продления SSL-сертфикатов от "Let`s Encrypt".

Учитывая то, что в рабочем режиме фронтальный web-сервер всегда запущен, достаточно будет лишь наладить регулярный запуск агента "certbot" (в примере раз в неделю, каждый Вторник, в полночь):

# vi /etc/crontab

....
# Certificates renew using Let`s Encrypt "Certbot client" in Docker
0 0  * * 2  root docker run --rm --name letsencrypt-certbot -v /var/opt/certbot/webroot:/var/www/letsencrypt -v /var/opt/letsencrypt:/etc/letsencrypt certbot/certbot renew --noninteractive >> /var/opt/certbot/logs/docker-output-certbot.log 2>&1 && docker restart nginx &

Подготовка конфигурации для фронтального web-сервера "Nginx".

При наладке аутентификации в спарке "Nginx" и "Docker Registry" важно учитывать два следующих нюанса.

Во-первых, "Nginx" поддерживает для хранения паролей только формат "crypt" (документация), а "Docker Registry" - только более безопасный "bcrypt" (документация). Потому, при выносе аутентификации на сторону "Nginx" для генерации паролей следует явно использовать более слабый "crypt", что в целом нестрашно, так как в любом случае все данные передаются через защищённое SSL/TLS соединение.

Во-вторых, "Docker Registry" при аутентификации осуществляет привязку пользователя только к точке входа в сервер, откидывая URL-path (подкаталоги, имена файлов, ?-запросы и #-якоря), оставляя лишь URL-host и URL-port. Таким образом, нельзя в рамках одного "доменного имени" (FQDN) разделять обращения к разным инстансам "Docker Registry", так как и при обращении к условно "https://example.net/v2/registry1", и при обращении к "https://example.net/v2/registry2", docker-клиент будет запрашивать аутентификацию строго на URL "https://example.net/v2/". Теоретически и такую схему можно счесть полезной, производя предварительную аутентификацию на входе посредством "Nginx", распределяя потом транзации по разным инстансам "Docker Registry" - но полезный эффект минимален, на мой взгляд.

В условиях описанного выше ограничения "Docker Registry" посредством фронтального web-сервера можно было бы вначале аутентифицировать пользователя в условной точке входа "https://example.net/v2/", а уже после для аутентифицированного пользователя средствами того же web-сервера проводить авторизацию при доступе ниже по иерархии, например к подразделам "https://example.net/v2/registry1" и "https://example.net/v2/registry2" - но "Nginx" такой функциональностью не обладает, а другие web-серверы я в работе практически не применяю и о таких возможностях ничего не знаю.

В общем, на практике следует воспринимать "Docker Registry" как крайне простой сервис, строго привязанный адресу вида "FQDN:port", когда на одном сочетании "доменное имя и порт" может обслуживаться только один "Docker Registry".

Создадим в файловой системе несущего сервера структуры для конфигурации "Nginx":

# mkdir -p /var/opt/nginx/etc/auth
# mkdir -p /var/opt/nginx/etc/conf.d
# mkdir -p /var/opt/nginx/logs

Установим утилиту генерирования паролей в формате "htpasswd":

# apt-get install apache2-utils

Создаём необходимое количество учётных записей для метода аутентификации "htpasswd":

# htpasswd -bm /var/opt/nginx/etc/auth/registry.htpasswd service-ci-docker strongPassword

Конфигурация сайтов принимающего подключения пользователей web-сервера "Nginx" проста и сводится к описанию параметров "проксирования" запросов соответствующим web-сервисам, запущенным внутри docker-контейнеров:

# vi /var/opt/nginx/etc/conf.d/dr.example.net.conf

server {
  listen      80;
  listen [::]:80;
  server_name dr.example.net;
  server_tokens off;
  location /.well-known/acme-challenge {
    root /var/www/letsencrypt;
  }
  location / {
    return 307 https://$host$request_uri;
  }
}

server {
  listen      443 ssl http2;
  listen [::]:443 ssl http2;
  server_name dr.example.net;
  server_tokens off;
  ssl_certificate /etc/letsencrypt/live/dr.example.net/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/dr.example.net/privkey.pem;
  ssl_dhparam /etc/letsencrypt/dhparam-2048.pem;
  ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
  ssl_prefer_server_ciphers on;
  ssl_ciphers "HIGH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4";
  ssl_buffer_size 8k;
  ssl_stapling on;
  ssl_stapling_verify on;

  # Optimizing parameters for transferring large files
  client_max_body_size 0; # (default: 1m)
  proxy_connect_timeout 3s; # (default: 60s)
  proxy_read_timeout 900; # (default: 60s)
  chunked_transfer_encoding on;

  # Accepting connections only with the second version of the Docker Registry protocol
  location /v2/ {

    # Do not allow connections from docker 1.5 and earlier
    if ($http_user_agent ~ "^(docker\/1\.(3|4|5(?!\.[0-9]-dev))|Go ).*$" ) {
      return 404;
    }

    # Applying basic authentication for access to Docker Registry
    auth_basic "Registry realm";
    auth_basic_user_file /etc/nginx/auth/registry.htpasswd;

    # Connection settings with the Docker Registry
    proxy_set_header X-Real-IP $remote_addr; # (required)
    proxy_set_header X-Forwarded-Proto $scheme; # (required)
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # (required)
    proxy_set_header Host $http_host;
    proxy_set_header X-NginX-Proxy true;
    proxy_pass http://registry:5000;
    proxy_redirect off;
  }

  # Connection settings with the Docker Registry (web) UI
  location /ui/ {
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;
    proxy_set_header X-NginX-Proxy true;https://joxit.dev/docker-registry-ui/
    proxy_pass http://registry-ui:80/;
    proxy_redirect off;
  }

  # Redirect to frontend when requesting site root
  location = / {
    rewrite ^ https://$host/ui/$request_uri permanent;
  }
}

По умолчанию "Nginx" внутри docker-контейнера стартует в контексте суперпользователя "root", так что убираем доступ к его конфигурации для всех остальных:

# chown -R root:root /var/opt/nginx
# chmod -R go-rwx /var/opt/nginx

Наладка запуска посредством "Docker Compose".

Создаём директорию для размещения конфигурационных файлов "Docker Compose":

# mkdir -p /usr/local/etc/compose
# cd /usr/local/etc/compose

Воспользуемся для единственного в нашей схеме конфигурационного файла именем "по умолчанию":

# vi ./docker-compose.yml

version: "3"
services:
  nginx:
    depends_on:
      - registry
      - registry-ui
    container_name: nginx
    image: nginx:latest
    networks:
      registry:
        aliases:
          - "nginx"
    ports:
      - 80:80
      - 443:443
    environment:
      TZ: "/etc/timezone"
    volumes:
      - "/var/opt/nginx/etc/auth:/etc/nginx/auth:ro"
      - "/var/opt/nginx/etc/conf.d:/etc/nginx/conf.d:ro"
      - "/var/opt/letsencrypt:/etc/letsencrypt:ro"
      - "/var/opt/certbot/webroot:/var/www/letsencrypt:ro"

  registry:
    container_name: registry
    image: registry:2
    networks:
      registry:
        aliases:
          - "registry"
    environment:
      TZ: "/etc/timezone"
      REGISTRY_STORAGE_DELETE_ENABLED: "true"
      REGISTRY_LOG_ACCESSLOG_DISABLED: "false"
      #REGISTRY_LOG_LEVEL: "debug"
    working_dir: "/var/opt/docker-registry"
    volumes:
      - "/var/opt/docker-registry/data:/var/lib/registry:rw"
      - "/var/opt/docker-registry/logs:/var/log/registry:rw"

  registry-ui:
    depends_on:
      - registry
    container_name: registry-ui
    image: joxit/docker-registry-ui:static
    networks:
      registry:
        aliases:
          - "registry-ui"
    environment:
      TZ: "/etc/timezone"
      REGISTRY_TITLE: "Example`s Registry"
      REGISTRY_URL: "https://dr.example.net"
      PULL_URL: "https://dr.example.net"
      DELETE_IMAGES: "true"

networks:
  registry:
    driver: bridge
    internal: false
    ipam:
      driver: default
      config:
        - subnet: 100.127.255.0/24

Запускаем посредством "Docker Compose" всю пачку контейнеров:

# cd /usr/local/etc/compose
# docker-compose up --remove-orphans --build --force-recreate -d

Пример точечного запуска определённого контейнера:

# docker-compose up --no-start registry
# docker-compose start registry

Пример точечной остановки и высвобождения ресурсов определённого контейнера:

# docker-compose stop registry
# docker-compose rm -f -v registry

Останавливаем разом все docker-контейнеры, описанные в конфигурации "Docker Compose":

# docker-compose down

Настройка автозапуска "Docker Compose" посредством "Systemd".

Создаём файл описания параметров запуска и остановки docker-контейнера посредством "Docker Compose" посредством короткоживущего сервиса "Systemd":

# vi /etc/systemd/system/registry-docker.service

[Unit]
Description=Docker Registry in Docker Compose Service
Requires=network.target docker.service containerd.service
After=docker.service

[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/usr/local/etc/compose

ExecStartPre=-/bin/bash -c 'chown -R root:root /var/opt/docker-registry'
ExecStartPre=/usr/local/bin/docker-compose -f docker-compose.yml down --remove-orphans
ExecStart=/usr/local/bin/docker-compose -f /usr/local/etc/compose/docker-compose.yml up --remove-orphans --build --force-recreate --detach
#
ExecStop=/usr/local/bin/docker-compose -f /usr/local/etc/compose/docker-compose.yml down

[Install]
WantedBy=multi-user.target

Указываем "Systemd" перечитать и принять новую конфигурацию, а потом явно активируем и запускаем новый сервис:

# systemctl daemon-reload
# systemctl enable registry-docker.service
# systemctl start registry-docker

Смотрим журнал событий "Systemd" если "что-то пошло не так":

# systemctl status registry-docker.service
# journalctl -xe

Наладка ротации файлов журнала событий.

Не забываем, что для самодельных сервисов ротация их журналов событий не ведётся, и нужно настраивать это отдельно:

# vi /etc/logrotate.d/docker-opt

/var/opt/*/logs/*.log {
  size 30M
  missingok
  notifempty
  rotate 5
  compress
  delaycompress
  copytruncate
  su root root
}

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

# logrotate -d /etc/logrotate.d/docker-opt

Проверка функциональности docker-репозитория.

Первым делом осуществляем аутентификацию docker-клиента на целевом сервере "Docker Registry":

$ docker login -u service-ci-docker https://dr.example.net

Загружаем из официального docker-репозитория образ для тестирования, например web-сервера "Nginx":

$ docker pull nginx:latest

После загрузки образ можно наблюдать в локальном репозитории:

$ docker images

REPOSITORY TAG    IMAGE ID
....
nginx      latest 35c43ace9216

Помечаем локальный образ как доступный для загрузки в наш репозиторий:

$ docker tag 35c43ace9216 dr.example.net/nginx:latest

Теперь в локальном репозитории условно два одинаковых docker-образа с разными адресами хранения (на самом деле docker-образ физически один, но ему присвоено два "тега" с разными адресами):

$ docker images

REPOSITORY           TAG    IMAGE ID
....
dr.example.net/nginx latest f6d0b4767a6c
nginx                latest f6d0b4767a6c

Отдаём команду выгрузить в целевЕсли после этого пройти в web-интерфейс "Nexus", то там можно будет обнаружить сведения о загруженном docker-образе и описание его параметров в виде большого количества иерархически распределённых мета-данных.ой docker-репозиторий соответствующий ему docker-образ:

$ docker push dr.example.net/nginx

Если после этого пройти в web-интерфейс попутно развёрнутого здесь "Docker Registry UI", то там можно будет обнаружить сведения о загруженном docker-образе и описание его параметров в виде большого количества иерархически распределённых мета-данных.

Неудобно, что бесплатные версии docker-клиентов не поддерживают простое прямое удаление docker-образов из удалённого репозитория - это приходится делать через "docker API" нелинейной серией HTTP-запросов - мы рассмотрим эту методику далее.

Проще всего неподготовленному пользователю удалить ненужный ему образ через web-интерфейс "Docker Registry UI" (в примере для этого нужно будет обратиться по адресу "https://dr.example.net/ui"), если это разрешено опцией "REGISTRY_STORAGE_DELETE_ENABLED" конфигурации "Docker Registry".

Как минимум, легко можно удалить docker-образ (вернее один из "тегов" - только после удаления последнего "тега" будет удалён и docker-образ, как таковой) из локального репозитория операционной системы:

$ docker rmi docker push dr.example.net/nginx

После завершения всех процедур явно закрываем аутентифицированный сеанс с docker-репозиторием, во избежание:

$ docker logout dr.example.net

Удаление контейнеров из регистра посредством "docker API".

Простое удаление образов посредством CLI-утилит в "Docker Repository" доступно только для продуктов серии "Docker Enterprise", небесплатных и отдельно устанавливаемых. Для простого пользователя "Community Edition" остаётся только REST-API.

Разберём процесс по шагам с последующей автоматизацией такового.

Получаем список репозиториев:

$ curl -u service-ci-docker:*** -sSL "https://dr.example.net/v2/_catalog" | jq -r '.repositories[0]'

nginx

Получаем список "тэгов" в репозитории:

$ curl -u service-ci-docker:*** -sSL "https://dr.example.net/v2/nginx/tags/list" | jq -r '.tags[0]'

latest

Получаем из заголовков (HTTP Headers) ответа идентификатор (или список таковых) тегированного "образа":

$ curl -u service-ci-docker:*** -sSL -I -H "Accept: application/vnd.docker.distribution.manifest.v2+json" "https://dr.example.net/v2/nginx/manifests/latest" | tr -d '\r' | sed -En 's/^Docker-Content-Digest: (.*)/\1/pi'

sha256:b08...75a

Обращаю внимание, на то, что в "Docker Registry" удаление "образа" возможно только через удаление его "манифеста", по идентификатору последнего - в этом отличие от "Docker Hub (hub.docker.com)", например, где удаление "образа" возможно посредством указания его "тега".

Удаляем описание целевого "образа" по идентификатору его "манифеста":

$ curl -u service-ci-docker:*** -sSL -X DELETE "https://dr.example.net/v2/nginx/manifests/sha256:b08...75a"

При успешном исполнении операции никаких сообщений не будет. В противном случае покажут сообщение об ошибке, вроде следующих:

{"errors":[{"code":"UNSUPPORTED","message":"The operation is unsupported."}]}
....
{"errors":[{"code":"MANIFEST_UNKNOWN","message":"manifest unknown"}]}

Вышеописанная последовательность команд напрашивается на оформление в виде простого скрипта:

# vi ./docker-repository-rmi.sh

#!/bin/bash

REGISTRY='dr.example.net'
IMAGE='nginx'
AUTH='-u service-ci-docker:***'

curl ${AUTH} -sSL -X DELETE "https://${REGISTRY}/v2/${IMAGE}/manifests/$( \
  curl ${AUTH} -sSL -I \
    -H "Accept: application/vnd.docker.distribution.manifest.v2+json" \
    "https://${REGISTRY}/v2/${IMAGE}/manifests/$( \
      curl ${AUTH} -sSL "https://${REGISTRY}/v2/${IMAGE}/tags/list" | jq -r '.tags[0]' \
    )" \
  | tr -d '\r' | sed -En 's/^Docker-Content-Digest: (.*)/\1/pi' \
)"

exit $?

Примерный скрипт легко адаптировать упрощением до функционала удаления лишь конкретного "тега".

При том, надо понимать, что мы удалили только метаданные описания "образа", но сами файлы слоёв данных "образа" остались в файловой системе:

# du -h -d1 /var/opt/docker-registry/

8,0K /var/opt/docker-registry/logs
77M  /var/opt/docker-registry/data
77M  /var/opt/docker-registry/

Удалить файлы данных слоёв "образа" можно примерно по тому же принципу, как это делается для удаления описаний "образа" - но уже оперируя сущностями "blob", что сложно. Гораздо проще запустить внутреннюю процедуру "уборки мусора", инспектирующую слои данных "Docker Repository" и удаляющую осиротевшие (лишённые описаний и нигде более не используемые):

$ docker exec registry \
  registry garbage-collect \
    /etc/docker/registry/config.yml --dry-run=false --delete-untagged=true

В результате вышеописанных процедур мы высвободим файловую систему от слоёв данных "образа", но в директории "/var/opt/docker-registry/data/docker/registry/v2/repositories/nginx/_uploads/" может всё ещё остаться немало мегабайт ненужных данных:

# du -h -d1 /var/opt/docker-registry/

20K  /var/opt/docker-registry/logs
26M  /var/opt/docker-registry/data
26M  /var/opt/docker-registry/

Эти лишние данные будут удалены автоматически фоновой процедурой "Docker Registry", по умолчанию запускаемой раз в сутки. Удаляются файлы по истечению одной недели после их создания, когда они точно никому уже не понадобятся. Настройки определены в конфигурационном файле "/etc/docker/registry/config.yml" в следующей структуре:

version: 0.1
....
storage:
  maintenance:
    uploadpurging:
      enabled: true
      age: 168h
      interval: 24h
      dryrun: false
    readonly:
      enabled: false
....

Автоматизация очистки от ненужных данных в "Docker Registry".

Уборщик осиротевших слоёв docker-образов в эталонной реализации "Docker Registry" самостоятельно не работает, так что наладим его запуск по расписанию, раз в сутки:

# vi /etc/crontab

....
# Daily garbage collection in "Docker Registry"
01 5 * * * root docker exec registry registry garbage-collect /etc/docker/registry/config.yml -m &


Заметки и комментарии к публикации:


Оставьте свой комментарий ( выразите мнение относительно публикации, поделитесь дополнительными сведениями или укажите на ошибку )