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/checkoutna build context: twój Dockerfile i kod źródłowydocker/setup-buildx-actionna builder BuildKit, bo runner ma domyślnie tylko Docker Engine, a ten nie potrafi eksportować cache warstw ani budować dla innych platformdocker/login-actionna dane logowania do registrydocker/metadata-action, żeby zamienić referencję Git na tagi i etykiety OCIdocker/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.
| |
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.
| |
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.
| |
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:
| |
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:
| |
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.
| |
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.