Привет! Меня зовут Илья, я занимаюсь автоматизацией разработки аппаратного обеспечения в 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: bridge
Shm_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_production
3. Проверяем, что восстановление прошло без ошибок и к базе можно подключиться удаленно. Если все хорошо, редактируем конфигурационный файл 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: bridge
2. Запускаем контейнер 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: bridge
Gitlab работает с 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
Планируем время недоступности. Сам перезапуск займет не более минуты, все текущие соединения не прервутся.
-
Останавливаем докер:
stop single-node-gitlab -
Запускаем docker‑compose:
-f /docker/docker-compose/docker-compose.yml up -d Не забываем проверить все свои CRON‑задания.
Заключение
Спасибо всем дочитавшим: не ожидал, что материал по масштабированию и кластеризации Gitlab через Docker‑контейнеры окажется таким обширным. Примеры конфигураций заняли много места, но официальная документация Gitlab аккуратно обходит некоторые вопросы, поэтому я разместил примеры в статье целиком. Старался составлять docker‑compose.yml так, чтобы их при желании можно было объединить в один. Надеюсь, теперь вы сможете не переезжать сразу на следующую архитектуру, а самостоятельно решить, что конкретно нужно масштабировать в вашей инсталляции. Возможно, этим вы сэкономите время и деньги на развитие и сопровождение инфраструктуры.
Жаль, что совсем не осталось места на наш опыт по работе с кластером Gitlab в Docker. Если будет интересно, пишите, обязательно постараюсь рассказать.
Если вам интересна работа в DevOps, обратите внимание на наши вакансии: