GitLab Runner: Budowanie obrazów Docker

Efektywne budowanie i wysyłanie obrazów Docker z pipeline’ów GitLab CI/CD przy użyciu runnerów Stackhero i Docker-in-Docker

👋 Witamy w dokumentacji Stackhero!

Stackhero oferuje prostą usługę GitLab Runner cloud, która umożliwia wydajne i bezproblemowe uruchamianie zadań GitLab CI/CD. Oto, czego możesz się spodziewać:

  • Nielimitowane minuty CI/CD: uruchamiaj swoje pipeline'y tak często, jak potrzebujesz, bez rozliczania za minuty i nieprzewidzianych kosztów.
  • Wiele równoczesnych zadań: przyspiesz rozwój, uruchamiając kilka zadań jednocześnie.
  • Docker executor z obsługą Docker-in-Docker: łatwo buduj i wysyłaj obrazy kontenerów w ramach procesu CI/CD.
  • Działa bezproblemowo zarówno z GitLab.com, jak i z instancjami self-managed GitLab.
  • Prywatna, dedykowana infrastruktura z szybkim dyskiem NVMe/SSD zapewnia stabilną i przewidywalną wydajność budowania.
  • Dostępne w regionach 🇪🇺 Europa oraz 🇺🇸 USA, aby dopasować się do potrzeb Twojego zespołu.

Oszczędzaj czas: możesz podłączyć swojego pierwszego GitLab Runner i rozpocząć uruchamianie pipeline'ów w zaledwie kilka minut!

W przypadku Stackhero GitLab Runner każdy job uruchamiany jest w świeżym kontenerze z wykorzystaniem Docker executor. Możesz budować własne obrazy Docker bezpośrednio w swoim pipeline, włączając Docker-in-Docker (DinD). Ta konfiguracja uruchamia demona Docker równolegle z Twoim jobem, dzięki czemu możesz wykonywać polecenia docker build oraz docker push w ramach procesu CI/CD.

Każde uruchomienie korzysta z nielimitowanych minut CI/CD: możesz budować tak często, jak potrzebujesz, bez obaw o limity wykorzystania. Twój cache buildów jest przechowywany na dedykowanym dysku runnera, co pozwala na ponowne wykorzystanie wcześniejszych warstw podczas kolejnych buildów. Znacząco skraca to czas budowania i przyspiesza realizację pipeline’ów.

Możesz dodać poniższy przykładowy plik .gitlab-ci.yml do swojego repozytorium. Ta konfiguracja buduje Dockerfile znajdujący się w katalogu głównym projektu:

build-image:
  stage: build
  image: docker:29
  services:
    - name: docker:29-dind
      alias: docker
  variables:
    DOCKER_HOST: "tcp://docker:2375"
    DOCKER_TLS_CERTDIR: ""
  before_script:
    - docker info
  script:
    # Zamień "my-image" na wybraną nazwę:
    - docker build -t my-image .
    # Opcjonalnie, uruchom szybki test na zbudowanym obrazie:
    # - docker run --rm my-image /path/to/tests

W tym przykładzie używamy obrazu Docker w wersji 29. Możesz użyć nowszej wersji, jeśli jest dostępna. Najnowsze tagi znajdziesz na oficjalnej stronie obrazu Docker.

W tej konfiguracji serwis docker:29-dind uruchamia demona Docker obok Twojego joba, a DOCKER_HOST: "tcp://docker:2375" wskazuje CLI docker, aby korzystało z tego demona. Zawsze ustawiaj DOCKER_HOST, gdy deklarujesz serwis docker:dind: bez tego CLI po cichu połączy się z innym demonem, przez co build może przejść mimo błędnej konfiguracji serwisu, co ukrywa rzeczywiste problemy (i powoduje błędy w narzędziach takich jak Testcontainers, które polegają na tym serwisie). DOCKER_TLS_CERTDIR: "" oznacza połączenie przez zwykły, niezaszyfrowany port 2375 w wewnętrznej sieci joba.

Jako prostszą alternatywę możesz pominąć blok services i obie zmienne: runner Stackhero udostępnia gotowego do użycia demona Docker przez zamontowany socket, więc polecenie docker build działa bez dodatkowej konfiguracji.

Testcontainers to biblioteka testowa dostępna dla Java, Go, Node.js, Python, .NET i innych języków, która uruchamia rzeczywiste usługi jako tymczasowe kontenery Docker podczas wykonywania testów. Zamiast mockować bazę danych czy broker wiadomości, testy integracyjne komunikują się z prawdziwą instancją PostgreSQL, MySQL, Redis lub Kafka, utworzoną przed testami i usuwaną zaraz po nich. Rozwiązanie to jest szczególnie popularne w projektach Java i Spring.

Testcontainers wymaga demona Docker, więc działa dokładnie z tą samą konfiguracją Docker-in-Docker jak powyżej. Nie są potrzebne dodatkowe zmienne:

test:
  stage: test
  # Użyj obrazu wymaganego przez Twoje testy (tutaj JDK), niekoniecznie obrazu docker:
  image: gradle:jdk21
  services:
    - name: docker:29-dind
      alias: docker
  variables:
    DOCKER_HOST: "tcp://docker:2375"
    DOCKER_TLS_CERTDIR: ""
  script:
    - gradle test

Testcontainers odczytuje DOCKER_HOST, aby znaleźć demona, i używa tej samej nazwy hosta docker, aby uzyskać dostęp do portów publikowanych przez uruchamiane kontenery. Oba mechanizmy działają niezależnie, ponieważ alias docker jest rozpoznawany w sieci joba.

Zachowaj DOCKER_HOST w jobach korzystających z Testcontainers. Bez tej zmiennej Testcontainers użyje socketu Docker zamontowanego przez runnera, a następnie spróbuje połączyć się z kontenerami testowymi przez adres IP hosta, do którego job nie ma dostępu. Typowym objawem jest błąd kontenera pomocniczego Ryuk: Wait strategy failed. Container is removed lub Timed out waiting for log output matching '.*Started.*'.

GitLab udostępnia predefiniowane zmienne (CI_REGISTRY, CI_REGISTRY_USER, CI_REGISTRY_PASSWORD, CI_REGISTRY_IMAGE), dzięki którym pipeline może się uwierzytelnić i bezpiecznie wysyłać obrazy do rejestru kontenerów projektu. Nie są wymagane żadne dodatkowe sekrety.

Oto przykładowy job budujący i wysyłający obraz:

build-and-push:
  stage: build
  image: docker:29
  services:
    - name: docker:29-dind
      alias: docker
  variables:
    DOCKER_HOST: "tcp://docker:2375"
    DOCKER_TLS_CERTDIR: ""
  before_script:
    - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY"
  script:
    - docker build -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA" .
    - docker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA"
    # Jeśli jesteś na domyślnej gałęzi, możesz również otagować i wysłać "latest":
    - |
      if [ "$CI_COMMIT_BRANCH" = "$CI_DEFAULT_BRANCH" ]; then
        docker tag "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA" "$CI_REGISTRY_IMAGE:latest"
        docker push "$CI_REGISTRY_IMAGE:latest"
      fi

Aby wysłać obrazy do innego rejestru (np. Docker Hub lub prywatnego rejestru), możesz przechowywać dane uwierzytelniające jako zmienne CI/CD i użyć ich z docker login w ten sam sposób.

Dysk runnera jest utrzymywany pomiędzy pipeline’ami, co pozwala na ponowne wykorzystanie warstw obrazu jako cache buildów. Dzięki temu kolejne buildy są znacznie szybsze. Oto przykładowa konfiguracja:

build-cached:
  stage: build
  image: docker:29
  services:
    - name: docker:29-dind
      alias: docker
  variables:
    DOCKER_HOST: "tcp://docker:2375"
    DOCKER_TLS_CERTDIR: ""
  before_script:
    - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY"
  script:
    # Pobierz najnowszy obraz, aby zainicjować cache (jeśli istnieje):
    - docker pull "$CI_REGISTRY_IMAGE:latest" || true
    - docker build --cache-from "$CI_REGISTRY_IMAGE:latest" -t "$CI_REGISTRY_IMAGE:latest" .
    - docker push "$CI_REGISTRY_IMAGE:latest"

To podejście pozwala wykorzystać cache warstw Docker, więc przebudowywane są tylko nowe lub zmienione warstwy.

Twój plan określa, ile jobów może być uruchamianych jednocześnie. Joby w tej samej fazie startują razem, do limitu współbieżności. Oznacza to, że kilka niezależnych jobów może być wykonywanych równolegle i zakończyć się, gdy najwolniejszy z nich się skończy, zamiast czekać na zakończenie każdego po kolei.

Przykład:

stages:
  - test

unit:
  stage: test
  image: node:22
  script: npm run test:unit

integration:
  stage: test
  image: node:22
  script: npm run test:integration

e2e:
  stage: test
  image: node:22
  script: npm run test:e2e

Jeśli ustawisz współbieżność na 1 lub więcej, joby unit, integration i e2e zostaną uruchomione równocześnie.

Więcej informacji na temat budowania obrazów Docker w pipeline’ach GitLab CI/CD znajdziesz w oficjalnej dokumentacji GitLab dotyczącej używania Docker builds.