Собственный сервер для хранения кода - вещь, о которой разработчики и команды начинают задумываться рано или поздно. Причины разные, но сходятся они в одной точке. Контроль над репозиториями должен принадлежать тем, кто пишет код, а не сторонним платформам, которые могут поменять тарифы, заблокировать аккаунт или внезапно исчезнуть с рынка. GitLab в self-hosted варианте закрывает этот вопрос раз и навсегда. А запуск через Docker превращает развёртывание из многочасового марафона в процедуру на полчаса неспешной работы.
Почему именно Docker оказался лучшим способом запустить GitLab без головной боли и бесконечных зависимостей
Классическая установка GitLab традиционно доставляет удовольствие только первые пятнадцать минут. Дальше начинаются чудеса. Ruby, PostgreSQL, Redis, Nginx, Sidekiq, Puma, Gitaly - десятки взаимосвязанных компонентов, каждый со своими версиями, настройками и капризами. Обновление одного модуля может потянуть за собой перестройку половины системы. Резервное копирование превращается в ритуальный танец с бубном, а миграция на другой сервер занимает целый выходной.
Docker расставляет всё по местам. Один образ содержит всю экосистему целиком. Изоляция от основной системы избавляет от конфликтов версий. Конфигурация и данные хранятся в отдельных томах, которые легко бэкапить. Переезд на другой сервер сводится к копированию нескольких директорий и запуску контейнера. Элегантность подхода такая, что хочется немедленно переводить на него все подобные сервисы.
Community Edition в качестве бесплатной версии покрывает 90 процентов нужд средней команды. Приватные репозитории, ветвление, merge request'ы, встроенный CI/CD, трекер задач, вики, собственный реестр контейнеров. Если в какой-то момент этого станет мало, переход на Enterprise Edition делается через один параметр в конфигурации, данные при этом не теряются.
Подготовка брандмауэра и базовых утилит перед запуском тяжёлого контейнера с репозиториями
Любая настройка сервера начинается одинаково. Свежая система - меньше неожиданностей потом. Банальная истина, которая почему-то регулярно игнорируется и приводит к отладке странных ошибок, вызванных несовместимостью библиотек.
$ sudo apt update && sudo apt upgrade
GitLab живёт на сетевых портах, поэтому файрвол должен быть настроен заранее. Иначе получится комичная ситуация, когда сервис запущен, контейнер работает, а снаружи ничего не доступно. Смотрим, что уже открыто на сервере.
$ sudo ufw status
На чистой Ubuntu в ответе обычно одиноко висит правило для SSH. Это значит, что файрвол работает, но пропускает только административный трафик.
Status: active
To Action From
-- ------ ----
OpenSSH ALLOW Anywhere
OpenSSH (v6) ALLOW Anywhere (v6)
Веб-интерфейс GitLab будет слушать стандартные порты HTTP и HTTPS. Первый нужен для первоначального запроса и автоматического редиректа на защищённое соединение. Второй для всей основной работы. Порт 587 понадобится для отправки уведомлений через SMTP-ретранслятор.
$ sudo ufw allow http
$ sudo ufw allow https
Теперь порт почтового шлюза. Использовать 587 вместо древнего 25 - общая практика, поскольку большинство хостинг-провайдеров блокируют 25-й по умолчанию для борьбы со спам-рассылками.
$ sudo ufw allow http
$ sudo ufw allow 587
После всех изменений имеет смысл перепроверить состояние файрвола. Мелкие опечатки в правилах - частая причина долгих разборов, почему сервис не отвечает.
$ sudo ufw status
В ответе должна появиться уже развёрнутая картина с открытыми портами для IPv4 и IPv6. Если какого-то из них не хватает, правило надо дописать.
Следом подтягиваются утилиты, без которых дальше просто не обойтись. Часть из них наверняка уже установлена в системе, но apt разумно пропустит готовые пакеты и подхватит только недостающие.
$ sudo apt install ca-certificates curl openssh-server apt-transport-https gnupg lsb-release -y
Назначение пакетов в этом списке стоит понимать. Сертификаты корневых центров нужны для проверки подписей при скачивании образов из внешних реестров. Curl служит универсальным инструментом загрузки. OpenSSH-server обеспечивает удалённый доступ. Apt-transport-https даёт возможность работать с защищёнными репозиториями. Gnupg проверяет подлинность пакетов. Lsb-release отвечает за определение версии дистрибутива.
Смена стандартного SSH-порта системы ради избежания конфликта с SSH-сервером внутри контейнера
Здесь скрывается интересный нюанс, на который регулярно попадаются новички. GitLab внутри контейнера хочет слушать 22-й порт для Git-операций по SSH. Операционная система на хост-машине тоже использует 22-й порт для администрирования. Конфликт неизбежен. Решение простое - переезд системного SSH на нестандартный порт.
$ sudo nano /etc/ssh/sshd_config
Внутри конфига находим закомментированную строку с портом. Снимаем комментарий, меняем число. Логика выбора нового номера - любая свободная позиция выше 1024. Некоторые админы выбирают легко запоминающиеся числа вроде 2222, другие предпочитают случайные варианты ради минимальной защиты от автоматических сканеров.
#Port 22
Заменяется на следующее значение.
Port 2425
После сохранения файла службу нужно перезапустить, чтобы изменения вступили в силу. Важный момент - текущее SSH-соединение при этом не разорвётся. Но новые подключения пойдут уже по новому порту.
$ sudo systemctl restart sshd
Новый порт обязательно открывается в файрволе. Иначе после разрыва текущей сессии вернуться на сервер станет физически невозможно, и придётся лезть к провайдеру за консолью восстановления.
$ sudo ufw allow 2425
Теперь правильный рефлекс - проверить новое подключение, не закрывая старое. Открывается второе окно терминала и тестируется вход по новому порту.
$ ssh username@<serverIP> -p 2425
Если вторая сессия успешно устанавливается, первую можно спокойно закрывать. Если что-то пошло не так, есть возможность откатить изменения через уже открытое соединение.
Установка Docker с репозитория разработчика и подключение текущего пользователя к группе docker
Стандартный Docker из репозиториев Ubuntu обычно отстаёт на пару версий от актуального. Для production-нагрузок лучше брать свежий прямо у разработчика. Начинается всё с импорта GPG-ключа для проверки подлинности пакетов.
$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker.gpg
Сам репозиторий подключается одной развёрнутой командой. Архитектура определяется автоматически, кодовое имя релиза Ubuntu подставляется через lsb_release. Такой подход делает инструкцию универсальной - один и тот же код работает на Jammy, Focal и Bionic без изменений.
$ echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
$(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
Обновляем списки пакетов, чтобы новый репозиторий был виден системе.
$ sudo apt update
Ставим весь необходимый набор. Docker-ce - сам движок. Docker-ce-cli - утилита командной строки. Containerd.io - низкоуровневая среда исполнения. Docker-compose-plugin - плагин для работы с compose-файлами в новом стиле.
$ sudo apt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin
Стоит обратить внимание на одну смену парадигмы. Раньше Docker Compose был отдельным бинарным файлом, и команда вызывалась через дефис как docker-compose. Теперь это плагин самого Docker, и вызов выглядит как docker compose с пробелом. На функциональности это не сказывается, но мелкое различие порой сбивает с толку при копировании старых инструкций.
Чтобы не писать sudo перед каждой командой docker, имеет смысл добавить текущего пользователя в группу docker. Удобство существенное, особенно при частой работе.
$ sudo usermod -aG docker ${USER}
Переменная окружения USER подхватит текущее имя учётной записи автоматически. Если хочется выдать права другому пользователю, имя просто подставляется вместо переменной. Для применения нового членства в группе нужно либо перелогиниться, либо использовать команду su.
$ su - $(USER)
После этого можно работать с Docker без sudo. Небольшое замечание по безопасности - пользователь в группе docker фактически получает root-права на системе, поскольку контейнеры умеют монтировать произвольные директории. На сервере, где помимо админа сидят другие люди, это нужно учитывать.
Организация томов Docker для постоянного хранения данных и конфигурации GitLab
Контейнеры по своей природе эфемерны. Убили контейнер - данные внутри пропали. Для GitLab такое поведение недопустимо категорически. Репозитории, настройки, базы данных должны переживать перезапуски, обновления и даже полное пересоздание контейнера. Решает эту задачу механизм томов.
Создаём базовую директорию для данных.
$ sudo mkdir /srv/gitlab -p
Выбор именно /srv обоснован традицией Linux. Этот каталог изначально предназначен для данных, которые сервер предоставляет наружу. Альтернативы вроде /var/lib или /opt тоже рабочие, но /srv остаётся семантически наиболее правильным выбором.
Отдельный каталог под compose-файл делаем в домашней директории пользователя.
$ mkdir ~/gitlab-docker
Переходим в него.
$ cd ~/gitlab-docker
Создаём файл переменных окружения. Docker Compose умеет автоматически подхватывать .env-файл из текущей директории, что избавляет от необходимости каждый раз экспортировать переменные вручную.
$ nano .env
Внутри прописываем путь к директории с данными.
GITLAB_HOME=/srv/gitlab
Такой подход с отдельным .env даёт неожиданную гибкость. Если в будущем понадобится перенести данные на быстрый NVMe-диск или монтированную сетевую шару, достаточно поменять одну строку и перезапустить контейнер. Вся конфигурация, привязанная к путям, переедет следом.
Структура томов в GitLab устроена логично. Три директории внутри $GITLAB_HOME монтируются в соответствующие пути контейнера. Data хранит приложение целиком, включая репозитории и базу данных. Logs содержит файлы журналов всех компонентов. Config держит настройки и автоматически сгенерированные секреты.
Создание compose-файла с полным набором параметров и первый запуск контейнера GitLab
Главный файл конфигурации создаётся и открывается в редакторе.
$ nano docker-compose.yml
Содержимое получается объёмным, но каждая строка здесь выполняет конкретную функцию. Пропуск любой из них рискует превратить установку в источник странных проблем через несколько месяцев работы.
version: '3.6'
services:
web:
image: 'gitlab/gitlab-ee:latest'
container_name: 'gitlab-howtoforge'
restart: always
hostname: 'gitlab.example.com'
environment:
GITLAB_OMNIBUS_CONFIG: |
external_url 'https://gitlab.example.com'
gitlab_rails['smtp_enable'] = true
gitlab_rails['smtp_address'] = "email-smtp.us-west-2.amazonaws.com"
gitlab_rails['smtp_user_name'] = "SESUsername"
gitlab_rails['smtp_password'] = "SESKey"
gitlab_rails['smtp_domain'] = "example.com"
gitlab_rails['smtp_enable_starttls_auto'] = true
gitlab_rails['smtp_port'] = 587
gitlab_rails['smtp_authentication'] = "login"
gitlab_rails['gitlab_email_from'] = Адрес электронной почты защищен от спам-ботов. Для просмотра адреса в браузере должен быть включен Javascript. '
gitlab_rails['gitlab_email_reply_to'] = Адрес электронной почты защищен от спам-ботов. Для просмотра адреса в браузере должен быть включен Javascript. '
# Add any other gitlab.rb configuration here, each on its own line
ports:
- '80:80'
- '443:443'
- '22:22'
- '587:587'
volumes:
- '$GITLAB_HOME/config:/etc/gitlab'
- '$GITLAB_HOME/logs:/var/log/gitlab'
- '$GITLAB_HOME/data:/var/opt/gitlab'
shm_size: '256m'
Стоит разобрать ключевые параметры по очереди. Image указывает на образ Enterprise Edition, но на самом деле без платной лицензии он ведёт себя как Community Edition и автоматически ограничивает функции. Такой ход позволяет в любой момент активировать платные возможности простым вводом ключа, без перезаливки всего окружения. Container_name даёт контейнеру понятное имя, по которому к нему удобно обращаться из других команд.
Параметр restart со значением always - небольшая подстраховка. Если контейнер по какой-то причине упадёт, Docker автоматически поднимет его обратно. В production такое поведение критично. Никто не хочет получать звонки в три часа ночи из-за того, что сервис не вернулся после сбоя.
Блок environment содержит всю магию GitLab Omnibus Config. Любая настройка, которую можно было бы внести в gitlab.rb, здесь задаётся одной строкой. External_url определяет основной адрес сервиса. Использование https в URL автоматически включает получение сертификата Let's Encrypt. SMTP-параметры настраиваются под конкретного почтового провайдера - в примере используется Amazon SES, но подойдёт любой совместимый ретранслятор от Mailgun до SendGrid или собственного почтового сервера.
Раздел ports прокидывает порты из контейнера наружу. Формат хост:контейнер очень важен. Если на сервере 22-й порт занят системным SSH (а именно поэтому его ранее переводили на 2425), маппинг 22:22 отправит запросы на 22 хост-порте в 22-й порт контейнера. Для ситуаций, когда менять системный SSH нельзя, используется другой подход - на хосте берётся нестандартный порт, а внутри контейнера остаётся стандартный.
Shm_size заслуживает отдельного упоминания. Стандартные 64 мегабайта разделяемой памяти в Docker не хватает для корректной работы Prometheus-метрик внутри GitLab. При недостатке памяти в логах начинают появляться странные ошибки, не очевидные с первого взгляда. Установка 256 мегабайт - разумный минимум, на серверах с большим объёмом оперативки можно выставлять и больше.
Запускаем контейнер в фоновом режиме.
$ docker compose up -d
Первый запуск - зрелище не быстрое. GitLab скачивает образ размером около двух гигабайт, затем разворачивает внутри базу данных, инициализирует все компоненты, генерирует секреты. Процесс занимает от пяти до пятнадцати минут в зависимости от мощности сервера и скорости канала. Следить за происходящим удобно через логи.
$ docker logs gitlab-howtoforge -f
Ctrl+C прерывает слежение, но контейнер продолжает работать. Текущее состояние контейнера смотрится отдельной командой.
$ docker ps
Начиная с версии 14.0, GitLab больше не просит вводить пароль при первом входе. Вместо этого он сам генерирует случайный пароль для учётной записи root и сохраняет его в файле внутри контейнера. Забрать пароль можно через cat.
$ sudo cat /srv/gitlab/config/initial_root_password
Вывод выглядит так.
# WARNING: This value is valid only in the following conditions
# 1. If provided manually (either via `GITLAB_ROOT_PASSWORD` environment variable or via `gitlab_rails['initial_root_password']` setting in `gitlab.rb`, it was provided before database was seeded for the first time (usually, the first reconfigure run).
# 2. Password hasn't been changed manually, either via UI or via command line.
#
# If the password shown here doesn't work, you must reset the admin password following https://docs.gitlab.com/ee/security/reset_user_password.html#reset-your-root-password.
Password: Hz3t7Etn18wB6VAfBWyDlYbN2VQdMCO0xIIENfDHcFo=
# NOTE: This file will be automatically deleted in the first reconfigure run after 24 hours.
Очень важный нюанс из комментариев в файле - он автоматически удаляется через 24 часа после первого reconfigure. Пароль надо забрать и сохранить в надёжном месте немедленно, иначе потом придётся пользоваться процедурой сброса через консольные утилиты.
Настройка административной части GitLab после первого входа в веб-интерфейс
По адресу, указанному в external_url, теперь открывается свежеустановленный GitLab. Логин root, пароль из файла. После входа система показывает пустой дашборд с единственным проектом самомониторинга.
Первое, с чего стоит начать настройку - запрет открытой регистрации. По умолчанию любой человек, знающий адрес сервера, может создать учётную запись. Для личного или командного GitLab это категорически неприемлемо. Настройка находится либо в заметном попапе на дашборде, либо в разделе Admin Panel > Settings > General > Sign-up restrictions. Галку Sign-up enabled надо снять и сохранить изменения.
Следующий шаг - смена стандартного рутового пароля на нормальный. Даже если сгенерированный пароль выглядит достаточно случайным, оставлять его как есть - плохая практика. Личный пароль, известный только администратору, должен заменить автоматический. Делается это через меню пользователя, раздел Edit profile, далее Password.
Имя пользователя root тоже можно поменять на что-то более привычное. Многих админов раздражает работа под учёткой root, даже в веб-интерфейсе. Новое имя задаётся в том же профиле, раздел Account. После смены происходит принудительный выход, и залогиниться придётся уже под новым логином. Двухфакторную аутентификацию на той же странице тоже стоит включить.
Отдельный пункт - отключение телеметрии и сбора метрик. GitLab по умолчанию отсылает в компанию анонимную статистику использования. Многим такое поведение не нравится. Через Admin Panel > Settings > Metrics and profiling галка Enable Service Ping снимается, и телеметрия прекращается. В том же разделе можно выключить Prometheus-метрики, если мониторинг внутреннего состояния GitLab не нужен - это заметно снижает потребление ресурсов.
Создание SSH-ключа и первый коммит в новый репозиторий через стандартный клиент Git
Работа с Git по SSH - основной способ взаимодействия с репозиториями. Если ключ уже есть, его можно переиспользовать. Если нет - генерируется новый. Ed25519 предпочтительнее классического RSA по соображениям скорости и безопасности.
$ ssh-keygen -t ed25519 -C "gitlab.example.com"
Параметр -C добавляет комментарий к ключу, чтобы потом было понятно, откуда он взялся. На вопрос о пути сохранения можно жать Enter для дефолтного варианта. Passphrase желательно задавать непустой - это второй слой защиты на случай кражи файла ключа.
Добавление ключа в ssh-agent избавляет от необходимости вводить пароль при каждом обращении к репозиторию. На Linux и macOS агент стартует так.
$ eval $(ssh-agent -s)
Добавляем сам ключ.
$ ssh-add ~/.ssh/id_ed25519
Настройки подключения для конкретного хоста удобно прописать в ~/.ssh/config. Это освобождает от написания длинных команд с указанием ключа для каждого действия.
Host gitlab.example.com
PreferredAuthentications publickey
IdentityFile ~/.ssh/id_ed25519
Публичную часть ключа теперь надо загрузить в GitLab. Смотрим её содержимое.
$ cat ~/.ssh/id_ed25519.pub
Вывод копируется целиком и вставляется в настройках профиля GitLab в разделе SSH Keys. Проверяем соединение.
$ ssh -T Адрес электронной почты защищен от спам-ботов. Для просмотра адреса в браузере должен быть включен Javascript.
При первом подключении система попросит подтвердить подлинность хоста. После этого появится приветственное сообщение с именем пользователя - значит, всё работает.
Создание первого проекта делается через кнопку New project на дашборде. Имя, описание, видимость, инициализация с README - стандартный набор параметров. После создания проекта его можно клонировать к себе.
$ git clone Адрес электронной почты защищен от спам-ботов. Для просмотра адреса в браузере должен быть включен Javascript. :user/howtoforge-test.git
Тестовый файл и коммит делаются привычными командами Git.
$ cd howtoforge-test
$ touch CHANGELOG
$ git add CHANGELOG
$ git commit -m "add Changelog"
$ git push -u origin main
После push файл появится в веб-интерфейсе. Это момент, когда абстрактная установка превращается в рабочий инструмент.
Управление контейнером GitLab и процедуры резервного копирования на случай форс-мажоров
Повседневные операции с контейнером сводятся к нескольким командам. Остановка.
$ docker compose down
Запуск.
$ docker compose up -d
При изменении параметров в compose-файле одного рестарта недостаточно. Контейнер надо полностью пересоздать.
$ docker compose down --remove-orphans
$ docker compose up -d
Обычный рестарт без пересоздания используется для мелких ситуаций.
$ docker compose restart
Иногда нужно зайти внутрь контейнера - посмотреть логи конкретного компонента или запустить диагностическую утилиту.
$ docker exec -it <container name> bash
Резервное копирование делается одной командой. GitLab сам знает, что и как бэкапить.
$ docker exec -t gitlab-howtoforge gitlab-backup create
Файл бэкапа появляется в /srv/gitlab/data/backups. Важный нюанс - в бэкап не попадают секреты из gitlab-secrets.json и сам compose-файл. Их нужно сохранять отдельно, иначе восстановление на другом сервере станет проблематичным.
Обновление сводится к скачиванию свежего образа и пересозданию контейнера.
$ cd ~/gitlab-docker
$ docker compose down --remove-orphans
$ docker compose pull
$ docker compose up -d
Для минорных версий такая схема работает безупречно. Для мажорных переходов имеет смысл заранее прочитать официальную документацию - иногда требуются промежуточные шаги или специальные миграции.
На выходе получается полноценная замена облачным сервисам с полным контролем над кодом и данными. Домашний GitLab - не просто инструмент, а заявление о том, что разработчик сам решает, где живёт его код. В мире, где сервисы меняют правила игры без предупреждения, такая независимость стоит потраченных усилий на настройку.