GitLab CE в Docker Compose: конфиг с SMTP, Runner и нормальными лимитами
Привет! В этой статье я поделюсь конфигом для развертывания GitLab CE, который я собрал для своей команды. Когда я начинал, официальные гайды казались слишком общими: то почта не заводится, то Runner не видит сервер, то стандартные лимиты Nginx не дают запушить Docker-образ.
Я собрал "золотую середину": актуальная версия (18.10.0), настроенный SMTP для Gmail, готовый Runner и адекватные лимиты для артефактов. Этот конфиг мы успешно используем для хранения кода и автотестов.
Если вы хотите сразу перейти к делу — вот готовый docker-compose.yml, который мы используем в команде. Он уже содержит настройки SMTP, лимиты Nginx и подключённый Runner. Переменные окружения берутся из .env.
services:
gitlab:
image: gitlab/gitlab-ce:${GITLAB_VERSION}
hostname: "gitlab"
restart: unless-stopped
environment:
GITLAB_OMNIBUS_CONFIG: |
external_url '${EXTERNAL_URL}'
nginx['listen_port'] = 80
nginx['listen_https'] = false
nginx['client_max_body_size'] = '1024m'
gitlab_rails['gitlab_shell_ssh_port'] = 30022
gitlab_rails['initial_root_password'] = '${GITLAB_ROOT_PASSWORD}'
gitlab_rails['max_artifacts_size'] = 500
# === SMTP настройки для Gmail ===
gitlab_rails['smtp_enable'] = true
gitlab_rails['smtp_address'] = "smtp.gmail.com"
gitlab_rails['smtp_port'] = 587
gitlab_rails['smtp_user_name'] = "${SMTP_USER}"
gitlab_rails['smtp_password'] = "${SMTP_APP_PASSWORD}"
gitlab_rails['smtp_domain'] = "smtp.gmail.com"
gitlab_rails['smtp_authentication'] = "login"
gitlab_rails['smtp_enable_starttls_auto'] = true
gitlab_rails['smtp_tls'] = false
gitlab_rails['smtp_openssl_verify_mode'] = 'peer'
# === Настройки отправителя ===
gitlab_rails['gitlab_email_from'] = "${SMTP_USER}"
gitlab_rails['gitlab_email_reply_to'] = "noreply@gmail.com"
gitlab_rails['gitlab_email_display_name'] = "GitLab Notifications"
ports:
- "30080:80"
- "30022:22"
volumes:
- gitlab_data:/var/opt/gitlab
- gitlab_logs:/var/log/gitlab
- ./gitlab/backups:/var/opt/gitlab/backups
- ./gitlab/config:/etc/gitlab
networks:
- gitlab_net
gitlab-runner:
image: gitlab/gitlab-runner:${RUNNER_VERSION}
restart: unless-stopped
privileged: true
volumes:
- ./config:/etc/gitlab-runner
- ./data/runner/cache:/cache
- /var/run/docker.sock:/var/run/docker.sock
networks:
- gitlab_net
depends_on:
- gitlab
networks:
gitlab_net:
name: gitlab_net
driver: bridge
volumes:
gitlab_config:
gitlab_data:
gitlab_logs:Сам .env:
GITLAB_VERSION=18.10.0-ce.0
RUNNER_VERSION=18.10.0
EXTERNAL_URL=http://gitlab.local
GITLAB_ROOT_PASSWORD=MyPassword
SMTP_USER=your-email@gmail.com
SMTP_APP_PASSWORD=abcd-efgh-ijkl-mnop # ← создаётся в настройках GoogleЕдинственное что вам стоить сделать после запуска docker compose up так это зарегистрировать gitlab-runner, для этого нужен токен проекта создаём новый проект (если ещё не создали), переходим в него → Settings → CI/CD → Runners. Рядом с кнопкой “Create Project Runner” нажимаем на три точки и копируем токен. Далее с помощью этого токена регистрируем gitlab-runner, командой:
docker run --rm -t --network gitlab_net -v %cd%\config:/etc/gitlab-runner gitlab/gitlab-runner:v18.10.0 register --non-interactive --url "http://gitlab" --clone-url "http://gitlab" --registration-token "ВАШ ТОКЕН" --executor "docker" --description "docker-runner" --docker-image "docker:20" --docker-privileged --docker-volumes "/var/run/docker.sock:/var/run/docker.sock" --docker-network-mode "gitlab_net"
Эту команду также можно изменить, если вам она по каким либо причинам не подходит.
⚠️ Примечание: в этом конфиге используется HTTP для простоты локального развёртывания. Для продакшена рекомендуется настроить HTTPS через reverse proxy, например с помощью Nginx или Traefik.
Одна из самых частых проблем при запуске - gitlab-runner не может подключиться к gitlab, чтобы решить ее я использую:
networks:
gitlab_net:
name: gitlab_net
driver: bridgeВ этом примере мы создаем сеть для обоих сервисов, когда мы так делаем то Docker выполняет две вещи:
Создает виртуальный мост(bridge) к которому подключены оба контейнера. Они могут видеть друг друга по IP - адресам внутри этой сети.
DNS-разрешение (Service Discovery): Docker автоматически запускает внутренний DNS-сервер. Благодаря этому контейнеры могут обращаться друг к другу по именам сервисов, указанным в
docker-compose.yml.
gitlab-runner может достучаться до GitLab по адресу
http://gitlab.GitLab может отправить запрос на раннер (если нужно), используя имя
gitlab-runner.
А без общей сети могут возникнуть следующие проблемы:
Потеря связи по имени: Раннер не сможет найти GitLab по адресу
http://gitlab.Сложная настройка: Придется использовать нестабильные внутренние IP-адреса контейнеров или внешний IP сервера.
Лишний трафик: Данные пойдут через внешнюю сеть хоста, что медленнее и менее безопасно.
Ошибки клонирования: Раннер не сможет забрать код из репозитория, так как для него адрес
http://gitlabбудет просто неизвестен.
Итого: общая сеть = отсутствие “танцев с бубном”.
Теперь перейдем к SMTP протоколу и почте.
Как можете увидеть, я использую порт 587 — это современный стандарт. Старый 25 порт уже практически не используется, да и сам Google рекомендует SMTP через 587.
Помимо почты и порта нам пригодится пароль приложения. Его можно создать только если у вас включена двухфакторная аутентификация.
Как его получить:
Заходим в аккаунт Google(gmail)
Переходим в “Управление аккаунтом”

В поиске вводим “Пароли приложений”


Создаём новый пароль и сохраняем его

Полученный пароль используем в .env как SMTP_APP_PASSWORD.
После этого можно проверить, что всё работает:
docker compose upи создав пользователя с почтой, на эту почту и должно прийти уведомление о регистрации пользователя.
⚠️ Примечание: Gmail отлично подходит для тестирования и небольших проектов, но для продакшена лучше использовать специализированные SMTP-сервисы (например, SendGrid или Mailgun), чтобы избежать лимитов и проблем с доставкой писем.
Итак, почта работает, Runner видит сервер. Осталась последняя «боль» из вступления — большие образы.
Если вы делаете docker push в локальный реестр GitLab и на 80% прогресса получаете unexpected EOF или 413 Request Entity Too Large, проблема почти всегда в настройках Nginx. По умолчанию он ограничивает размер тела запроса одним мегабайтом — это защита от перегрузки, но для пуша даже небольшого образа этого катастрофически мало.
Немного проясним, что такое «образ» и почему он может быть большим. Docker-образ — это упакованное приложение со всеми зависимостями: код, библиотеки, системные утилиты. Простой веб-сервис на Node.js или Python обычно весит 200–500 МБ, приложение с компиляцией на Java или .NET — от 500 МБ до 1.5 ГБ, а проекты с данными или ML-моделями могут достигать 1–3 ГБ.
Важно понимать: в контексте GitLab «пуш образа» может означать два сценария. Первый — вы собираете образ локально и пушите его во встроенный реестр GitLab (registry.gitlab.local:30080/...). Второй — вы пушите только исходный код и Dockerfile, а сборку выполняет GitLab Runner внутри пайплайна, и он же отправляет результат обратно в реестр. В обоих случаях финальная отправка образа идёт через Nginx. И если файл весит больше 1 МБ (а он почти всегда весит больше), стандартный лимит его обрежет.
Решение в нашем конфиге простое: nginx['client_max_body_size'] = '1024m' — это разрешает запросы до 1 гигабайта. Почему именно 1024m? Этого хватает для 95% проектов: веб-сервисов, мобильных бэкендов, микросервисов. Значение не перегружает диск и сеть, и его легко изменить при необходимости — просто поменяйте параметр в .env.
В конфиге есть ещё один важный параметр, про который я бы хотел рассказать подробнее — privileged: true у сервиса gitlab-runner.
Многие гайды просто копируют эту строчку, не объясняя, зачем она нужна и чем рискует сервер.
Этот флаг даёт контейнеру раннера расширенные права, практически равные root на хост-машине. В экосистеме GitLab он критичен, если вы планируете собирать Docker-образы прямо внутри пайплайна — так называемый docker-in-docker (dind). Без него команда docker build или docker push в .gitlab-ci.yml упадёт с ошибкой permission denied или cannot connect to Docker daemon, потому что изолированный контейнер по умолчанию не имеет права управлять демоном Docker.
Важно понимать: privileged: true — это не просто «включить и забыть». Он снимает большинство механизмов изоляции контейнера. Теоретически, если в репозиторий попадёт злонамеренный код, он сможет выйти за пределы песочницы и получить доступ к файловой системе хоста. Для внутренней команды, где каждый участник проверен, а код ревьювится, это допустимый риск. Но если вы принимаете merge-реквесты от внешних контрибьюторов, размещаете несколько независимых проектов или храните на сервере чувствительные данные — такой режим становится уязвимостью.
Что делать, если безопасность в приоритете? Можно перейти на rootless-режим Docker в раннере, использовать Kubernetes executor (если инфраструктура уже в k8s), вынести раннер на отдельную виртуальную машину для полной изоляции или оставить privileged, но жёстко ограничить доступ к серверу через фаервол и права на запуск пайплайнов.
В нашем конфиге мы сознательно оставляем privileged: true, потому что это самый быстрый способ получить рабочий CI/CD «из коробки» для доверенной команды. Параметр легко отключить или заменить на альтернативу, когда проект вырастет.
Ну вот и всё — конфиг готов к использованию. Мне 18 лет, и я продолжаю учиться: если вы заметили ошибку, неточность или знаете способ сделать лучше — напишите в комментариях. Конструктивная критика поможет мне (и, надеюсь, другим читателям) стать лучше.
🙌 Спасибо, что дочитали!