Реестр контейнеров GitLab
PHP Docker GitLab git реестр контейнеров прокси зависимостей
В этой статье я поделюсь своим опытом настройки 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
Здесь конечно имеется в виду клиентская машина, серверу-то самому себя авторизовать наверное особого смысла нет. Если вы, как и я, пользуетесь токенами, то предварительно его нужно создать с правами
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. Для этого я создал персональный токен доступа.
Теперь, чтобы образы кэшировались, явно указываем префикс: git.example.com/docker/dependency_proxy/containers
(иными словами, вместо docker.io; library можно не писать). Под docker
в данном случае понимается группа (как я всех запутал ). Внимание, нужно авторизовать Docker еще и по основному адресу GitLab, иначе будет бо-бо
, в смысле 403 Access Denied:
docker login git.example.com
Теоретически можно выполнить вход с разными токенами: на glcr с правами read_registry и write_registry, а на git с read_virtual_registry и write_virtual_registry, но, сказать по правде, проверять это немножко лень.
Пример
Не будем изменять традиции и продолжим мучить демо-приложение Symfony. Создадим репозиторий demo, для простоты предлагаю инициализировать его файлом README (флаг по умолчанию). В настройках же (Settings – General; Visibility, project features, permissions) я предлагаю выключить все, кроме репозитория как такового, и включить Container registry.
Разработка
Нам понадобятся 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
Метку latest
вроде как лучше не использовать. Мало ли, что там в ней окажется… С другой стороны, dunglas/frankenphp я беру как раз самый последний, но там можно, там он правильный. В общем я решил присвоить тег
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
) и можем заходить на сайт.
Цикл
Подытоживая вышесказанное, весь процесс выглядит так:
- Внесение изменений в исходный код.
- Сборка образа:
docker build -t glcr.example.com/user/demo:release -f prod.dockerfile .
- Публикация:
docker push glcr.example.com/user/demo:release
- Обновление образа на проде:
docker compose pull
- Перезапуск приложения:
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)… Мечтать не вредно.
Пользуясь случаем, хочу поделиться с вами моими образами 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 в контейнер? Заодно на сервер можно будет установить что-нибудь еще.