Перенос GitLab на другой сервер в Docker
перенос VDS/VPS Docker GitLab git GitLab Runner GitLab Pages Traefik Debian
Примерно год я мучался с GitLab на сервере с двумя гигабайтами оперативки. Когда оплаченный период закончился, решил взять более мощный VDS по формуле 4/4/30. До этого сам GitLab был установлен непосредственно из репозитория, но для экспериментов с Pages и т.д. нужен Docker. А раз он и так есть, почему бы не завернуть GitLab в контейнер? Заодно на сервер можно будет установить что-нибудь еще.
Так как Arch Linux мой хостер не предустанавливает, а Rocky для Докера мне представляется избыточным (да и ядра там не слишком новые), в качестве ОС я выбрал Debian 12.
Подготовка
Поскольку на VDS/VPS предоставляется root-доступ, я бессовестным образом под ним и работаю (если явно не указано иное).
Файл подкачки
Наверное надо бы создать на всякий случай. Стандарт на 2 гига:
fallocate -l 2G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
В /etc/fstab добавляем:
/swapfile none swap defaults 0 0
В /etc/sysctl.conf добавляем:
vm.swappiness=10
Перезагружаемся для пущей важности.
Docker
Практически стандартная установка, разве что можно пропустить первые шаги – ca-certificates, curl и /etc/apt/keyrings по идее есть.
curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
chmod a+r /etc/apt/keyrings/docker.asc
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
tee /etc/apt/sources.list.d/docker.list > /dev/null
apt update
apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
Сервис включается и запускается сразу.
Traefik
Внезапно? Поскольку мне не хотелось, чтобы столь мощный (для меня, во всяком случае) сервер ограничивался одним лишь GitLab'ом, я решил поставить данный прокси. Плюсом он возьмет на себя все вопросы с SSL-сертификатами. Да, в прошлый раз я исхитрился применить Let's Encrypt, скажем так, не только для GitLab, но повторять этот опыт мне не хочется совершенно, равно как и лишний раз связываться с Nginx.
По аналогии с моим обзором запустим Traefik под пользователем traefik
, изолируем сокет с помощью wollomatic/socket-proxy и защитим панель управления базовой аутентификацией. Разве что пути будут короче, в данном случае /srv/traefik:
mkdir -p /srv/traefik/config
cd /srv/traefik
useradd -c 'Traefik user' -M -s /usr/sbin/nologin traefik
apt install apache2-utils
htpasswd -cbBC 10 config/usersfile admin "P@ssw0rd"
Понятно, что на самом деле пользователь панели не admin
и пароль не P@ssw0rd
Настройки по сути перекочевали из обзора, разве что я объявил websecure
точкой входа по умолчанию, чтобы каждый раз не указывать ее явно – файл /srv/traefik/config/traefik.yaml:
providers:
docker:
exposedByDefault: false
endpoint: 'tcp://dockerproxy:2375'
network: traefik
api: {}
entryPoints:
web:
address: ":10080" # will be routed to port 80
http:
redirections:
entrypoint:
to: ":443"
scheme: https
websecure:
address: ":10443" # will be routed to port 443
asDefault: true
http:
tls:
certResolver: letsencrypt
http3:
advertisedPort: 443
certificatesResolvers:
letsencrypt:
acme:
email: admin@example.com
storage: /etc/traefik/acme.json
tlsChallenge: {}
compose.yaml в немалой степени тоже:
services:
dockerproxy:
image: wollomatic/socket-proxy:1
restart: unless-stopped
user: "65534:${DOCKERGID:-994}"
mem_limit: 64M
read_only: true
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
command:
- '-loglevel=error'
- '-listenip=0.0.0.0'
- '-allowfrom=traefik' # allow only hostname "traefik" to connect
- '-allowGET=/v1\..{1,2}/(version|containers/.*|events.*)'
- '-allowbindmountfrom=/var/log,/tmp' # restrict bind mounts to specific directories
- '-watchdoginterval=3600' # check once per hour for socket availability
- '-stoponwatchdog' # halt program on error and let compose restart it
- '-shutdowngracetime=5' # wait 5 seconds before shutting down
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
- docker-proxynet # NEVER EVER expose this to the public internet!
# this is a private network only for traefik and socket-proxy
traefik:
image: traefik:v3.4
restart: unless-stopped
read_only: true
depends_on:
- dockerproxy
security_opt:
- no-new-privileges:true
ports:
- "80:10080" # use high ports inside the container so
- "443:10443" # we don't need to be root to bind the ports
networks:
- default
- docker-proxynet
labels:
- "traefik.enable=true"
- "traefik.http.routers.dashboard.rule=Host(`traefik.example.com`) && (PathPrefix(`/api`) || PathPrefix(`/dashboard`))"
- "traefik.http.routers.dashboard.service=api@internal"
- "traefik.http.routers.dashboard.middlewares=apiauth,compress"
- "traefik.http.middlewares.apiauth.basicauth.usersfile=/etc/traefik/usersfile"
- "traefik.http.middlewares.compress.compress.encodings=zstd,br,gzip"
- "traefik.http.middlewares.compress.compress.excludedContentTypes=image/png,image/jpeg,image/gif,font/*"
volumes:
- ./config/traefik.yaml:/etc/traefik/traefik.yaml:ro # static configuration
- ./config/usersfile:/etc/traefik/usersfile:ro # dashboard basic auth
- ./config/acme.json:/etc/traefik/acme.json # certificate storage
user: "${TRAEFIKUID:-1000}"
networks:
default:
name: traefik
docker-proxynet:
internal: true
Отличие в том, что я еще объявил промежуточный слой сжатия данных с приоритетом zstd и исключением сжатия картинок и шрифтов (жаль, что это приходится указывать самим). Не путать идентификатор (первый compress
) с типом (второй)!
Перед запуском не забудьте уточнить UID (id -u traefik
) и GID (getent group docker | cut -d: -f3
), а также еще немного похимичить:
touch /srv/traefik/config/acme.json
chmod 600 /srv/traefik/config/acme.json
chown -R traefik:traefik /srv/traefik/config
Вроде бы можно запускать.
GitLab
https://docs.gitlab.com/administration/backup_restore/migrate_to_new_server/
Сохранение данных
В принципе у меня как бы и так все сохранено, разве что Redis вызывает определенные вопросы. В инструкции прямо какая-то сложная последовательность, но на всякий случай наверное ее лучше выполнить.
Итак, отключаем CI/CD – можно подумать, у меня они есть. В /etc/gitlab/gitlab.rb правим:
nginx['custom_gitlab_server_config'] = "location = /api/v4/jobs/request {\n deny all;\n return 503;\n }\n"
И переконфигурирем:
gitlab-ctl reconfigure
Заходим в GitLab под root, админ-панель, и начинаем отключать всякие периодические задания (Monitoring – Background jobs). На вкладке cron отключаем все. Теперь в инструкции предлагается убедиться, что все задачи CI/CD завершены (почему-то говорится об Overview – Jobs, но наверное имеются в виду CI/CD – Jobs). У меня они определенно завершены . Возвращаемся в Monitoring – Background jobs и ждем, когда показатели «занят» и «в очереди» достигнут 0.
Сбрасываем БД Redis на диск с помощью весьма хитрой команды, а заодно отключаем лишние сервисы:
/opt/gitlab/embedded/bin/redis-cli -s /var/opt/gitlab/redis/redis.socket save && gitlab-ctl stop && gitlab-ctl start postgresql && gitlab-ctl start gitaly
И так бессовестным образом работаю под root, поэтому sudo поубирал. Архивируем:
gitlab-backup create
Наконец, предлагается остановить все сервисы на уровне gitlab.rb – добавить:
alertmanager['enable'] = false
gitlab_exporter['enable'] = false
gitlab_pages['enable'] = false
gitlab_workhorse['enable'] = false
logrotate['enable'] = false
gitlab_rails['incoming_email_enabled'] = false
nginx['enable'] = false
node_exporter['enable'] = false
postgres_exporter['enable'] = false
postgresql['enable'] = false
prometheus['enable'] = false
puma['enable'] = false
redis['enable'] = false
redis_exporter['enable'] = false
registry['enable'] = false
sidekiq['enable'] = false
В инструкции есть grafana['enable'] = false
, мой GitLab на это дело ругнулся и я эту строчку убрал.
Переконфигурируем и проверяем статусы:
gitlab-ctl reconfigure
gitlab-ctl status
Вроде как должны были все сервисы остановиться, но у меня nginx с gitaly так и работали. Наверное ничего страшного.
Установка в Docker
https://docs.gitlab.com/install/docker/installation/
mkdir /srv/gitlab
cd /srv/gitlab
Создал .env:
GITLAB_HOME=/srv/gitlab
Честно говоря, не очень понятно, зачем это надо, но пусть будет раз в инструкции так сказано. Не самый предварительный compose.yaml:
services:
gitlab:
image: gitlab/gitlab-ce:18.1.1-ce.0
container_name: gitlab
restart: always
environment:
GITLAB_OMNIBUS_CONFIG: |
# Add any other gitlab.rb configuration here, each on its own line
external_url 'https://git.example.com'
letsencrypt['enable'] = false
nginx['listen_port'] = 80
nginx['listen_https'] = false
gitlab_rails['gitlab_shell_ssh_port'] = 2424
ports:
- '2424:22'
labels:
- traefik.enable=true
- traefik.http.routers.gitlab.rule=Host(`git.example.com`)
- traefik.http.routers.gitlab.service=gitlab
- traefik.http.routers.gitlab.middlewares=compress
- traefik.http.services.gitlab.loadbalancer.server.port=80
volumes:
- '$GITLAB_HOME/config:/etc/gitlab'
- '$GITLAB_HOME/logs:/var/log/gitlab'
- '$GITLAB_HOME/data:/var/opt/gitlab'
shm_size: '256m'
networks:
default:
name: traefik
external: true
Обратите внимание на явное указание версии GitLab. Поскольку на старом сервере работала версия 18.1.1, точно такая же должна быть и на новом для успешного восстановления из бэкапа.
Укажем наиболее критические настройки в переменной окружения GITLAB_OMNIBUS_CONFIG
. Сразу же отключаем Let's Encrypt – этим у нас Traefik занимается. Для этого же настраиваем Nginx на прослушивание 80-го порта без https. Поскольку в контейнере первым открыт 22-й порт, 80-й нужно явно указать для Traefik'а.
Также ставим нестандартный порт SSH для git'а – мне как бы на сам сервер тоже иногда надо попадать. С другой стороны, я вроде как в настройках в принципе только HTTP(S) оставил, так что можно было бы наверное и вовсе не пробрасывать никакой порт. С третьей стороны, SSH можно было бы пробросить через Traefik по имени хоста как TCP-сервис.
Где-то ±в этот момент надо бы перенацелить DNS.
В первый раз предлагаю запустить просто docker compose up
– нам же потом это все надо будет останавливать (Ctrl+C в данном случае), а заодно можно и за процессом установки понаблюдать (довольно длительным).
Теперь пора восстанавливать конфигурацию. В моем случае это настройки отправки почты, gitlab_rails['omniauth_providers']
– подключение к BitBucket (я в свое время импортировал оттуда репозитории), время хранения бэкапов gitlab_rails['backup_keep_time'] = 604800
и отключение реестра контейнеров registry['enable'] = false
.
Также есть некоторые оптимизации по памяти:
puma['worker_processes'] = 0
sidekiq['concurrency'] = 8
prometheus_monitoring['enable'] = false
И настройки Pages:
pages_external_url 'https://pages.example.com'
gitlab_pages['enable'] = true
gitlab_pages['namespace_in_path'] = true
pages_nginx['enable'] = false
Для связки с Traefik как обратного прокси конфигурацию надо доработать. Для работы Pages указываем gitlab_pages['listen_proxy'] = "0.0.0.0:8090"
– по умолчанию прослушивается localhost, поэтому извне контейнера изначально доступа нет.
If running GitLab Pages behind a reverse proxy with TLS termination, specify
listen_proxy
instead ofexternal_http
.
Для получения реального IP-адреса Nginx'ом Гитлаба прописываем доверенные прокси (с запасом):
nginx['real_ip_trusted_addresses'] = ['172.16.0.0/12']
А также, раз мы передаем управление сжатием Трафику, отключаем ее в настройках: nginx['gzip_enabled'] = false
. Если честно, не особо помогает, поскольку то ли конь (Workhorse), то ли кошка (Puma) сами жмут в gzip и как это отключить, совершенно непонятно. Но хотя бы запросы к graphql сжимаются.
Соответственно добавляем метки в compose.yaml…
- traefik.http.routers.pages.rule=Host(`pages.example.com`)
- traefik.http.routers.pages.service=pages
- traefik.http.routers.pages.middlewares=compress
- traefik.http.services.pages.loadbalancer.server.port=8090
И дополнительно открываем порт:
expose:
- '8090'
По идее симметрично старому серверу надо временно отрубить CI/CD:
nginx['custom_gitlab_server_config'] = "location = /api/v4/jobs/request {\n deny all;\n return 503;\n }\n"
Вниз-вверх для переконфигурирования.
Перенос данных
https://docs.gitlab.com/administration/backup_restore/restore_gitlab/
Если что, обратно docker compose down
и раскладываем файлы:
- /srv/gitlab/config – gitlab-secrets.json
- /srv/gitlab/data/backups – наш бэкап, например 1751304392_2025_06_30_18.1.1_gitlab_backup.tar
- /srv/gitlab/data/gitlab-rails/shared/encrypted_settings – мой зашифрованный файл доступов к SMTP smtp.yaml.enc
- /srv/gitlab/data/redis – dump.rdb
Запускаем GitLab в фоне:
docker compose up -d
Не совсем понятно, сколько ждать, видимо пока не появятся HTTP-сервисы в Traefik и/или когда начнет отвечать веб. Отключаем сервисы:
docker exec gitlab gitlab-ctl stop puma
docker exec gitlab gitlab-ctl stop sidekiq
Предлагается на всякий случай проверить состояние:
docker exec gitlab gitlab-ctl status
Восстанавливаем данные («хвост» имени файла бэкапа не указывается):
docker exec -t gitlab gitlab-backup restore BACKUP=1751304392_2025_06_30_18.1.1
После вроде как успешного восстановления я сделал стоп-старт через compose.
Если все в порядке, можно будет зайти под root в админ-панель GitLab и включить обратно все cron-задания. В очередной раз docker compose down
, убираем nginx['custom_gitlab_server_config']
и наконец запускаем все обратно. Уфф.
Для очистки совести можно выполнить пару проверок:
docker exec -t gitlab gitlab-rake gitlab:check SANITIZE=true
docker exec -t gitlab gitlab-rake gitlab:doctor:secrets
По идее обе должны пройти успешно.
Поскольку Nginx по сути тоже работает обратным прокси для вышеупомянутой «рабочей лошадки», то целых два прокси (+Traefik), а на самом деле три, поскольку лошадь – тоже прокси, по-моему перебор. Но увы – если отключить Nginx и заставить коня слушать TCP, с которым будет работать Traefik, то перестает работать вебсокет. Теоретически работать можно, но неудобно – например раздел активности в проблемах (которые issue) «на лету» не обновляется, так что выкинуть мой «любимый» Nginx к сожалению не получилось.
Финальная конфигурация
Для удобства я решил подытожить, что же все-таки у меня вышло.
.env:
GITLAB_HOME=/srv/gitlab
compose.yaml:
services:
gitlab:
image: gitlab/gitlab-ce:latest
container_name: gitlab
restart: always
environment:
GITLAB_OMNIBUS_CONFIG: |
# Add any other gitlab.rb configuration here, each on its own line
external_url 'https://git.example.com'
letsencrypt['enable'] = false
nginx['listen_port'] = 80
nginx['listen_https'] = false
gitlab_rails['gitlab_shell_ssh_port'] = 2424
ports:
- '2424:22'
expose:
- '8090'
labels:
- traefik.enable=true
- traefik.http.routers.gitlab.rule=Host(`git.example.com`)
- traefik.http.routers.gitlab.service=gitlab
- traefik.http.routers.gitlab.middlewares=compress
- traefik.http.services.gitlab.loadbalancer.server.port=80
- traefik.http.routers.pages.rule=Host(`pages.example.com`)
- traefik.http.routers.pages.service=pages
- traefik.http.routers.pages.middlewares=compress
- traefik.http.services.pages.loadbalancer.server.port=8090
volumes:
- '$GITLAB_HOME/config:/etc/gitlab'
- '$GITLAB_HOME/logs:/var/log/gitlab'
- '$GITLAB_HOME/data:/var/opt/gitlab'
shm_size: '256m'
networks:
default:
name: traefik
external: true
gitlab.rb (без учета комментариев):
egrep -v "^$|^[[:space:]]*[#;]" /srv/gitlab/config/gitlab.rb
gitlab_rails['smtp_enable'] = true
gitlab_rails['smtp_address'] = "smtp.mail.ru"
gitlab_rails['smtp_port'] = 465
gitlab_rails['smtp_authentication'] = "login"
gitlab_rails['smtp_tls'] = true
gitlab_rails['smtp_enable_starttls_auto'] = false
gitlab_rails['smtp_openssl_verify_mode'] = 'peer'
gitlab_rails['gitlab_email_from'] = 'noreply@example.com'
gitlab_rails['gitlab_email_reply_to'] = 'noreply@example.com'
gitlab_rails['omniauth_providers'] = [
{
name: "bitbucket",
app_id: "xxxxxxxxxxxxxxxxxx",
app_secret: "yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy",
url: "https://bitbucket.org/"
}
]
gitlab_rails['backup_keep_time'] = 604800
registry['enable'] = false
puma['worker_processes'] = 0
sidekiq['concurrency'] = 8
nginx['gzip_enabled'] = false
nginx['real_ip_trusted_addresses'] = ['172.16.0.0/12']
pages_external_url 'https://pages.example.com'
gitlab_pages['enable'] = true
gitlab_pages['listen_proxy'] = "0.0.0.0:8090"
gitlab_pages['namespace_in_path'] = true
pages_nginx['enable'] = false
prometheus_monitoring['enable'] = false
Напомню, что доступы к SMTP у меня хранятся в типа секретном файле /srv/gitlab/data/gitlab-rails/shared/encrypted_settings/smtp.yaml.enc.
Копия документации
https://docs.gitlab.com/administration/docs_self_host/
Здесь все просто – очередной /srv/gldocs/compose.yaml с учетом Traefik (хотя в принципе можно было бы просто объявить вторым сервисом GitLab):
services:
gitlab_docs:
image: registry.gitlab.com/gitlab-org/technical-writing/docs-gitlab-com/archives:18.1
restart: unless-stopped
labels:
- traefik.enable=true
- traefik.http.routers.gldocs.rule=Host(`gldocs.dmkos.ru`)
- traefik.http.routers.gldocs.service=gldocs
- traefik.http.routers.gldocs.middlewares=compress
- traefik.http.services.gldocs.loadbalancer.server.port=4000
networks:
default:
name: traefik
external: true
Я не переопределяю ссылку на документацию в самом GitLab (админ-панель, Settings – Preferences, Help page, Documentation pages URL). В свое время меня выбесили предупреждения о куках, а еще и редирект на текущую версию не работал. Впрочем все это видимо починили – сейчас работает, как положено.
GitLab Runner
По отработанной схеме создадим директории:
mkdir -p /srv/runner/config
cd /srv/runner
И compose.yaml по аналогии с предлагаемой в документации docker run
:
services:
gitlab-runner:
image: gitlab/gitlab-runner:latest
container_name: gitlab-runner
restart: always
volumes:
- ./config:/etc/gitlab-runner
- /var/run/docker.sock:/var/run/docker.sock:ro
Как обычно, пробрасываем в контейнер директорию конфигурации, а еще в данном случае необходим сокет Докера (я все-таки решил указать режим только для чтения). Подключать сервис к сети Трафика не нужно, он не должен быть доступен извне. Тут бы, конечно, тоже применить прокси для сокета, но у меня ничего не получилось – Докер вроде бы что-то делает, но runner потом не видит запущенные контейнеры или вообще отваливается с ошибкой 143 при подготовке окружения (хотя казалось бы, при чем тут профиль). А переключать бегуна в пользовательский режим тоже особого смысла нет, поскольку опять-таки нужен доступ к «розетке», а это в свою очередь означает довольно обширные привилегии.
Восстанавливаю .runner_system_id и config.toml в директорию config. В этих ваших интернетах предлагается в настройках самого бегуна в очередной раз пробросить несчастный сокет:
volumes = ["/cache", "/var/run/docker.sock:/var/run/docker.sock"]
Но по-моему необходимости в этом нет. По крайней мере в случае статических Pages все работает со стандартной настройкой volumes = ["/cache"]
, так что у себя ничего менять не стал.
Как обычно, docker compose up -d
, и через некоторое время сможем проверить:
docker exec -t gitlab-runner gitlab-runner list
docker exec -t gitlab-runner gitlab-runner verify
К сожалению программа по умолчанию видимо не задана, поэтому приходится дублировать имя контейнера (первый gitlab-runner) и имя команды (второй gitlab-runner). compress.compress
, да.
Но самое главное – перепроверить ранее настроенный CI/CD, в моем случае – публикацию Pages. Для этого делаем какой-нибудь коммит и наблюдаем Build – Jobs репозитория. Как ни странно, все получилось.
Резервное копирование
В принципе схема резервирования не особо отличается от случая с установкой GitLab непосредственно на хост. Необходимо вручную сохранить многочисленные compose.yaml (+ .env) и конфиги:
- /srv/gitlab/config
- /srv/gitlab/data/gitlab-rails/shared/encrypted_settings/smtp.yaml.enc (напомню, что я зачем-то шифрую доступы к SMTP-серверу)
- /srv/runner/config
- /srv/traefik/config (возможно кроме acme.json)
А с учетом Докера cron для архивирования GitLab как такового будет выглядеть так:
0 2 * * * /usr/bin/docker exec -t gitlab gitlab-backup create CRON=1
Путь к архивам теперь /srv/gitlab/data/backups.
rclone из apt почему-то не поддерживает облачный диск mega.nz, поэтому скачиваем и устанавливаем актуальную версию с сайта, например:
wget https://downloads.rclone.org/v1.70.2/rclone-v1.70.2-linux-amd64.deb
dpkg -i rclone-v1.70.2-linux-amd64.deb
Далее, как обычно, с помощью rclone config
настраиваю доступ, для определенности под именем meganz, и добавляю примерно такое задание:
0 4 * * * /usr/bin/rclone sync /srv/gitlab/data/backups meganz:/gitlab/backups
Обновление
Первоначально я указал конкретную версию GitLab, чтобы корректно восстановить данные со старого сервера. Останавливаем приложение и меняем метку образа на latest:
image: gitlab/gitlab-ce:latest
Запускаем обратно.
Непосредственно для обновления в документации предлагаются такие команды:
cd /srv/gitlab
docker compose pull
docker compose up -d
Разумеется, в идеале предварительно надо бы создать архив.
Аналогичным образом обновляется и бегун. С копией документации чуть сложнее – там нет такой метки. Посему: «вниз», меняем тег версии, «вверх». С точки зрения обновлений, конечно, менеджер пакетов проще – тот же dnf update
обновляет все сам. Теперь же придется самому каким-то образом отслеживать появление новых версий, а потом еще и удалять ненужные образы.
Заключение
Вот так я перенес GitLab с Oracle Linux 8 в Докер. Благодаря Traefik решилась проблема с сертификатами, а еще теперь можно смело размещать на сервере что-то помимо тануки. Хотя в общем случае это наверное неправильно.
Все еще остается открытым вопрос с реестром контейнеров. Как только, что называется, так сразу.
Категория: Решение проблем | Опубликовано 03.08.2025 | Редакция от 14.08.2025
Похожие материалы
Переезд реестра контейнеров GitLab в S3
Ранее я писал о MinIO в качестве хранилища реестра контейнеров, но в случае VDS/VPS такой вариант экономически не выгоден: чуть ли не на порядок дешевле воспользоваться услугой аренды S3 у какого-нибудь облачного провайдера. Что я и решил сделать, ведь место на сервере стало очень быстро заканчиваться. Заодно мигрируем прокси зависимостей, LFS и всякое такое.