Zwykle zaczyna się podobnie: kod leży na GitHubie albo w chmurowym GitLabie, ktoś buduje aplikację na własnym laptopie, a na produkcję trafia ona przez git pull po SSH. Dopóki programista jest jeden, to nawet działa. Potem dochodzi drugi i trzeci, pojawia się serwer testowy i nikt już nie wie na pewno, jaka wersja chodzi na produkcji ani kto ją tam wrzucił.

Druga połowa problemu to zależność od cudzej usługi. Limity minut CI, opłata za użytkownika rosnąca razem z zespołem, obrazy kontenerów w cudzym rejestrze. A część firm po prostu musi trzymać kod i artefakty na własnej infrastrukturze.

Stawiamy własnego GitLaba, konfigurujemy pipeline i rozdzielamy środowiska tak, żeby na produkcję trafiało tylko to, co przeszło build, testy i czyjąś akceptację.

Co robimy

  • GitLab CE na Waszym serwerze. Osobna maszyna wirtualna na Proxmoxie albo serwer dedykowany w OVH lub Hetznerze. Pakiet Omnibus, HTTPS, poczta, logowanie kluczami SSH, 2FA dla wszystkich.
  • Container Registry. Włączamy wbudowany rejestr obrazów. Runner buduje obraz i wypycha go do rejestru, a serwery pobierają go deploy-tokenem z prawem wyłącznie do odczytu rejestru.
  • Runnery. Docker executor do buildów i testów, każde zadanie w czystym kontenerze. Osobny runner produkcyjny, który bierze tylko chronione gałęzie i tagi. Shell executor tam, gdzie kontener przeszkadza, np. przy deployu Ansible’em.
  • Pipeline build → test → deploy. Budowa obrazu, testy, deploy: na serwer loguje się osobny użytkownik deploy, pobiera właściwy tag obrazu i restartuje docker compose.
  • Środowiska dev, stage, prod. Każde ma własne zmienne, własny serwer i własną historię wdrożeń w GitLabie. Dev aktualizuje się z każdym commitem w gałęzi roboczej, stage z main, prod z tagu wydania.
  • Review apps (opcjonalnie). Dla każdej gałęzi tymczasowa kopia aplikacji na osobnej subdomenie, sprzątana po merge’u.

Jak chronimy produkcję przed przypadkiem

Zasada jest prosta: nie da się wrzucić na produkcję „z gałęzi, żeby zobaczyć". W .gitlab-ci.yml wygląda to tak:

deploy_prod:
  stage: deploy
  tags: [prod]                 # tylko runner produkcyjny
  environment:
    name: production
  resource_group: production   # dwa deploye naraz nie pójdą
  rules:
    - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/
      when: manual             # przycisk klika człowiek
  script:
    - ssh deploy@$PROD_HOST "cd /opt/app && IMAGE_TAG=$CI_COMMIT_TAG docker compose pull && docker compose up -d"

Do tego chronione gałęzie i tagi: tag v* mogą tworzyć tylko maintainerzy. Środowisko production też jest chronione, więc przycisk deployu widzi wąskie grono osób. Runner produkcyjny jest zarejestrowany jako protected i nie bierze cudzych zadań.

Sekrety trzymamy w zmiennych CI/CD, nie w repozytorium. Oznaczamy je jako masked (nie wyciekają do logów) i protected (dostępne tylko na chronionych gałęziach). Zmienne przypisujemy do środowisk: DATABASE_URL dla stage i prod jest inny, a zadanie z gałęzi deweloperskiej hasła produkcyjnego po prostu nie zobaczy.

Backupy, aktualizacje i pułapki

Backupy. gitlab-backup create z crona, archiwum od razu kopiowane poza serwer. Pierwsza pułapka: w archiwum nie ma gitlab.rb ani gitlab-secrets.json. Bez tego drugiego po odtworzeniu nie odszyfrujecie zmiennych CI i tokenów. Dlatego zapisujemy je osobno i przynajmniej raz robimy próbne odtworzenie na czystej maszynie. Backup da się odtworzyć tylko na tej samej wersji GitLaba.

Aktualizacje. GitLab wychodzi co miesiąc i trzeba go aktualizować regularnie, bo łatki bezpieczeństwa idą bez przerwy. Ze starej wersji na najnowszą nie przeskoczycie w jednym kroku: są obowiązkowe wersje pośrednie (upgrade path), a po każdej trzeba poczekać na migracje w tle. Przed aktualizacją robimy snapshot VM. Po gitlab-ctl reconfigure sprawdzamy, czy wstały wszystkie usługi — zdarza się, że Sidekiq nie startuje za pierwszym razem i trzeba go podnieść ręcznie.

Monitoring. GitLab wystawia metryki dla Prometheusa. Pilnujemy pamięci, dysku (rejestr i artefakty rosną szybko, więc ustawiamy politykę czyszczenia starych obrazów), kolejek Sidekiqa i dostępności runnerów. Alerty trafiają do Grafany albo komunikatora.

Uczciwie o zasobach. GitLab jest ciężki. Na 4 GB RAM wystartuje tylko z okrojoną konfiguracją i będzie wyraźnie zwalniał. Dla małego zespołu planujcie co najmniej 8 GB i 4 rdzenie, plus osobne zasoby dla runnerów — buildów lepiej nie puszczać na tej samej maszynie.

Kiedy GitLab nie jest potrzebny. Dwóch programistów, jeden projekt, proste CI? Zobaczcie Gitea albo Forgejo. Mieszczą się w kilkuset megabajtach pamięci, mają własny rejestr i CI zgodne z GitHub Actions. Powiemy to wprost, jeśli uznamy, że GitLab to dla Was za dużo.

Co dostajecie

  • GitLaba na Waszym serwerze, z dostępem administratora dla Was, nie dla nas.
  • Container Registry z polityką czyszczenia starych obrazów.
  • Runnery: wspólne do buildów i osobny, chroniony dla produkcji.
  • Gotowy .gitlab-ci.yml z etapami build → test → deploy i szablon dla nowych projektów.
  • Środowiska dev, stage, prod z osobnymi zmiennymi, chronionymi gałęziami i ręcznym zatwierdzaniem wydań.
  • Backupy GitLaba razem z plikami konfiguracji i sprawdzone odtwarzanie.
  • Monitoring i alerty na pamięć, dysk, kolejki i runnery.
  • Dokumentację: jak dodać projekt, wypuścić wydanie, zrobić rollback i zaktualizować GitLaba.

Jeśli dziś wdrażacie „ręcznie po SSH" albo rachunek za chmurowe CI rośnie szybciej niż zespół — napiszcie do nas. Powiedzcie, ilu macie ludzi i projektów, a zaproponujemy, co postawić: GitLaba czy coś lżejszego.