Собственный сервер для хранения кода - вещь, о которой разработчики и команды начинают задумываться рано или поздно. Причины разные, но сходятся они в одной точке. Контроль над репозиториями должен принадлежать тем, кто пишет код, а не сторонним платформам, которые могут поменять тарифы, заблокировать аккаунт или внезапно исчезнуть с рынка. 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 - не просто инструмент, а заявление о том, что разработчик сам решает, где живёт его код. В мире, где сервисы меняют правила игры без предупреждения, такая независимость стоит потраченных усилий на настройку.