В этой статье я поделюсь своим опытом настройки container registry и приведу пример работы с ним. Что интересно, у меня GitLab сам работает в контейнере Docker, а еще непосредственно с интернетом взаимодействует Traefik.

Сначала я поставил GitLab непосредственно на сервер с Oracle Linux 8, а потом купил более мощный и перенес все в Docker. Ранее руки до настройки реестра не доходили, но, как говорится, хватит это терпеть. Обговорим, что работы проводятся на версии 18.2.1, и что я буду делать отдельный субдомен glcr.example.com (с соответствующими записями A и AAAA) и приступим.

Настройка

Но сначала давайте на всякий случай сделаем резервную копию. В идеале, когда фоновые задания не работают:

docker exec -t gitlab gitlab-backup create

А также стоит отдельно сохранить gitlab.rb и, на всякий, gitlab-secrets.json. В моем случае эти файлы находятся в /srv/gitlab/config.

Итак, изначально у меня реестр контейнеров был выключен. Это облегчает задачу создания БД метаданных, которую предлагается использовать, начиная с GitLab 17.3. Для начала, я удалил собственную правку registry['enable'] = false (по умолчанию он включен). Базово настраиваем реестр, причем с отключением собственного https (напоминаю про Traefik), но оставляем встроенный nginx слушать 5050 порт.

registry_external_url 'https://glcr.example.com'
registry_nginx['listen_port'] = 5050
registry_nginx['listen_https'] = false

Сейчас надо добавить блок настройки базы данных, но возникла дилемма: пользоваться ли собственным PostgreSQL GitLab или, раз у нас Docker, быстренько намутить отдельный. Решил сделать отдельный; таков путь. Предположительно обслуживать этот вариант проще, хотя меня немного смущают 100500 запущенных слонов.

Примечание от 20.09.2025. Получилось так, что вскоре после публикации статьи вышел GitLab 18.3, а в нем, согласно документации, достаточно просто включить базу данных в gitlab.rb. Поэтому настройка внешней БД, как описано далее, скорее всего уже не актуальна.

Выключаем систему (docker compose down) и правим compose.yaml:

services:
  regmd:
    image: postgres:17
    restart: always
    shm_size: 128mb
    environment:
      POSTGRES_USER: registry
      POSTGRES_PASSWORD: '<registry_password_placeholder_change_me>'
    networks:
      - gitlab
    volumes:
      - regmd:/var/lib/postgresql/data
  gitlab:
    image: gitlab/gitlab-ce:latest
    container_name: gitlab
    restart: always
    depends_on:
      - regmd
    environment:
      GITLAB_OMNIBUS_CONFIG: |
        # Add any other gitlab.rb configuration here, each on its own line
        external_url 'https://git.example.com'
        letsencrypt['enable'] = false
        nginx['listen_port'] = 80
        nginx['listen_https'] = false
        gitlab_rails['gitlab_shell_ssh_port'] = 2424
    ports:
      - '2424:22'
    networks:
      - default
      - gitlab
    expose:
      - '5050'
      - '8090'
    labels:
      - traefik.enable=true
      - traefik.http.routers.gitlab.rule=Host(`git.example.com`)
      - traefik.http.routers.gitlab.service=gitlab
      - traefik.http.routers.gitlab.middlewares=compress
      - traefik.http.services.gitlab.loadbalancer.server.port=80
      - traefik.http.routers.registry.rule=Host(`glcr.example.com`)
      - traefik.http.routers.registry.service=registry
      - traefik.http.services.registry.loadbalancer.server.port=5050
      - traefik.http.routers.pages.rule=Host(`pages.example.com`)
      - traefik.http.routers.pages.service=pages
      - traefik.http.routers.pages.middlewares=compress
      - traefik.http.services.pages.loadbalancer.server.port=8090
    volumes:
      - '$GITLAB_HOME/config:/etc/gitlab'
      - '$GITLAB_HOME/logs:/var/log/gitlab'
      - '$GITLAB_HOME/data:/var/opt/gitlab'
    shm_size: '256m'

networks:
  default:
    name: traefik
    external: true
  gitlab:
    internal: true

volumes:
  regmd:

Добавил сервис regmd для базы реестра, сделал внутреннюю сеть для его изоляции от Traefik и поставил запуск GitLab в зависимость, открыл 5050 порт контейнера и прописал правила хоста glcr.example.com для Traefik. Сжатие по идее не нужно, ведь слои и так передаются в запакованном виде.

В gitlab.rb добавляем настройки базы метаданных реестра. В соответствии с инструкцией, сначала в выключенном виде:

registry['database'] = {
  'enabled' => false,
  'host' => 'regmd',
  'user' => 'registry',
  'password' => '<registry_password_placeholder_change_me>',
  'dbname' => 'registry'
}

Сгенерируйте пароль для переменной POSTGRES_PASSWORD и подставьте сюда.

Включаем обратно (docker compose up -d), ждем, когда это все запустится (например до обнаружения сервиса Traefik'ом) и применяем миграции:

docker exec -t gitlab gitlab-ctl registry-database migrate up

Включаем БД реестра в настройках и вновь перезапускаем приложение для применения.

Примечание от 15.08.2025. Первоначально я почему-то решил, что раз реестр слушает 127.0.0.1:5000, то Nginx выключить не получится, а ведь в данном случае у него единственная задача – проксировать подключения. Оказывается, за это отвечает настройка registry['registry_http_addr'], таким образом в gitlab.rb можно внести изменения:

registry['registry_http_addr'] = "0.0.0.0:5000"
registry_nginx['enable'] = false

И перенастроить Traefik (через /srv/gitlab/compose.yaml) для взаимодействия с 5000-м портом вместо 5050.

Теперь мы можем авторизовать Docker в реестре:

docker login glcr.example.com

Здесь конечно имеется в виду клиентская машина, серверу-то самому себя авторизовать наверное особого смысла нет. smile Если вы, как и я, пользуетесь токенами, то предварительно его нужно создать с правами read_registry, write_registry, read_virtual_registry и, на всякий случай, write_virtual_registry. Виртуальные реестры появляются в рамках работы…

Прокси зависимостей

Оно же Dependency proxy for container images. Иными словами, GitLab может кэшировать образы с docker.io. По умолчанию включено, но работает анонимно. Если есть аккаунт в Docker Hub, то целесообразно его задействовать – будет меньше ограничений на загрузку. Возможный недостаток в том, что работает это все на уровне групп.

Соответственно создадим ее, например Docker. В Settings – Package and registers, разделе Dependency proxy, заполняем Docker Hub authentication. Для этого я создал персональный токен доступа.

Screenshot_2025-08-11_at_15-37-41.png

Теперь, чтобы образы кэшировались, явно указываем префикс: git.example.com/docker/dependency_proxy/containers (иными словами, вместо docker.io; library можно не писать). Под docker в данном случае понимается группа (как я всех запутал crazy). Внимание, нужно авторизовать Docker еще и по основному адресу GitLab, иначе будет бо-бо vava, в смысле 403 Access Denied:

docker login git.example.com

Теоретически можно выполнить вход с разными токенами: на glcr с правами read_registry и write_registry, а на git с read_virtual_registry и write_virtual_registry, но, сказать по правде, проверять это немножко лень. sorry

Пример

Не будем изменять традиции и продолжим мучить демо-приложение Symfony. Создадим репозиторий demo, для простоты предлагаю инициализировать его файлом README (флаг по умолчанию). В настройках же (Settings – General; Visibility, project features, permissions) я предлагаю выключить все, кроме репозитория как такового, и включить Container registry.

Screenshot_2025-08-11_at_17-50-28.png

Разработка

Нам понадобятся Docker и git. Я пользуюсь виртуальной машиной Arch Linux, в которой у меня есть пользователь dev, включенный в группу docker (дабы выполнять команды docker без sudo).

Клонируем вновь созданный репозиторий:

cd /srv/docker
git clone https://git.example.com/user/demo.git
cd demo
mkdir app

Давайте сделаем отдельные образы для среды разработки и боевой. На основе обзора Traefik формируем dev.dockerfile:

FROM git.example.com/docker/dependency_proxy/containers/dunglas/frankenphp

# Install PHP extensions
RUN install-php-extensions \
        intl

# Install composer and unzip
RUN set -eux; \
        curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer; \
        # smoke test
        composer --version; \
        apt-get update; \
        apt-get install -y --no-install-recommends \
                unzip \
        ; \
        rm -rf /var/lib/apt/lists/*

# Use the default development configuration
RUN cp "$PHP_INI_DIR/php.ini-development" "$PHP_INI_DIR/php.ini"

# Run under unprivileged user
ARG WWWUSER
RUN set -eux; \
        useradd -m -u $WWWUSER dev; \
        # Add additional capability to bind to port 80
        setcap CAP_NET_BIND_SERVICE=+eip /usr/local/bin/frankenphp; \
        # Give write access to /data/caddy and /config/caddy
        chown -R ${WWWUSER}:${WWWUSER} /data/caddy && chown -R ${WWWUSER}:${WWWUSER} /config/caddy

USER dev

Образ FrankenPHP довольно большой, но гулять так гулять. Как видите, я его проксировал. Еще одно отличие – использование конфигурации PHP для среды разработки. Наконец, будем работать под «симметричным» пользователем dev.

Файл compose.yaml:

services:
  php:
    build:
      context: .
      dockerfile: dev.dockerfile
      args:
        WWWUSER: "${WWWUSER:-1000}"
    restart: unless-stopped
    ports:
      - "80:80"
    environment:
      CADDY_GLOBAL_OPTIONS: "auto_https off"
      SERVER_NAME: "http://"
    volumes:
      - ./app:/app
      - caddy_data:/data
      - caddy_config:/config

volumes:
  caddy_data:
  caddy_config:

Чтобы не усложнять, открыл 80-й порт.

По отработанной схеме разворачиваем проект через composer. Заходим:

docker compose run -it --rm php bash

В контейнере:

cd /
composer create-project symfony/symfony-demo app
echo '/data' >> app/.gitignore
exit

Генерировать ключ для dev-окружения уже не нужно. Вместо этого подправим app/.gitignore, чтобы в репозиторий не попали базы данных. Запускаем приложение:

docker compose up -d

Надеюсь, у вас все заработает. Пушим:

git add app
git add compose.yaml
git add dev.dockerfile
git commit -m 'установил приложение'
git push

Отправлять образ для разработки в реестр скорее всего не имеет смысла.

Сборка

Создадим .dockerignore, который будет повторять app/.gitignore с учетом путей:

app/.php-version

###> symfony/framework-bundle ###
app/.env.local
app/.env.local.php
app/.env.*.local
app/config/secrets/prod/prod.decrypt.private.php
app/public/bundles/
app/var/
app/vendor/
###< symfony/framework-bundle ###

###> symfony/asset-mapper ###
app/public/assets/
app/assets/vendor/
###< symfony/asset-mapper ###

###> phpstan/phpstan ###
app/phpstan.neon
###< phpstan/phpstan ###

###> phpunit/phpunit ###
app/.phpunit.result.cache
app/phpunit.xml
###< phpunit/phpunit ###

app/data

Этот файл требуется, чтобы не скопировать ничего лишнего в образ, который будет собираться с помощью «продуктового» prod.dockerfile:

FROM git.example.com/docker/dependency_proxy/containers/dunglas/frankenphp AS build

# Install composer and unzip
RUN set -eux; \
        curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer; \
        # smoke test
        composer --version; \
        apt-get update; \
        apt-get install -y --no-install-recommends \
                unzip \
        ; \
        rm -rf /var/lib/apt/lists/*

# Build project
COPY app/ /app/
ENV APP_ENV=prod
RUN set -eux; \
        cd /app; \
        chmod 755 bin/console; \
        composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader; \
        php bin/console asset-map:compile

#------------------------------------------------------------------------#
FROM git.example.com/docker/dependency_proxy/containers/dunglas/frankenphp

# Install PHP extensions
RUN install-php-extensions \
        intl

COPY --from=build --chown=33:33 /app/ /app/

# Use the default production configuration
RUN cp "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"

# https://frankenphp.dev/docs/docker/#running-as-a-non-root-user
RUN set -eux; \
        # Add additional capability to bind to port 80 and 443
        setcap CAP_NET_BIND_SERVICE=+eip /usr/local/bin/frankenphp; \
        # Give write access to /data/caddy and /config/caddy
        chown -R www-data:www-data /data/caddy && chown -R www-data:www-data /config/caddy

USER www-data
ENV APP_ENV=prod

Применяем многоэтапную сборку. На первом этапе дополнительные расширения не нужны, но для сборки проекта потребуются composer и unzip. Копируем исходники в образ и натравляем на них composer, причем нам не нужно устанавливать пакеты для среды разработки. Следствием этого требуется явное указание среды приложения (ENV APP_ENV=prod) (или можно было бы sed'ом подправить app/.env), иначе пытаются загрузиться отладочные классы, которых не будет.

На втором этапе теперь уже устанавливаем расширения PHP, а composer и компания нам не нужны. Копируем сборку из первого этапа, используем php.ini для рабочего окружения, и настраиваем запуск под непривилегированным пользователем.

Собираем образ:

docker build -t glcr.example.com/user/demo:release -f prod.dockerfile .

Точка в конце – это контекст сборки (текущий каталог).

Пушим изменения в git (это понятно), а образ – в реестр:

docker push glcr.example.com/user/demo:release
Screenshot_2025-08-11_at_19-57-41.png

Метку latest вроде как лучше не использовать. Мало ли, что там в ней окажется… С другой стороны, dunglas/frankenphp я беру как раз самый последний, но там можно, там он правильный. smile В общем я решил присвоить тег release.

Выкат

Проверить лучше на другой машине, совсем хорошо на VPS/VDS. Здесь потребуется лишь Докер. Авторизуем его:

docker login glcr.example.com

В идеале нужен отдельный токен доступа с правом read_registry.

Для определенности на сей раз пусть будет Debian/Ubuntu, а основной директорией /var/www. Подготовимся:

mkdir -p /var/www/demo/data
cd /var/www/demo
chown www-data:www-data data

Пример compose.yaml для VPS с доменом demo.example.com (HTTPS, HTTP/3, все как положено):

services:
  php:
    image: glcr.example.com/user/demo:release
    restart: unless-stopped
    ports:
      - "80:80" # HTTP
      - "443:443" # HTTPS
      - "443:443/udp" # HTTP/3
    environment:
      CADDY_GLOBAL_OPTIONS: "email admin@example.com"
      SERVER_NAME: "demo.example.com"
      APP_SECRET: "<generate_secure_secret_key>"
    volumes:
      - ./data:/app/data
      - caddy_data:/data
      - caddy_config:/config

volumes:
  caddy_data:
  caddy_config:

Не забудьте сгенерировать APP_SECRET, а еще мы пробрасываем директорию для базы данных SQLite. Запустим:

docker compose up -d

Сначала будет ошибка 500, поскольку базы данных по сути нет. Входим в контейнер:

docker exec -it demo-php-1 bash

Создаем схему БД:

php bin/console doctrine:schema:create

Поскольку зависимости среды разработки не установлены, выполнить команду php bin/console doctrine:fixtures:load невозможно, т.е. заполнить базу демонстрационными данными не получится. Так что, оставаясь в контейнере, можно лишь воссоздать (или создать своих) пользователей:

php bin/console app:add-user jane_admin kitten jane_admin@symfony.com "Jane Doe" --admin
php bin/console app:add-user john_user kitten john_user@symfony.com "John Doe"

Выходим из контейнера (exit) и можем заходить на сайт.

Screenshot_2025-08-11_at_22-03-25.png

Цикл

Подытоживая вышесказанное, весь процесс выглядит так:

  1. Внесение изменений в исходный код.
  2. Сборка образа: docker build -t glcr.example.com/user/demo:release -f prod.dockerfile .
  3. Публикация: docker push glcr.example.com/user/demo:release
  4. Обновление образа на проде: docker compose pull
  5. Перезапуск приложения: docker compose up -d

Напрашивается CI/CD, но с вашего позволения об этом как-нибудь в другой раз.

Резервное копирование

Стоит сказать, что работа с реестром резко повышают требования к дисковому пространству, в частности для архивов. Например, если до описанных экспериментов архив занимал 30 мегабайт, то после – более 400.

Как и сказано в документации, архивировать базу метаданных реестра (а потом, в случае чего, восстанавливать) необходимо самостоятельно. Создал директорию /srv/gitlab/regmd/backups и скрипт /srv/gitlab/backup:

#!/bin/bash

# основной архив GitLab
docker exec -t gitlab gitlab-backup create CRON=1

# формируем переменную для имени архива на основе текущей даты
now=$(date +"%Y%m%d")
# архив метаданных реестра
docker exec gitlab-regmd-1 pg_dump -U registry registry | gzip -9 > /srv/gitlab/regmd/backups/dump_$now.sql.gz
# удаляем архивы старше 7 дней
for FILE in `find /srv/gitlab/regmd/backups -type f -mtime +7`; do
  rm $FILE
done

# синхронизируем с облаком
rclone sync /srv/gitlab/data/backups meganz:/gitlab/backups
rclone sync /srv/gitlab/regmd/backups meganz:/regmd/backups

По традиции для облачной синхронизации я пользуюсь сочетанием rclone и mega.nz. Вместо отдельных cron-заданий для архивации GitLab и отправки в облако теперь достаточно вызвать данный скрипт:

0 2 * * * /srv/gitlab/backup

Все-таки жаль, что базу метаданных gitlab-backup не архивирует. Я некоторым образом переживаю за возможную несогласованность данных, если что-то произойдет между моментом архивации основной БД и метаданных реестра.

Обновление

В случае с Docker обновление GitLab сводится к вызову трех команд:

cd /srv/gitlab
docker compose pull
docker compose up -d

Теперь к ним добавляется применение миграций:

docker exec -t gitlab gitlab-ctl registry-database migrate up

Надеюсь, что когда-нибудь это будет происходить автоматически.

Заключение

Не так страшен черт, как его малюют. Хотя возможно меня закалил эксперимент с установкой GitLab на FreeBSD. Даже с учетом создания базы метаданных, настройка происходит довольно быстро. Но за удобства приходится расплачиваться резко возросшими требованиями к дисковому пространству. Еще один недостаток связан с ручным управлением этой самой базой. Теоретически можно обходиться без нее, но за счет еще большего расхода места и вроде как неких ограничений функциональности.

Тем не менее, если вы пользуетесь Docker (и/или Podman) и, тем более, личным (корпоративным) GitLab, то иметь свой реестр по сути обязательно. Тем более что собакен может подстраховать в плане образов с Docker Hub. Умел бы он кэшировать еще и гитхаб (ghcr.io)… rolleyes Мечтать не вредно.

Пользуясь случаем, хочу поделиться с вами моими образами PHP. Так получилось, что я уже выложил образ для FreeBSD (!), а статью про него решил выпустить до этой (хотел понаблюдать за расходом места). В работе находятся и другие, надеюсь, будет интересно и полезно.


Категория: Программирование, веб | Опубликовано 14.08.2025 | Редакция от 20.09.2025

Похожие материалы

Переезд реестра контейнеров GitLab в S3

Ранее я писал о MinIO в качестве хранилища реестра контейнеров, но в случае VDS/VPS такой вариант экономически не выгоден: чуть ли не на порядок дешевле воспользоваться услугой аренды S3 у какого-нибудь облачного провайдера. Что я и решил сделать, ведь место на сервере стало очень быстро заканчиваться. Заодно мигрируем прокси зависимостей, LFS и всякое такое.

Перенос GitLab на другой сервер в Docker

Примерно год я мучался с GitLab на сервере с двумя гигабайтами оперативки. Когда оплаченный период закончился, решил взять более мощный VDS по формуле 4/4/30. До этого сам GitLab был установлен непосредственно из репозитория, но для экспериментов с Pages и т.д. нужен Docker. А раз он и так есть, почему бы не завернуть GitLab в контейнер? Заодно на сервер можно будет установить что-нибудь еще.


Комментарии, обсуждение