docker build && docker push z własnego laptopa działa dopóki ktoś inny nie musi opublikować tego samego obrazu albo dopóki nie trzeba tego robić przy każdym merge’u bez ciebie przy klawiaturze. GitHub Actions wykonuje ten sam build na serwerach GitHub, nadaje obrazowi tag na podstawie tego, co uruchomiło workflow, i publikuje go w registry, z którego twój krok deploy może zrobić pull. Nic nie dzieje się lokalnie.

Co jest potrzebne, żeby zbudować i opublikować obraz Docker z poziomu CI

Pięć elementów musi być na miejscu, zanim runner będzie mógł opublikować obraz:

  • actions/checkout na build context: twój Dockerfile i kod źródłowy
  • docker/setup-buildx-action na builder BuildKit, bo runner ma domyślnie tylko Docker Engine, a ten nie potrafi eksportować cache warstw ani budować dla innych platform
  • docker/login-action na dane logowania do registry
  • docker/metadata-action, żeby zamienić referencję Git na tagi i etykiety OCI
  • docker/build-push-action, żeby wykonać build i push w jednym wywołaniu

Docker utrzymuje wszystkie pięć, i są napisane tak, żeby się ze sobą łączyć: wyjścia kroku metadata trafiają wprost do wejść kroku build. Pod spodem to ten sam docker build i docker push, który uruchamiasz ręcznie, tylko przeniesiony na runner i podpięty pod eventy Git.

Dlaczego runner potrzebuje Buildx, zanim cokolwiek zbuduje

ubuntu-latest ma wbudowany Docker Engine, a Docker Engine buduje obrazy bez problemu za pomocą docker build. Czego nie potrafi zrobić samodzielnie, to wyeksportować cache z etapu build gdziekolwiek poza lokalnym dyskiem, albo zbudować obrazu dla innej platformy niż ta, na której działa runner. docker/setup-buildx-action tworzy buildera Buildx oparty na BuildKit i ustawia go jako domyślny, co dopiero umożliwia cache-to/cache-from oraz buildy multi-platformowe, opisane dalej w artykule.

1
2
- name: Set up Docker Buildx
  uses: docker/setup-buildx-action@v4

Jeden krok, bez potrzebnych wejść w typowym przypadku. Jeśli go pominiesz, build-push-action i tak zadziała, korzystając z buildera, który runner już ma — build się powiedzie, ale eksport cache i multi-arch odpadają.

Jak uwierzytelnić się w GHCR wbudowanym tokenem

Każde uruchomienie workflow dostaje krótkotrwały GITHUB_TOKEN, ograniczony do repozytorium, w którym działa. Do publikowania w GitHub Container Registry (ghcr.io) wystarczy ten token — nie musisz tworzyć ani rotować personal access tokena, nie potrzebujesz też żadnego secret.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
permissions:
  contents: read
  packages: write

steps:
  - name: Log in to GHCR
    uses: docker/login-action@v4
    with:
      registry: ghcr.io
      username: ${{ github.actor }}
      password: ${{ secrets.GITHUB_TOKEN }}

Blok permissions liczy się tyle samo co krok logowania. Bez packages: write token, który GitHub przekazuje workflow, potrafi czytać, ale nie publikować, więc logowanie się udaje, a push kończy się błędem 403. W repozytorium organizacji jest jeszcze jedno miejsce do sprawdzenia: ustawienie uprawnień workflow w Settings → Actions → General może ograniczyć tokeny do samego odczytu niezależnie od tego, co żąda plik workflow. Jeśli push nadal zawodzi mimo powyższego bloku, sprawdź to ustawienie.

Jak automatycznie tagować obrazy na podstawie referencji Git

Wpisanie na sztywno wartości tag w pliku workflow oznacza edycję YAML-a przy każdym wydaniu release. docker/metadata-action czyta event, który uruchomił dany przebieg (który branch, który tag, który PR) i sam generuje tagi oraz etykiety OCI.

1
2
3
4
5
6
7
8
9
- name: Extract metadata
  id: meta
  uses: docker/metadata-action@v6
  with:
    images: ghcr.io/${{ github.repository }}
    tags: |
      type=semver,pattern={{version}}
      type=raw,value=latest,enable={{is_default_branch}}
      type=sha,format=short

Publikujesz tag v1.4.0 i reguła semver generuje 1.4.0. Robisz push na main i reguła raw dodaje latest. Każdy build dostaje tag oparty na short-SHA niezależnie od wyzwalacza, więc konkretny commit zostaje dostępny do pull nawet gdy latest wskazuje już coś innego. steps.meta.outputs.tags to tagi rozdzielone znakiem nowej linii — wygenerowane przez te trzy reguły dla jednego, konkretnego eventu, a nie suma wszystkich trzech ze wszystkich przebiegów.

Jak wygląda pełny workflow

Połącz wszystkie elementy i dodaj docker/build-push-action, który wykonuje docker build i docker push w jednym wywołaniu BuildKit:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
name: Build and push image

on:
  push:
    branches: [main]
    tags: ["v*.*.*"]

permissions:
  contents: read
  packages: write

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  build-and-push:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v7

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v4

      - name: Log in to GHCR
        uses: docker/login-action@v4
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v6
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=semver,pattern={{version}}
            type=raw,value=latest,enable={{is_default_branch}}
            type=sha,format=short

      - name: Build and push
        uses: docker/build-push-action@v7
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

context: . buduje z repozytorium, które właśnie pobrano, korzystając z Dockerfile, który tam znajdzie; wskaż file: na inną ścieżkę, jeśli twój leży gdzie indziej. To, co trafia do środka obrazu, wciąż zależy od Dockerfile — od kolejności warstw po nie-rootowy USER. Ten workflow decyduje tylko o tym, kiedy build się uruchamia i dokąd trafia wynik.

Jedna rzecz do ustawienia od razu: jeśli sam build potrzebuje danych uwierzytelniających, na przykład tokena do prywatnego registry npm, przekaż go przez wejście secrets: akcji, a nie przez build-arg, który zostaje widoczny w historii obrazu. Zmienne Środowiskowe i Secrets w Kontenerach Docker opisuje tę różnicę dokładniej.

Jak cache’ować warstwy między przebiegami workflow

Runner hostowany przez GitHub startuje za każdym razem od czystego stanu. Bez zewnętrznego cache każda warstwa buduje się od nowa przy każdym przebiegu, RUN npm ci włącznie. cache-from: type=gha i cache-to: type=gha,mode=max w kroku powyżej mówią BuildKit, żeby czytał i zapisywał swój cache przez usługę cache GitHub Actions zamiast na lokalnym dysku, dzięki czemu cache przeżywa runner.

mode=max cache’uje każdą warstwę pośrednią, łącznie z tymi z wcześniejszych etapów Dockerfile multi-stage. Domyślne mode=min zachowuje tylko warstwy, które trafiają do finalnego obrazu, co oznacza, że etap build w Dockerfile multi-stage kompiluje się od zera przy każdym przebiegu.

Cache Actions ma limit rozmiaru na repozytorium i usuwa najstarsze wpisy logiką LRU. Dla większości obrazów aplikacyjnych nigdy nie staje się to problemem. Repozytorium budujące kilka dużych obrazów multi-arch może ten limit osiągnąć, a wtedy wyjściem jest cache oparty na registry (cache-to: type=registry,ref=ghcr.io/org/app:buildcache), bo nie jest ograniczony limitem cache Actions.

Jak budować przy każdym push, ale publikować tylko przy release

Powyższy workflow publikuje przy każdym push na main oraz gdy pojawi się nowy tag wersji, co jest właściwym domyślnym ustawieniem dla projektu wdrażanego z main. Bardziej ostrożna konfiguracja buduje i testuje każdy pull request bez niczego nie publikując, a publikuje dopiero po merge’u:

1
2
3
4
5
6
7
8
9
on:
  pull_request:
  push:
    branches: [main]
    tags: ["v*.*.*"]

# w kroku build-push-action:
with:
  push: ${{ github.event_name != 'pull_request' }}

Pełny build i tak uruchamia się na PR, więc zepsuty Dockerfile zawodzi przed merge’em, a push: false pomija zapis do registry. Gdy cache-to: type=gha jest już na miejscu, ten build z PR przygotowuje też cache, z którego skorzysta push po merge’u.

Co się zmienia, gdy publikujesz do Docker Hub zamiast GHCR

Docker Hub wymaga innego argumentu registry i innych danych logowania: access tokena z Account Settings → Security, a nie hasła do konta, którego Docker Hub już nie akceptuje przy logowaniu przez API.

1
2
3
4
5
6
7
8
- name: Log in to Docker Hub
  uses: docker/login-action@v4
  with:
    username: ${{ vars.DOCKERHUB_USERNAME }}
    password: ${{ secrets.DOCKERHUB_TOKEN }}

# wejście images: w metadata-action zmienia się na:
images: docker.io/yourusername/app

Konfiguracja Buildx, ekstrakcja metadata i krok build-push się nie zmieniają. Registry to parametr, a nie inny pipeline.

Czego ten workflow nie obejmuje

Obrazy multi-platformowe (linux/amd64 plus linux/arm64 w jednym manifeście) wymagają docker/setup-qemu-action przed Buildx oraz wejścia platforms: w kroku build. Emulacja QEMU sprawia, że runner amd64 buduje arm64 wolno — często kilkukrotnie wolniej niż natywnie. Jeśli budujesz arm64 regularnie, runnery arm64 hostowane przez GitHub całkowicie eliminują emulację.

Runnery self-hosted, które mają już skonfigurowany Buildx, nie potrzebują setup-buildx-action, a taki runner z trwałym builderem potrafi pobić backend cache Actions, trzymając warstwy na lokalnym dysku między przebiegami. Monorepo publikujące trzy obrazy usług zwykle przechodzi na matrix build zamiast trzech kopii tego joba, co zmienia sposób parametryzacji context i images, ale nie same kroki.

Jak przetestować workflow, zanim otagujesz release

Zrób push najpierw na branch, nie na tag wersji. Obejrzyj przebieg w zakładce Actions, potem sprawdź obraz na stronie Packages swojego repozytorium albo zrób pull bezpośrednio przez docker pull ghcr.io/<owner>/<repo>:<short-sha>, wstawiając wartość tag z logu przebiegu. Gdy ten pull zadziała, release to ten sam git tag v1.0.0 && git push --tags, którego już używasz — resztą zajmuje się workflow.

Powiązane artykuły