docker build && docker push dal tuo portatile funziona finchĂ© qualcun altro non deve pubblicare la stessa immagine, oppure finchĂ© non deve succedere a ogni merge senza che tu sia davanti alla tastiera. GitHub Actions esegue quella build sui runner di GitHub, tagga l’immagine in base a cosa ha scatenato il workflow e la pubblica su un registry da cui il tuo step di deploy può fare il pull. Niente gira in locale.

Cosa serve per costruire e pubblicare un’immagine Docker dalla CI

Cinque elementi devono essere al loro posto prima che un runner possa pubblicare un’immagine:

  • actions/checkout per il build context: il tuo Dockerfile e il tuo codice sorgente
  • docker/setup-buildx-action per un builder BuildKit, perchĂ© il Docker Engine di serie sul runner non può esportare la cache dei layer nĂ© costruire per altre piattaforme
  • docker/login-action per le credenziali del registry
  • docker/metadata-action per trasformare il ref Git in una lista di tag e in etichette OCI
  • docker/build-push-action per eseguire build e push in un’unica invocazione

Docker mantiene tutti e cinque, e sono scritti per comporsi tra loro: gli output dello step di metadata entrano direttamente negli input dello step di build. Sotto il cofano è lo stesso docker build e docker push che lanci a mano, spostato su un runner e collegato agli eventi Git.

Perché il runner ha bisogno di Buildx prima di poter costruire qualcosa

ubuntu-latest include Docker Engine, e Docker Engine costruisce immagini senza problemi con docker build. Quello che non può fare da solo è esportare una cache di build altrove che sul disco locale, o costruire per una piattaforma diversa da quella del runner. docker/setup-buildx-action crea un builder Buildx basato su BuildKit e lo imposta come predefinito, il che rende possibili cache-to/cache-from e le build multi-piattaforma più avanti in questa guida.

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

Un solo step, senza input necessari nel caso comune. Se lo salti, build-push-action gira comunque, usando qualsiasi builder il runner abbia giĂ  — la build riesce, ma l’esportazione della cache e il multi-arch restano fuori portata.

Come autenticarsi a GHCR con il token integrato

Ogni esecuzione del workflow riceve un GITHUB_TOKEN di breve durata, con lo scope limitato al repository in cui gira. Per pubblicare sulla GitHub Container Registry (ghcr.io), quel token basta: nessun personal access token, nessun secret da creare o ruotare.

 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 }}

Il blocco permissions conta tanto quanto lo step di login. Senza packages: write, il token che GitHub consegna al workflow può leggere ma non pubblicare, quindi il login riesce e il push fallisce con un 403. Su un repository di organizzazione c’è un secondo posto da controllare: un’impostazione dei permessi dei workflow sotto Settings → Actions → General può limitare i token alla sola lettura indipendentemente da cosa chiede il file del workflow. Se il push continua a fallire con il blocco sopra giĂ  in posizione, controlla lì.

Come taggare le immagini automaticamente dal ref Git

Scrivere un tag fisso nel file del workflow significa modificare lo YAML ogni volta che tagli una release. docker/metadata-action legge l’evento che ha scatenato l’esecuzione (quale branch, quale tag, quale PR) e produce la lista dei tag e le etichette OCI al posto tuo.

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

Pubblichi un tag v1.4.0 e la regola semver produce 1.4.0. Fai push su main e la regola raw aggiunge latest. Ogni build riceve un tag basato sullo short-SHA a prescindere dal trigger, così un commit specifico resta scaricabile anche dopo che latest è andato avanti. steps.meta.outputs.tags è una lista di tag separati da newline prodotta da queste tre regole per questo singolo evento, non l’unione di tutte e tre attraverso ogni esecuzione.

Come si presenta il workflow completo

Metti insieme i pezzi e aggiungi docker/build-push-action, che esegue docker build e docker push in un’unica chiamata 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: . costruisce dal repository appena fatto checkout usando il Dockerfile che trova lì; punta file: a un altro percorso se il tuo si trova altrove. Cosa finisce dentro l’immagine resta comunque compito del Dockerfile, dall’ordine dei layer a uno USER non root. Questo workflow decide solo quando gira la build e dove finisce il risultato.

Se la build stessa ha bisogno di una credenziale, ad esempio un token per un registry npm privato, passala attraverso l’input secrets: dell’action invece che con un build-arg, che resta visibile nella storia dell’immagine. Variabili d’Ambiente e Secrets in Docker approfondisce questa differenza.

Come mettere in cache i layer tra un’esecuzione e l’altra del workflow

Un runner ospitato da GitHub parte pulito ogni volta. Senza una cache esterna, ogni layer si ricostruisce a ogni esecuzione, RUN npm ci incluso. cache-from: type=gha e cache-to: type=gha,mode=max nello step sopra dicono a BuildKit di leggere e scrivere la sua cache attraverso il servizio cache di GitHub Actions invece che sul disco locale, così la cache sopravvive al runner.

mode=max mette in cache ogni layer intermedio, compresi quelli degli stage precedenti di un Dockerfile multi-stage. Il default, mode=min, tiene solo i layer che finiscono nell’immagine finale, il che significa che lo stage di build di un Dockerfile multi-stage viene ricompilato da zero a ogni esecuzione.

La cache di Actions ha un tetto di dimensione per repository ed elimina le voci piĂą vecchie con logica LRU. Per la maggior parte delle immagini applicative questo non è mai un problema. Un repository che costruisce diverse immagini multi-arch grandi può raggiungerlo, e una cache basata su registry (cache-to: type=registry,ref=ghcr.io/org/app:buildcache) è la via d’uscita, perchĂ© non è limitata dalla quota della cache di Actions.

Come costruire a ogni push ma pubblicare solo alla release

Il workflow sopra pubblica a ogni push su main e a ogni tag di versione, che è il default giusto per un progetto che fa deploy da main. Una configurazione più prudente costruisce e testa ogni pull request senza pubblicare nulla, e pubblica solo dopo un merge:

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

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

La build completa gira comunque sulla PR, così un Dockerfile rotto fallisce prima del merge, mentre push: false salta la scrittura sul registry. Con cache-to: type=gha già in posizione, quella build sulla PR prepara anche la cache che il push post-merge riutilizza.

Cosa cambia se pubblichi su Docker Hub invece di GHCR

Docker Hub richiede un argomento di registry diverso e credenziali diverse: un access token da Account Settings → Security, non la password del tuo account, che Docker Hub non accetta più per i login via 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 }}

# nell'input images: di metadata-action diventa:
images: docker.io/yourusername/app

Il setup di Buildx, l’estrazione dei metadata e lo step di build-push non cambiano. Il registry è un parametro, non una pipeline diversa.

Cosa questo workflow non copre

Le immagini multi-piattaforma (linux/amd64 piĂą linux/arm64 in un unico manifest) richiedono docker/setup-qemu-action prima di Buildx e un input platforms: nello step di build. L’emulazione QEMU rende una build arm64 su un runner amd64 lenta, spesso diverse volte piĂą lenta del nativo. Se costruisci arm64 regolarmente, i runner arm64 ospitati da GitHub eliminano del tutto l’emulazione.

I runner self-hosted che hanno giĂ  Buildx configurato non hanno bisogno di setup-buildx-action, e un builder persistente lì può battere il backend della cache di Actions tenendo i layer su disco locale tra un’esecuzione e l’altra. Un monorepo che pubblica tre immagini di servizio di solito passa a una matrix di build invece di tre copie di questo job, il che cambia come vengono parametrizzati context e images ma non gli step in sĂ©.

Come testare il workflow prima di taggare una release

Fai push su un branch prima, non su un tag di versione. Guarda l’esecuzione nella tab Actions, poi controlla l’immagine nella pagina Packages del tuo repository, oppure fai il pull direttamente con docker pull ghcr.io/<owner>/<repo>:<short-sha> usando il tag preso dal log dell’esecuzione. Una volta che quel pull funziona, una release è lo stesso git tag v1.0.0 && git push --tags che giĂ  usi, e il workflow prende il resto da lì.

Articoli correlati