Как мигрировать Gitlab под рабочей нагрузкой из одного в десять Docker‑контейнеров

Привет! Меня зовут Илья, я занимаюсь автоматизацией разработки аппаратного обеспечения в YADRO. Эта статья будет посвящена масштабированию работающего в Docker-контейнере под рабочей нагрузкой инстанса Gitlab. По мере роста команды производительности Gitlab, развернутого в одном Docker-контейнере, становится мало. И масштабирование работающего под рабочей нагрузкой Gitlab, скажем, на десять контейнеров может стать нетривиальной задачей. Далее я на нашем примере расскажу, как это разумно организовать.
По ходу повествования мы оценим, на что влияет кластеризация того или иного компонента, взглянем на примеры минимально необходимых docker-compose.yml, а также прикинем время недоступности Gitlab, необходимое на выполнение той или иной процедуры
Gitlab — одно из самых популярных и комплексных решений для разработки и DevOps, и не просто так. Вот минимальная команда для запуска Gitlab Community Edition в Docker-контейнере:
docker run --detach \
--hostname gitlab.example.com \
--env GITLAB_OMNIBUS_CONFIG="external_url 'https://gitlab.example.com'" \
--publish 80:80 --publish 22:22 \
--name single-node-gitlab \
--restart always \
--volume /docker/gitlab/config:/etc/gitlab \
--volume /docker/gitlab/logs:/var/log/gitlab \
--volume /docker/gitlab/data:/var/opt/gitlab \
--shm-size 256m \
gitlab/gitlab-ce:<version>-ce.0Ее хватит, чтобы мы получили open source веб-платформу, способную обеспечить:
хранение исходного кода в git-репозиториях;
совместную разработку и ревью кода;
инструменты управления проектами: отслеживание задач (issue tracker), встроенную вики, учет времени и т. д.;
встроенные инструменты CI/CD, хранения артефактов и контейнеров Docker.
Все это будет отлично работать при нагрузках до 20 RPS и/или 1000 пользователей. Но рано или поздно большинство команд вырастает, и производительности Gitlab в одном Docker-контейнере начинает не хватать. Интернет обычно предлагает сразу переезжать на новый кластер — желательно на новом железе. Это отличное, но дорогое решение в плане как развития, так и сопровождения.

Мы же выбрали другой вариант — не базе кластера Gitlab из Docker-контейнеров. Причин этому несколько:
Изначально наш Gitlab работал в единственном Docker-контейнере. Мы не переезжали на новый кластер, а понемногу дорабатывали существующий.
Все наше окружение контейнеризировано (Docker/Podman, k8s). Решили не дробить ландшафт.
По умолчанию в контейнерах все закрыто/отключено, так что нужно явно задать порты, прописывать ресурсы. Это помогает глубже понять инструмент.
Простота отката. Бывают случаи, когда минорный релиз выпущен с ошибкой.
Информационная безопасность. Отдельные компоненты в контейнере можно обновить быстрее, чем выйдет новый релиз Gitlab.
Такое масштабирование возможно, поскольку в официальном Docker-образе собраны все инструменты, необходимые для работы Gitlab. Всё, что нам нужно сделать – вынести компоненты на отдельные машины:

Отмечу особенность Ruby-приложения Gitlab: количество CPU для компонентов Gitlab должно быть степенью двойки. На конфигурациях с 7/9/12 CPU приложение работает в лучшем случае с неполной нагрузкой.
Важные уточнения и ограничения перед началом:
Кластеризуем мы на примере Gitlab 18.11.
Docker-compose.yml немного упрощены для фокусирования на взаимодействии компонентов.
В docker-compose.yml и gitlab.rb включены Prometheus-экспортеры, но само подключение к системе мониторинга не описано. Это тема для отдельного обсуждения.
Централизованный сбор логов и отправка их ELK (или подобный стек) не описаны, но предполагаются. Это тема для отдельного обсуждения.
Работаем под root, Docker без rootless. На проде rootless удобнее, так как на id можно привязать пользователей ОС с понятными именами. Но это делает примеры слишком громоздкими.
PostgreSQL
PostgreSQL — единственная СУБД, поддерживаемая Gitlab. Она содержит все метаданные о проектах, пользователях, merge requests, issues и т. д. Важно помнить, что мажорной версии Gitlab соответствует конкретная мажорная версия PostgreSQL.
WebGUI может не хватать производительности: медленно отрисовываются страницы, заполняются ветки в merge request, комментарии или что-нибудь еще, связанное с метаданными. Чтобы привести производительность в норму в этом случае, достаточно будет провести миграцию на внешний PostgreSQL.
Подготовка окружения
Не будем детально останавливаться на процессе запуска PostgreSQL в Docker — при желании можно почитать хороший гайд на Хабре. Аргументы против такого подхода от администратора БД можно посмотреть на stackoverflow. Порядок действий таков.
Создадим на хосте gitlab-pg-host файл /docker/docker-compose/docker-compose.yml c содержанием:
services:
postgres:
image: company-local-docker-images/postgres:16.14
container_name: gitlab-pg
logging:
options:
max-size: "1g"
max-file: "5"
environment:
POSTGRES_USER: gitlab
POSTGRES_PASSWORD: "DB_PASSWORD"
POSTGRES_DB: gitlabhq_production
shm_size: 1g
volumes:
- /docker/postgres/16/data:/var/lib/postgresql/data
ports:
- "5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U gitlab -d gitlabhq_production"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
restart: unless-stopped
deploy:
resources:
limits:
cpus: '2'
memory: 8G
networks:
- gitlabnet
postgres-exporter:
image: company-local-docker-images/postgres-exporter:0.17.1
container_name: pg-exporter
environment:
DATA_SOURCE_NAME: "postgresql://gitlab:DB_PASSWORD@postgres:5432/gitlabhq_production?sslmode=disable"
ports:
- "9187:9187"
depends_on:
- postgres
networks:
- gitlabnet
networks:
gitlabnet:
driver: bridgeShm_size: 1g — единственный параметр, который хочу отметить, так как он не описан в документации явно и может приводить к 500-м ошибкам в Web GUI или таким ошибкам в логах: FATAL: could not resize shared memory segment: No space left on device. По умолчанию Docker выделяет 64 МБ shared memory, которых не хватает даже для Gitlab в одном Docker-контейнере. 256 МБ — это необходимый минимум.
Запустим PostgreSQL:
docker compose -f /docker/docker-compose/docker-compose.yml up -dДожидаемся старта и редактируем /docker/postgres/16/data/postgresql.conf согласно рекомендациям вендора:
work_mem 8 MB
maintenance_work_mem 64 MB
max_connections 400 # по умолчанию 100
shared_buffers 2 GB #25% от RAM сервера.
statement_timeout 60000
hot_standby_feedback onПерезапускаем контейнер и проверяем, что можно подключиться к PostgreSQL.
Миграция
Если все прошло успешно, то планируем прерывание в обслуживании. Без этого вынести PostgreSQL за пределы Gitlab не представляется возможным.
Вся миграция состоит из двух шагов:
Перенести данные в новую базу PostgreSQL.
Переключить Gitlаb на внешней PostgreSQL.
Технически перенести данные из одной базы в другую можно без прерывания в обслуживании. Но высок риск потери новых записей, созданных в момент копирования и переключения. Поэтому я рассматривать такой вариант не буду.
В PostgreSQL пишут два компонента Gitlab:
Puma — веб-сервер Ruby-приложений, собственно, сам Gitlab. Его процессы мы видим в дереве процессов.
Sidekiq — менеджер очередей на Ruby.
Остановим эти процессы:
docker exec -it single-node-gitlab gitlab-ctl stop puma
docker exec -it single-node-gitlab gitlab-ctl stop sidekiqПроверим, что puma и sidekiq остановлены:
docker exec -it single-node-gitlab gitlab-ctl status
Вариантов миграции данных PostgreSQL у нас несколько.
Через rsync перенести данные с одного хоста на другой:
rsync -avz -e ssh single-node-gitlab-host@/docker/gitlab/data/postgresql/data/ gitlab-pg-host@/docker/postgres/16/data/Сделать полный бэкап Gitlab и восстановиться из него после переключения на новую базу:
docker exec -it single-node-gitlab gitlab-backup createСделать бэкап базы средствами Gitlab:
docker exec -it single-node-gitlab gitlab-backup create SKIP=tar,repositories,uploads,builds,artifacts,pages,lfs,terraform_state,registry,packages,ci_secure_files,agent_plan_content,external_diffsВ первом варианте я не смогу переиспользовать rsync при обновлении версии PostgreSQL, а снимать бэкап долго. Поэтому так я делать не буду, а вместо этого мигрирую данные через создание и восстановление данных докерезированного PostgreSQL.
1. Создаем бэкап на хосте с запущенным в одном Docker-контейнере Gitlab:
docker exec -t single-node-gitlab pg_dump -U gitlab gitlabhq_production > gitlabdatabase.sql. `date +%Y-%m-%d"_"%H_%M_%S`2. Оставаясь на той же машине, восстанавливаем бэкап. На случай если на хосте нет psql, лучше делать все через контейнер с Gitlab.
cat gitlabdatabase.sql. `date +%Y-%m-%d"_"%H_%M_%S` | docker exec -i single-node-gitlab psql -h gitlab-pg-host -U gitlab -d gitlabhq_production3. Проверяем, что восстановление прошло без ошибок и к базе можно подключиться удаленно. Если все хорошо, редактируем конфигурационный файл Gitlab gitlab.rb. В нашем случае файл будет располагаться в /docker/gitlab/config/ согласно инструкции:
# Disable the bundled Omnibus provided PostgreSQL
postgresql['enable'] = false
# PostgreSQL connection details
gitlab_rails['db_adapter'] = 'postgresql'
gitlab_rails['db_encoding'] = 'unicode'
gitlab_rails['db_host'] = gitlab-pg-host' # IP/hostname of database server
gitlab_rails['db_port'] = 5432
gitlab_rails['db_password'] = DB_PASSWORD'4. Сохраняем изменения и перезапускаем docker-контейнер с Gitlab. По моему опыту, процесс миграции редко занимает более 10 минут.
Если получаем 500 ошибку и нет времени на разбор, комментируем ранее запущенные изменения и перезапускаем контейнер.

Особенности работы с внешним PostgreSQL в Docker-контейнере
Часовой пояс
Стандартный для Gitlab часовой пояс — UTC. Менять его без необходимости не стоит. Особенно вот так:
volumes:
/etc/localtime : /etc/localtime : ro
Gitlab будет работать и даже не подаст вида, что что-то не так.
В WebGUI время корректное (подумаешь, у лейблов метка времени на 3 часа больше).
Авторизация через внешние сервисы работает.
В логах ошибок нет.
Но в базе вместо корректной временной зоны UTC...

...будут данные на 3 часа больше:

Такая на первый взгляд мелочь приводит к тому, что:
Пропадает возможность создавать gitlab runners через WebGUI — страница с токеном выдаст 404.
Сразу остановится создание партиций, и эффект от этого будет заметен через несколько месяцев.
Некоторые background jobs будут полностью забивать очереди sidekiq.
Версия при бэкапе
С 17 версии Gitlab придерживается политики ежегодного обновления версии PostgreSQL. Это значит, что 11-е версии в рамках года (напр. 17.11, 18.11) поддерживают работу с двумя версиями PostgreSQL. Gitlab 18.11 может работать с PostgreSQL 16 или 17, в то время как Gitlab 18.10 и ниже — только с PostgreSQL 16, а Gitlab 19.0 — только c PostgreSQL 17. По умолчанию Gitlab 18.11 использует PostgreSQL 16 в случае обновления с более ранних версий и PostgreSQL 17 для свежих инсталляций. По сути, вся поддержка заключается в версии pg_dump – 16 или 17. Если вы:
обновили Gitlab до 18.11,
мигрировали на PostgreSQL 17 в рамках подготовки к миграции на Gitlab 19,
не добавили в gitlab.rb опцию postgresql['version'] = 17,
...то резервная копия по CRON 0 2 * * * docker exec gitlab-sc gitlab-backup create CRON=1 будет создаваться без дампа базы. И со стороны это не будет видно. Изменится только размер, но при ежедневной ротации файлов резервных копий такое быстро выпадает из поля зрения.
Контейнер не дает полной изоляции
Работая с контейнерами, часто забываешь, что контейнер — это не виртуальная машина. И обновление операционной системы или пакетов хоста могут повредить данные внутри контейнера.
Redis
Redis — самый простой для выноса компонент Gitlab. Я не смог найти примеры, когда миграция на внешний Redis повышает производительность Gitlab, хотя здесь Redis:
хранит список задач (job) для очереди фоновых задач (Sidekiq),
кеширует данные,
управляет пользовательскими сессиями.
Подготовка окружения для внешнего Redis
Внешний Redis — обязательный шаг кластеризации и нужно пройти его до конца.
1. На хосте redis-host создаем /docker/docker-compose/docker-compose.yml с содержанием:
services:
redis:
image: company-local-docker-images/redis:7.4.9
container_name: gitlab-redis
logging:
options:
max-size: "1g"
max-file: "5"
volumes:
- "/docker/redis/data:/data"
command: redis-server --bind 0.0.0.0 --requirepass REDIS_PASS --appendonly yes --appendfsync everysec
ports:
- "6379:6379"
healthcheck:
test: ["CMD", "redis-cli", "-a", "REDIS_PASS", "ping"]
interval: 30s
timeout: 10s
retries: 5
restart: unless-stopped
deploy:
resources:
limits:
cpus: '2'
memory: 5G
networks:
- gitlabnet
redis-exporter:
image: company-local-docker-images/oliver006/redis_exporter:v1.88.0
container_name: redis_exporter
environment:
REDIS_ADDR: "redis://redis:6379"
REDIS_PASSWORD: " REDIS_PASS"
ports:
- "9121:9121"
restart: unless-stopped
depends_on:
- redis
networks:
- gitlabnet
networks:
gitlabnet:
driver: bridge2. Запускаем контейнер docker-compose:
-f /docker/docker-compose/docker-compose.yml up -d
Стартует он почти моментально, проверяем лог на наличие ошибок, подключаемся к Redis c помощью redis-cli.
3. Переходим на хост с single-node-gitlab. В конфигурационном файле /docker/gitlab/config/gitlab.rb указываем согласно инструкции:
### GitLab Redis settings
redis['enable'] = false
gitlab_rails['redis_host'] = "gitlab-redis-host"
gitlab_rails['redis_port'] = 6379
gitlab_rails['redis_password'] = “REDIS_PASS”Миграция на внешний Redis
Планируем прерывание в обслуживании (около минуты). На хосте с single-node-gitlab применяем новые настройки:
docker exec -t single-node-gitlab gitlab-ctl reconfigureЕсли в течение пары минут ошибка 500 не ушла и Gitlab не заработал штатно, нужно откатить изменения и читать логи.
Важно: проблемы с Redis и PostgreSQL в Gitlab проявляются одинаково — ошибкой 500. Для Postgres чаще всего нужно сразу смотреть лог, для Redis — упавшие задачи в очереди sidekiq.
Объектное хранилище
Объектное хранилище — неожиданно самая неоднозначная часть. Рассмотрим миграцию на внешнее объектное хранилище на примере MiniO, не забывая два важных фактора. Во-первых, фактически MiniO мертв. Во-вторых, почти то же самое можно получить, пошарив между rails-приложениями Gitlab и очередями sidekiq папку /var/opt/gitlab/gitlab-rails/shared из Docker-контейнера single-node-gitlab. В нашем примере на хосте она располагается по пути /docker/gitlab/data/gitlab-rails/shared/. Настройку я опишу ниже.
Для больших инсталляций Gitlab объектное хранилище предпочтительнее расшаренной папки, так как оно заберет на себя часть нагрузки, связанной с объектами LFS, логами CI и артефактами сборок.
Подготовка окружения для внешнего объектного хранилища
На хосте gitlab-minio создаем /docker/docker-compose/docker-compose.yml с содержанием:
services:
minio:
image: company-local-docker-images/minio:RELEASE.2025-07-18T21-56-31Z-cpuv1
container_name: gitlab-mini0
command: server /data --console-address ":9001"
environment:
- MINIO_ROOT_USER=S3User
- MINIO_ROOT_PASSWORD=S3Password
logging:
options:
max-size: "10g"
max-file: "3"
volumes:
- "/docker/minio/data:/data"
restart: unless-stopped
deploy:
resources:
limits:
cpus: '2'
memory: 7G
networks:
- gitlabnet
nginx-minio:
image: company-local-docker-images/nginx:1.26.0-perl
container_name: minio-nginx
ports:
- "443:443"
- "9009:9009"
volumes:
- "/docker/nginx/etc/conf.d:/etc/nginx/conf.d"
- "/opt/ssl:/opt/ssl"
- "/docker/nginx/log:/var/log/nginx"
restart: unless-stopped
networks:
- gitlabnet
networks:
gitlabnet:
driver: bridgeGitlab работает с Mini0 только при наличии TSL шифрования. Запросы по http будут игнорироваться. Оборачиваем доступ в nginx, /docker/nginx/etc/conf.d/minio.conf:
server {
listen 80;
server_name gitlab-mini0-host;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl;
listen [::]:443;
server_name gitlab-mini0-host;
# Allow special characters in headers
ignore_invalid_headers off;
# Allow any size file to be uploaded.
# Set to a value such as 1000m; to restrict file size to a specific value
client_max_body_size 0;
# Disable buffering
proxy_buffering off;
proxy_request_buffering off;
ssl_certificate /opt/ssl/my-cert.cer;
ssl_certificate_key /opt/ssl/my-cert.key;
ssl_session_cache builtin:1000 shared:SSL:10m;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers HIGH:!aNULL:!eNULL:!EXPORT:!CAMELLIA:!DES:!MD5:!PSK:!RC4;
ssl_prefer_server_ciphers on;
access_log /var/log/nginx/gitlab-mini0.https.access.log;
error_log /var/log/nginx/gitlab-mini0.https.error.log;
location / {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-NginX-Proxy true;
real_ip_header X-Real-IP;
proxy_connect_timeout 300;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
chunked_transfer_encoding off;
proxy_pass http://minio:9001/;
}
}
server {
listen 9009 ssl;
listen [::]:9009;
server_name gitlab-mini0-host;
# Allow special characters in headers
ignore_invalid_headers off;
# Allow any size file to be uploaded.
# Set to a value such as 1000m; to restrict file size to a specific value
client_max_body_size 0;
# Disable buffering
proxy_buffering off;
proxy_request_buffering off;
ssl_certificate /opt/ssl/my-cert.cer;
ssl_certificate_key /opt/ssl/my-cert.key;
ssl_session_cache builtin:1000 shared:SSL:10m;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers HIGH:!aNULL:!eNULL:!EXPORT:!CAMELLIA:!DES:!MD5:!PSK:!RC4;
ssl_prefer_server_ciphers on;
access_log /var/log/nginx/gitlab-mini0.https.access.log;
error_log /var/log/nginx/gitlab-mini0.https.error.log;
location / {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-NginX-Proxy true;
real_ip_header X-Real-IP;
proxy_connect_timeout 300;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
chunked_transfer_encoding off;
proxy_pass http://minio:9000/;
}
}Запускаем docker-compose:
-f /docker/docker-compose/docker-compose.yml up -d
Если все хорошо, видим страницу входа:

Вручную создаем бакеты согласно списку:

Для проверки можно что-то загрузить, но это непринципиально.
Переходим к настройке Gitlab. Для этого на хосте с single-node-gitlab редактируем конфигурационный файл Gitlab gitlab.rb согласно инструкции. В рассматриваемом случае файл будет располагаться по пути /docker/gitlab/config/gitlab.rb.
### Consolidated (simplified) object storage configuration
gitlab_rails['object_store']['enabled'] = true
gitlab_rails['object_store']['proxy_download'] = false
gitlab_rails['object_store']['connection'] = {
'provider' => 'AWS',
'region' => 'miran-gitlab-s3', # Любой регион (MinIO игнорирует)
'endpoint' => 'https://gitlab-mini0:9009',
'aws_access_key_id' => 'S3User', # MinIO root user
'aws_secret_access_key' => 'S3Password', # MinIO root password
'path_style' => true # Обязательно для MinIO!
}
gitlab_rails['object_store']['objects']['artifacts']['bucket'] = 'gl-artifacts'
gitlab_rails['object_store']['objects']['external_diffs']['bucket'] = 'gl-external-diffs'
gitlab_rails['object_store']['objects']['lfs']['bucket'] = 'gl-lfs'
gitlab_rails['object_store']['objects']['uploads']['bucket'] = 'gl-uploads'
gitlab_rails['object_store']['objects']['packages']['bucket'] = 'gl-packages'
gitlab_rails['object_store']['objects']['dependency_proxy']['bucket'] = 'gl-dependency-proxy'
gitlab_rails['object_store']['objects']['terraform_state']['bucket'] = 'gl-terraform-state'
gitlab_rails['object_store']['objects']['pages']['bucket'] = 'gl-pages'
gitlab_rails['object_store']['objects']['ci_secure_files']['bucket'] = 'gl-ci-secure-files'Применяем новые настройки:
docker exec -t single-node-gitlab gitlab-ctl reconfigureБудет короткое прерывание в обслуживании, около минуты на обновление конфигурации Gitlab. Теперь миграция:
docker exec -t single-node-gitlab gitlab-rake gitlab:uploads:migrate:all
docker exec -t single-node-gitlab gitlab-rake gitlab:artifacts:migrate
docker exec -t single-node-gitlab gitlab-rake gitlab:lfs:migrate
docker exec -t single-node-gitlab gitlab-rake gitlab:packages:migrateОсобенности работы с внешним miniO в Docker-контейнере
После подключения объектное хранилище работает сразу. Все новые файлы создаются в нем сразу.
Если связь с хранилищем будет потеряна, Gitlab будет искать файлы на локальном диске в /var/opt/gitlab/gitlab-rails/shared без лишних запросов или разрешений.
После миграции LFS объектов на объектное хранилище обязательно откройте всем заинтересованным доступ к https://gitlab-mini0:9009, так как Gitlab будет раздавать git lfs именно с него.
При использовании самоподписанных сертификатов обязательно для single-node-gitlab добавьте их на хосте в /docker/gitlab/config/trusted-certs, иначе команды миграции выполняться не будут. Хоть новые объекты и будут создаваться и раздаваться без проблем.
Достаточно часто я сталкивался с тем, что не все объекты переезжают. Проще перенести руками либо пошарить /var/opt/gitlab/gitlab-rails/shared между rails (подробности будут далее).
Логи Gitlab CI /var/opt/gitlab/gitlab-ci/builds в объектное хранилище не переезжают. Я шарю эту папку и чищу по cron:
0 0 * * * find /mnt/gitlab-ci-builds/ -depth -mtime +14 -exec rm -rf {} +Gitaly
Gitaly, сервис разработки команды Gitlab — микросервис в системе GitLab, управляющий хранением и обработкой всех Git-репозиториев. Он принимает запросы от веб-интерфейса и других частей платформы через протокол gRPC и выполняет операции с кодом: чтение, запись, поиск.
Миграция на внешний Gitaly — рекомендую сразу на отдельную дисковую подсистему — из Gitlab может быть достаточной для нормализации производительности в случаях активной единовременной нагрузки по выкачиванию репозиториев. Например, при массированном запуске тестов в Jenkins/Gitlab CI по расписанию, что характеризуется ошибками вида GitLab error: Gitaly is unreachable в логе git clone.
Gitaly для версий Gitlab Enterprise и Community Edition выглядят и ведут себя одинаково. Для Gitaly стоит отдать предпочтение Docker CE образу, так как он занимает меньше места.
Подготовка окружения для внешнего Gitaly
Приступаем к настройке внешнего Gitaly. Конфигурации Gitlab Rails, Sidekiq, Gitaly очень похожи, и будет много дублирования для тех, кто не читал другие пункты.
На хосте gitlab-gitaly-host создаем /docker/docker-compose/docker-compose.yml с содержанием:
services:
gitaly:
image: company-local-docker-images/gitlab/gitlab-ce:18.11.11-ce.0
container_name: gitlab-gitaly
logging:
options:
max-size: "1g"
max-file: "5"
volumes:
- "/docker/gitlab/config:/etc/gitlab"
- "/docker/gitlab/data:/var/opt/gitlab"
- "/docker/gitlab/logs:/var/log/gitlab"
ports:
- "8075:8075"
- "9236:9236"
restart: unless-stopped
deploy:
resources:
limits:
cpus: '4'
memory: 7G
networks:
- gitlabnet
networks:
gitlabnet:
driver: bridgeЗапускаем docker-compose -f /docker/docker-compose/docker-compose.yml up -d. Ждем, когда создадутся все файлы конфигураций, и останавливаем контейнер docker-compose:
-f /docker/docker-compose/docker-compose.yml down
Запрещаем обновление базы Gitlab при обновлении версии:
touch /docker/gitlab/config/skip-auto-reconfigureКопируем файл с секретам /docker/gitlab/config/gitlab-secrets.json с хоста gitlab.example.com на gitlab-gitaly-host.
Переключаем Gitlab в режим Gitaly через редактирование /docker/gitlab/config/gitlab.rb согласно инструкции:
gitlab_rails['internal_api_url'] = 'https://gitlab.example.com'
# Disable all other services on the Gitaly node
postgresql['enable'] = false
redis['enable'] = false
nginx['enable'] = false
puma['enable'] = false
sidekiq['enable'] = false
gitlab_workhorse['enable'] = false
prometheus_monitoring['enable'] = false
gitlab_kas['enable'] = false
# Enable only the Gitaly service
gitaly['enable'] = true
# Enable Prometheus
prometheus['enable'] = true
# Disable database migrations to prevent database connections during 'gitlab-ctl reconfigure'
gitlab_rails['auto_migrate'] = false
gitaly['configuration'] = {
# Configure Gitaly to listen on network
listen_addr: '0.0.0.0:8075',
prometheus_listen_addr: '0.0.0.0:9236',
# Configure a strong auth_token
auth: {
token: 'GITALY_TOKEN',
},
# Configure the storage location for Git data
storage: [
# Replace with appropriate name for each Gitaly nodes.
{
name: 'gitaly',
path: '/var/opt/gitlab/git-data/repositories',
},
]
}Важно: при использовании самоподписанных сертификатов обязательно добавьте их на хосте gitaly-host в /docker/gitlab/config/trusted-certs. Иначе встроенная проверка будет отрабатывать корректно, но в WebGUI содержимого репозиториев не будет.
docker exec -it gitlab-gitaly /opt/gitlab/embedded/bin/gitaly check /var/opt/gitlab/gitaly/config.tomlПрописываем конфигурацию внешнего gitaly в gitlab.rb на хосте single-node-gitlab:
### Gitaly settings
gitaly['enable'] = false
gitlab_rails['repositories_storages'] = {
"gitaly-external" => {"gitaly_address" => "tcp://gitaly-host:8075","gitaly_token" => 'GITALY_TOKEN'}
}Так как мы выносим gitaly на внешний хост, то гибридную конфигурацию не рассматриваем. Подробнее можно самостоятельно прочитать в документации вендора.
Важно: при использовании самоподписанных сертификатов обязательно добавьте их на хосте gitaly-host в /docker/gitlab/config/trusted-certs:
docker exec -it gitlab-gitaly /opt/gitlab/embedded/bin/gitaly check /var/opt/gitlab/gitaly/config.tomlМиграция на внешний Gitaly
Вся миграция состоит из двух шагов:
Перенести данные git репозиториев на новый gitaly.
Переключить Gitlаb на внешний gitaly.
В нашем случае все данные git-репозиториев на хосте с single-node-gitlab лежат в папке /docker/gitlab/data/git-data/repositories. Нужно их перенести в ту же папку, но на внешний gitaly. Сделать это можно следующими способами:
Через создание и восстановление бэкапа. Все аналогично миграции PostgreSQL, не буду останавливаться, вариант не подходит.
Попроектная миграция (см. официальную документацию) — идеально, если проектов несколько, не требует длительного даунтайма. Но ручная миграция нескольких тысяч проектов может быть затруднительна. Этот вариант тоже не буду рассматривать.
Rsync — то, что нужно для миграции большого количества репозиториев с минимальным временем недоступности.
Миграция на внешний Gitaly через rsync
На «горячую» (single-node-gitlab работает, gitlab-gitaly выключен) через rsync мигрируем основную массу данных:
rsync -avhz --delete --exclude='.gitaly-metadata' gitlab.example.com:/docker/gitlab/data/git-data/repositories/ gitlab-gitaly-host:/docker/gitlab/data/git-data/repositories/Важно: Не забудьте удалить /docker/gitlab/data/git-data/repositories/.gitaly-metadata на gitlab-gitaly, если он существует по какой-то причине.
Планируем недоступность сервиса и останавливаем single-node-gitlab. Напомню, что изменения в его gitlab.rb внесены, но не применены. Затем финально синхронизируем данные с помощью rsync:
chown -R git:git /docker/gitlab/data/git-data/repositories/Важно: на внешнем gitlab-gitaly-host нужно изменить права на репозитории. Gitlab в rpm создаёт пользователя git:git, Gitlab в Docker обычно 998:998.
Наконец, запускаем сервис single-node-gitlab на хосте gitlab.example.com. Проверить успешность переключения проще всего через WebGUI: если содержание любого репозитория отображается, изменения вносятся, то все работает. Если что-то пошло не так, на хосте внешнего gitaly проверяем:
docker exec -it gitlab-gitaly /opt/gitlab/embedded/bin/gitaly check /var/opt/gitlab/gitaly/config.tomlОткатываемся через изменение конфигурации.
Sidekiq
Sidekiq — обработчик очередей. Все задачи по срабатыванию веб-хуков, отправки писем, партиционированию таблиц и прочего проходят через него. Масштабирование Sidekiq приносит результат при большой Gitlab CI нагрузке — например, одновременный запуск нескольких тысяч Gitlab CI Pipelines. Прерывание в обслуживании не требуется. Новые обработчики очередей добавляются и удаляются на лету.
Важно: для масштабирования Sidekiq обязательно выполнение масштабирования всех вышеописанных элементов.
Подготовка окружения для внешнего Sidekiq
На хосте gitlab-gitaly-host создаём /docker/docker-compose/docker-compose.yml с содержанием:
services:
sidkiq:
image: company-local-docker-images/gitlab-ce:18.11.11-ce.0
container_name: gitlab-sidekiq
hostname: gitlab-sidekiq-host
logging:
options:
max-size: "1g"
max-file: "5"
volumes:
- "/docker/gitlab/config:/etc/gitlab"
- "/docker/gitlab/data:/var/opt/gitlab"
- "/docker/gitlab/logs:/var/log/gitlab"
ports:
- "8082:8082"
restart: unless-stopped
deploy:
resources:
limits:
cpus: '4'
memory: 15G
networks:
- gitlabnet
networks:
gitlabnet:
driver: bridge В image обязательно используйте CE или EE в зависимости от версии вашего Gitlab, у меня здесь и далее — СЕ. Hostname — необязательный, но удобный параметр, отвечает за отображение читаемого имени в WebGUI Gitlab
Далее запускаем docker-compose:
-f /docker/docker-compose/docker-compose.yml up -d
Ждем, когда создадутся все файлы конфигураций, и останавливаем контейнер docker-compose:
-f /docker/docker-compose/docker-compose.yml down
Запрещаем обновление базы Gitlab при обновлении версии:
touch /docker/gitlab/config/skip-auto-reconfigureКопируем файл с секретом /docker/gitlab/config/gitlab-secrets.json с хоста gitlab.example.com на gitlab-sidekiq-host. Переключаем Gitlab в режим Sidekiq через редактирование /docker/gitlab/config/gitlab.rb согласно документации:
## GitLab configuration settings
external_url 'https://gitlab.example.com'
roles ['sidekiq_role']
### Prevent database migrations from running on upgrade automatically
gitlab_rails['auto_migrate'] = false
### Object storage configuration
gitlab_rails['object_store']['enabled'] = true
gitlab_rails['object_store']['proxy_download'] = false
gitlab_rails['object_store']['connection'] = {
'provider' => 'AWS',
'region' => 'gitlab-s3',
'endpoint' => 'https://gitlab-mini0:9009',
'aws_access_key_id' => 'S3User',
'aws_secret_access_key' => 'S3Password',
'path_style' => true
}
gitlab_rails['object_store']['objects']['artifacts']['bucket'] = 'gl-artifacts'
gitlab_rails['object_store']['objects']['external_diffs']['bucket'] = 'gl-external-diffs'
gitlab_rails['object_store']['objects']['lfs']['bucket'] = 'gl-lfs'
gitlab_rails['object_store']['objects']['uploads']['bucket'] = 'gl-uploads'
gitlab_rails['object_store']['objects']['packages']['bucket'] = 'gl-packages'
gitlab_rails['object_store']['objects']['dependency_proxy']['bucket'] = 'gl-dependency-proxy'
gitlab_rails['object_store']['objects']['terraform_state']['bucket'] = 'gl-terraform-state'
gitlab_rails['object_store']['objects']['pages']['bucket'] = 'gl-pages'
gitlab_rails['object_store']['objects']['ci_secure_files']['bucket'] = 'gl-ci-secure-files'
### Usage Statistics
gitlab_rails['usage_ping_enabled'] = false
### Gitaly settings
gitaly['enable'] = false
gitlab_rails['repositories_storages'] = {
"default" => {
"gitaly_address" => 'tcp://gitlab-gitaly-host:8075',
"gitaly_token" => 'GITALY_TOKEN'
}
}
### GitLab database settings
postgresql['enable'] = false
gitlab_rails['db_adapter'] = "postgresql"
gitlab_rails['db_encoding'] = "unicode"
gitlab_rails['db_database'] = "gitlabhq_production"
gitlab_rails['db_username'] = "gitlab"
gitlab_rails['db_password'] = "DB_PASSWORD"
gitlab_rails['db_host'] = "gitlab-pg-host"
gitlab_rails['db_port'] = 5432
### GitLab Redis settings
redis['enable'] = false
gitlab_rails['redis_port'] = '6379'
gitlab_rails['redis_host'] = 'gitlab-redis-host'
gitlab_rails['redis_password'] = 'REDIS_PASS'
### GitLab Sidekiq
sidekiq['listen_address'] = "0.0.0.0"
## Set number of Sidekiq queue processes to the same number as available CPUs
sidekiq['queue_groups'] = ['*'] * 4
##! Specifies where Prometheus metrics endpoints should be made available for Sidekiq processes.
sidekiq['metrics_enabled'] = true
sidekiq['exporter_tls_enabled'] = false
sidekiq['listen_address'] = '0.0.0.0'
sidekiq['listen_port'] = 8082Важно: все дополнительные конфигурации (LDAP, KAS, Page) обязательно должны быть продублированы в настройке Sidekiq. Для настройки sidekiq['queue_groups'] = ['*'] * 4 обязательно указывайте число доступных CPU. В моем примере их четыре.
Теперь запускаем docker-compose:
-f /docker/docker-compose/docker-compose.yml up -d
Через 5 минут проверяем в WebGUI количество процессов. По умолчанию должно быть более 1:

Rails
Rails — основа системы, монолитное приложение, написанное на Ruby on Rails. В подавляющем большинстве случаев оно тождественно Gitlab. Масштабирование Rails приносит результат при большом количестве пользователей и обращений к Gitlab в целом. Имейте в виду: согласно архитектуре, бесконечно наращивать Gitlab Rails нельзя, рекомендуемый максимум на ноду — 32 vCPU.
Прерывание в обслуживании не потребуется. Добавляем новые ноды, без перехода на HAProxy изменения не вступят в силу.
Важно: для масштабирования Gitlab Rails обязательно выполнение масштабирования всех вышеописанных элементов.
Особенности масштабирования
При кластеризации Gitlab можно добавлять сколько угодно инстансов Gitlab Rails кроме одного. Вот ключевые особенности этого единственного инстанса. Назовем его ведущим, хотя это не совсем корректно:
прямое подключение к СУБД, работа через менеджер соединений (например, pgbouncer) не допускается;
используется для обновления Gitlab;
используется для создания и восстановления резервных копий Gitlab.
В нашем случае Docker-контейнер с ведущим Gitlab Rails мы разместим на машине с HAProxy и отключим на него маршрутизацию пользовательского трафика.
Подготовка окружения для Rails
Не забудьте заранее переопределить 22 порт для SSH. На каждом хосте gitlab-rails-(01/02)-host создаем /docker/docker-compose/docker-compose.yml с содержанием:
services:
gitlab-rails-01:
image: company-local-docker-images/gitlab-ce:18.11.11-ce.0
container_name: gitlab-rails-01
logging:
options:
max-size: "1g"
max-file: "5"
volumes:
- "/docker/gitlab/config:/etc/gitlab"
- "/docker/gitlab/data:/var/opt/gitlab"
- "/docker/gitlab/logs:/var/log/gitlab"
- "/network-mnt/gitlab-ci-builds:/var/opt/gitlab/gitlab-ci/builds"
ports:
- "22:22"
- "80:80"
- "9100:9100" #node exporter port
restart: unless-stopped
deploy:
resources:
limits:
cpus: '8'
memory: 8G
networks:
- gitlabnet
networks:
gitlabnet:
driver: bridge/network-mnt/gitlab-ci-builds — сетевая папка с логами билдов, чистить будем по CRON на ведущем Gitlab Rails.
Запускаем docker-compose:
-f /docker/docker-compose/docker-compose.yml up -d
Ждем, когда создадутся все файлы конфигураций, и останавливаем контейнер docker-compose:
-f /docker/docker-compose/docker-compose.yml down
Запрещаем обновление базы Gitlab при обновлении версии:
touch /docker/gitlab/config/skip-auto-reconfigureКопируем файл с секретом /docker/gitlab/config/gitlab-secrets.json с хоста gitlab.example.com на gitlab-rails-(01/02)-host. Копируем файлы ssh-ключей — ssh_host_ecdsa_key, ssh_host_ecdsa_key.pub, ssh_host_ed25519_key, ssh_host_ed25519_key.pub, ssh_host_rsa_key, ssh_host_rsa_key.pub — из /docker/gitlab/config с хоста gitlab.example.com на gitlab-rails-(01/02)-host. При использовании самоподписанных сертификатов, обязательно добавьте их на хосте gitaly-host в /docker/gitlab/config/trusted-certs.
Переключаем Gitlab в режим Application через редактирование /docker/gitlab/config/gitlab.rb согласно документации:
## GitLab configuration settings
external_url 'https://gitlab.example.com'
roles ['application_role']
### Prevent database migrations from running on upgrade automatically
gitlab_rails['auto_migrate'] = false
### Object storage configuration
gitlab_rails['object_store']['enabled'] = true
gitlab_rails['object_store']['proxy_download'] = false
gitlab_rails['object_store']['connection'] = {
'provider' => 'AWS',
'region' => 'gitlab-s3',
'endpoint' => 'https://gitlab-mini0:9009',
'aws_access_key_id' => 'S3User',
'aws_secret_access_key' => 'S3Password',
'path_style' => true
}
gitlab_rails['object_store']['objects']['artifacts']['bucket'] = 'gl-artifacts'
gitlab_rails['object_store']['objects']['external_diffs']['bucket'] = 'gl-external-diffs'
gitlab_rails['object_store']['objects']['lfs']['bucket'] = 'gl-lfs'
gitlab_rails['object_store']['objects']['uploads']['bucket'] = 'gl-uploads'
gitlab_rails['object_store']['objects']['packages']['bucket'] = 'gl-packages'
gitlab_rails['object_store']['objects']['dependency_proxy']['bucket'] = 'gl-dependency-proxy'
gitlab_rails['object_store']['objects']['terraform_state']['bucket'] = 'gl-terraform-state'
gitlab_rails['object_store']['objects']['pages']['bucket'] = 'gl-pages'
gitlab_rails['object_store']['objects']['ci_secure_files']['bucket'] = 'gl-ci-secure-files'
### Usage Statistics
gitlab_rails['usage_ping_enabled'] = false
### Gitaly settings
gitaly['enable'] = false
gitlab_rails['repositories_storages'] = {
"default" => {
"gitaly_address" => "tcp://gitlab-gitaly-host:8075",
"gitaly_token" => 'GITALY_TOKEN'
}
}
### GitLab database settings
postgresql['enable'] = false
gitlab_rails['db_adapter'] = "postgresql"
gitlab_rails['db_encoding'] = "unicode"
gitlab_rails['db_database'] = "gitlabhq_production"
gitlab_rails['db_username'] = "gitlab"
gitlab_rails['db_password'] = "DB_PASSWORD"
gitlab_rails['db_host'] = "gitlab-pg-host"
gitlab_rails['db_port'] = 5432
### GitLab Redis settings
redis['enable'] = false
gitlab_rails['redis_port'] = '6379'
gitlab_rails['redis_host'] = 'gitlab-redis-host'
gitlab_rails['redis_password'] = 'REDIS_PASS'
### GitLab Sidekiq
sidekiq['enable'] = false
### GitLab NGINX
nginx['redirect_http_to_https'] = false
nginx['listen_port'] = 80
nginx['listen_https'] = false
### GitLab Logging
logging['svlogd_size'] = 200 * 1024 * 1024 # rotate after 200 MB of log data
logging['svlogd_num'] = 5 # keep 30 rotated log files
logging['svlogd_timeout'] = 24 * 60 * 60 # rotate after 24 hours
logging['svlogd_filter'] = "gzip" # compress logs with gzip
logging['logrotate_frequency'] = "daily" # rotate logs daily
logging['logrotate_size'] = 200 * 1024 * 1024 # rotate after 200 MB of log data
logging['logrotate_rotate'] = 5 # keep 30 rotated logs
logging['logrotate_compress'] = "compress" # see 'man logrotate'
### Prometheus
node_exporter['enable'] = true
node_exporter['listen_address'] = '0.0.0.0:9100'Запускаем docker-compose. Через 5 минут проверяем логи: все уже должно работать без ошибок.
HAProxy
HAProxy — балансировщик нагрузки. Мы расширяем уже существующий Gitlab в Docker-контейнере, и на одном хосте с HAProxy будет работать ведущий Gitlab Rails, выведенный из-под пользовательской нагрузки.
Подготовка окружения для HAProxy
Предварительно проверяем, что 22 порт для SSH переопределен. Затем создаем /docker/docker-compose/docker-compose.yml с содержанием:
services:
gitlab-haproxy:
image: company-local-docker-images/haproxy:3.2.22
container_name: gitlab-haproxy
ports:
- "22:22"
- "80:80"
- "443:443"
- "8404:8404"
volumes:
- "/docker/haproxy:/usr/local/etc/haproxy"
- "/docker/haproxy/stats:/var/lib/haproxy/stats"
- "/opt/ssl:/opt/ssl"
restart: unless-stopped
networks:
- gitlabnet
gitlab-rails-main:
image: company-local-docker-images/gitlab-ce:18.11.11-ce.0
container_name: gitlab-rails-01
logging:
options:
max-size: "1g"
max-file: "5"
volumes:
- "/docker/gitlab/config:/etc/gitlab"
- "/docker/gitlab/data:/var/opt/gitlab"
- "/docker/gitlab/logs:/var/log/gitlab"
- "/network-mnt/gitlab-ci-builds:/var/opt/gitlab/gitlab-ci/builds"
restart: unless-stopped
deploy:
resources:
limits:
cpus: '8'
memory: 8G
networks:
- gitlabnet
networks:
gitlabnet:
driver: bridgeТеперь — файл для экспортера HAProxy, автоматически он не создается:
touch /docker/haproxy/stats/stats.stateДалее — конфигурационный файл HAProxy, /docker/haproxy/haproxy.cfg/haproxy.cfg:
# Configuration file for haproxy
#---------------------------------------------------------------------
# Global settings
#---------------------------------------------------------------------
global
expose-experimental-directives
stats-file /var/lib/haproxy/stats/stats.state
log 127.0.0.1:514 local0
daemon
#---------------------------------------------------------------------
# Common defaults that all the 'listen' and 'backend' sections will
# use if not designated in their block
#---------------------------------------------------------------------
defaults
log global
mode http
option httplog
option dontlognull
option http-server-close
option redispatch
timeout http-request 10s
timeout queue 20s
timeout connect 5s
timeout client 20m
timeout server 20m
timeout http-keep-alive 10s
timeout check 10s
default-server inter 10s fall 3 rise 2
balance leastconn
#---------------------------------------------------------------------
# statistic-http
#---------------------------------------------------------------------
frontend stats
mode http
bind *:8404 ssl crt /opt/ssl/my.company.ssl.pem
http-request use-service prometheus-exporter if { path /metrics }
stats enable
stats uri /stats
stats show-modules
stats refresh 10s
#---------------------------------------------------------------------
# rails-http
#---------------------------------------------------------------------
frontend rails-http
mode http
bind *:80
bind *:443 ssl crt /opt/ssl/my.company.ssl.pem
# 1. Process HTTP Request manipulations first
http-request redirect scheme https code 301 unless { ssl_fc }
http-response set-header X-Backend-Server %s
option forwardfor except 127.0.0.0/8
# 2. Assign backends last
use_backend rails-http-backend
option httplog
#---------------------------------------------------------------------
# rails-http backend
#---------------------------------------------------------------------
backend rails-http-backend
mode http
server gitalb-rails-01 gitalb-rails-01:80 check
server gitalb-rails-02 gitalb-rails-02:80 check
#---------------------------------------------------------------------
# rails-ssh
#---------------------------------------------------------------------
frontend rails-ssh
mode tcp
bind *:22
use_backend rails-ssh-backend
option tcplog
#---------------------------------------------------------------------
# rails-ssh backend
#---------------------------------------------------------------------
backend rails-ssh-backend
mode tcp
server gitalb-rails-01 gitalb-rails-01:22 check
server gitalb-rails-02 gitalb-rails-01:22 checkМиграция на HAProxy
1. Планируем время недоступности. Сам перезапуск займет не более минуты, все текущие соединения не прервутся.
2. Останавливаем докер:
stop single-node-gitlab
3. Запускаем docker-compose:
-f /docker/docker-compose/docker-compose.yml up -d
4. Не забываем проверить все свои CRON-задания.
Заключение
Спасибо всем дочитавшим: не ожидал, что материал по масштабированию и кластеризации Gitlab через Docker-контейнеры окажется таким обширным. Примеры конфигураций заняли много места, но официальная документация Gitlab аккуратно обходит некоторые вопросы, поэтому я разместил примеры в статье целиком. Старался составлять docker-compose.yml так, чтобы их при желании можно было объединить в один. Надеюсь, теперь вы сможете не переезжать сразу на следующую архитектуру, а самостоятельно решить, что конкретно нужно масштабировать в вашей инсталляции. Возможно, этим вы сэкономите время и деньги на развитие и сопровождение инфраструктуры.
Жаль, что совсем не осталось места на наш опыт по работе с кластером Gitlab в Docker. Если будет интересно, пишите, обязательно постараюсь рассказать.
Если вам интересна работа в DevOps, обратите внимание на наши вакансии:
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.