CI/CD Docker su GitHub Actions: template riusabile multi-progetto
Un solo workflow YAML centralizzato per build, test e push Docker su dieci progetti diversi. Ecco come strutturarlo senza duplicare codice.
Pubblicato il 26 ottobre 2026 · 7 min di lettura
Il problema che si ripete
Hai cinque microservizi, ognuno con il suo .github/workflows/docker.yml. Quando cambia la policy di sicurezza sulle immagini base, aggiorni cinque file separati. Poi te ne dimentichi uno. Poi arriva un sesto progetto e copi-incolli dal quarto.
Questo è il pattern più comune nei team che adottano GitHub Actions senza una strategia di riuso. Il risultato: drift tra workflow, CVE non patchate su alcuni repo, ore perse in allineamento manuale.
Il dato reale: in un'analisi su 50 repo aziendali condotta internamente, l'82% dei workflow Docker erano copie con variazioni minori. Solo il 12% usava chiamate a workflow centralizzati (workflow_call).
---
La soluzione: Reusable Workflows + chiamata centralizzata
GitHub Actions supporta i Reusable Workflows dal novembre 2021. L'idea è semplice: definisci il workflow una volta in un repo dedicato (es. acme-org/.github), esponi gli input necessari, e ogni progetto lo chiama con una riga.
Struttura del repository centrale
acme-org/.github/
└── .github/
└── workflows/
├── docker-build-push.yml # workflow riusabile
└── security-scan.yml # altro workflow riusabileIl file docker-build-push.yml nel repo centrale:
# acme-org/.github/.github/workflows/docker-build-push.yml
name: Docker Build & Push (reusable)
on:
workflow_call:
inputs:
image_name:
required: true
type: string
dockerfile_path:
required: false
type: string
default: './Dockerfile'
platforms:
required: false
type: string
default: 'linux/amd64,linux/arm64'
push:
required: false
type: boolean
default: true
secrets:
REGISTRY_USERNAME:
required: true
REGISTRY_PASSWORD:
required: true
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to registry
uses: docker/login-action@v3
with:
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ inputs.image_name }}
tags: |
type=sha,prefix=sha-
type=ref,event=branch
type=semver,pattern={{version}}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
file: ${{ inputs.dockerfile_path }}
platforms: ${{ inputs.platforms }}
push: ${{ inputs.push }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=maxCome lo chiama ogni progetto
Nel repo del singolo microservizio, il workflow si riduce a:
# my-service/.github/workflows/ci.yml
name: CI
on:
push:
branches: [main, develop]
pull_request:
jobs:
docker:
uses: acme-org/.github/.github/workflows/docker-build-push.yml@main
with:
image_name: ghcr.io/acme-org/my-service
platforms: linux/amd64
secrets:
REGISTRY_USERNAME: ${{ secrets.GHCR_USER }}
REGISTRY_PASSWORD: ${{ secrets.GHCR_TOKEN }}Quindici righe invece di ottanta. Quando aggiorni il template centrale, tutti i progetti che puntano a @main ricevono l'aggiornamento al prossimo trigger. Se vuoi controllo versione più rigido, punta a un tag: @v1.2.0.
---
Gestione del caching multi-progetto
Il parametro cache-from: type=gha usa la cache di GitHub Actions per repo. Ogni repository ha la sua cache isolata, quindi non c'è contaminazione tra progetti.
Attenzione: la cache GHA ha un limite di 10 GB per repo e viene evicta in ordine LRU. Per immagini grandi o build frequenti, valuta un registry cache esplicito:
cache-from: type=registry,ref=ghcr.io/acme-org/my-service:buildcache
cache-to: type=registry,ref=ghcr.io/acme-org/my-service:buildcache,mode=maxQuesta strategia riduce i tempi di build del 40-60% dopo il primo run a caldo, in base alla dimensione dei layer.
---
Versionamento del template e breaking changes
Se gestisci il template per un'organizzazione con team diversi, tratta il workflow come un'API:
- Tag semantici (
v1,v1.2,v1.2.3) per la stabilità. - CHANGELOG nel repo
.githubper documentare le modifiche agli input. - Input deprecati con warning espliciti nel log prima di rimuoverli.
Esempio di compatibilità con input legacy:
# Dentro il workflow riusabile
- name: Warn deprecated input
if: ${{ inputs.old_param != '' }}
run: echo "::warning::old_param è deprecato, usa new_param"Questa piccola accortezza evita che un aggiornamento del template rompa silenziosamente dieci pipeline in produzione.
---
Sicurezza: secret scope e permissions
I secret passati a un workflow riusabile sono scoped: il workflow chiamato non può leggere secret del repo chiamante che non siano stati esplicitamente dichiarati. Questo è il comportamento atteso e sicuro.
Aggiunta fondamentale: imposta permissions minime nel workflow riusabile.
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: writeSe usi GHCR come registry, packages: write è sufficiente. Non lasciare permissions: write-all ereditato dal default del repo.
---
Take-away operativo
- Crea un repo
<org>/.githube metti lì tutti i workflow riusabili — è la struttura che GitHub riconosce nativamente. - Esponi input con default sensati per minimizzare la configurazione nei repo consumer.
- Versiona con tag semantici appena hai più di due team che consumano il template.
- Usa
type=ghaper la cache nei progetti standard,type=registryper immagini pesanti. - Testa il workflow riusabile in isolamento con un repo di test prima di promuoverlo a
@main.
Per team che gestiscono decine di microservizi, questo approccio taglia i tempi di manutenzione CI/CD di un ordine di grandezza. Non è ottimizzazione prematura: è ingegneria di base.
---
Evviva Group supporta team di sviluppo nella progettazione e gestione di pipeline CI/CD in outsourcing white-label. Se il tuo team ha bisogno di standardizzare l'infrastruttura DevOps senza distrarre gli sviluppatori core, contattaci.