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".
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
# 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
# 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
# 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
# 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
# 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;
}
}
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 &
# 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
# 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;
proxy_pass http://registry-ui:80/;
proxy_redirect off;
}
# Redirect to frontend when requesting site root
location = / {
rewrite ^ https://$host/ui/$request_uri permanent;
}
}
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;
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
# chmod -R go-rwx /var/opt/nginx
Наладка запуска посредством "Docker Compose".
Создаём директорию для размещения конфигурационных файлов "Docker Compose":
# mkdir -p /usr/local/etc/compose
# cd /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
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 --remove-orphans --build --force-recreate -d
Пример точечного запуска определённого контейнера:
# docker-compose up --no-start registry
# docker-compose start registry
# docker-compose start registry
Пример точечной остановки и высвобождения ресурсов определённого контейнера:
# docker-compose stop registry
# docker-compose rm -f -v 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
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
# systemctl enable registry-docker.service
# systemctl start registry-docker
Смотрим журнал событий "Systemd" если "что-то пошло не так":
# systemctl status registry-docker.service
# journalctl -xe
# journalctl -xe
Наладка ротации файлов журнала событий.
Не забываем, что для самодельных сервисов ротация их журналов событий не ведётся, и нужно настраивать это отдельно:
# vi /etc/logrotate.d/docker-opt
/var/opt/*/logs/*.log {
size 30M
missingok
notifempty
rotate 5
compress
delaycompress
copytruncate
su root root
}
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
....
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
....
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"}]}
....
{"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 $?
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/
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
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/
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
....
....
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 &
# Daily garbage collection in "Docker Registry"
01 5 * * * root docker exec registry registry garbage-collect /etc/docker/registry/config.yml -m &