Configurazione della Pipeline CI/CD Che Funziona Davvero in Produzione
Modelli CI/CD collaudati per progetti reali — dai workflow di GitHub Actions alle strategie di deployment che non mandano in crash la produzione di venerdì.

La prima pipeline CI/CD che ho configurato per un progetto in produzione era un singolo workflow di GitHub Actions che eseguiva npm test e poi faceva il deployment su un VPS via SSH. Ha funzionato finché non ha smesso di farlo — un deployment fallito ha lasciato il server in uno stato semi-aggiornato un venerdì sera, e ho passato il weekend a ripristinare manualmente i file. Quell'esperienza mi ha insegnato che una pipeline di deployment non è solo "esegui i test e poi fai il deployment". È l'intero sistema di controlli, gate e meccanismi di rollback che si frappongono tra un git push e i tuoi utenti che vedono il nuovo codice.
Questo post copre l'architettura della pipeline che ho affinato attraverso molteplici progetti in produzione, con esempi concreti di GitHub Actions che puoi adattare.
Architettura della Pipeline
Una pipeline di produzione ha fasi distinte, e ogni fase ha uno scopo specifico. Saltare le fasi fa risparmiare minuti ora e costa ore dopo.
Le Fasi
Code Push
│
├─→ Fase 1: Validazione (lint, format, type-check)
│
├─→ Fase 2: Testing (unit, integration)
│
├─→ Fase 3: Build (compilazione, bundling, containerizzazione)
│
├─→ Fase 4: Deployment in Staging
│
├─→ Fase 5: Smoke Tests / E2E in Staging
│
└─→ Fase 6: Deployment in Produzione
Ogni fase agisce come un gate. Se la validazione fallisce, i test non vengono mai eseguiti. Se i test falliscono, il build non inizia mai. Questo risparmia tempo di calcolo e fornisce un feedback rapido — gli sviluppatori sanno entro 30 secondi se hanno dimenticato di eseguire il linter, invece di aspettare 8 minuti che una suite di test fallisca.
Strategia di Trigger
Non ogni push richiede la pipeline completa. Ecco la configurazione del trigger che utilizzo:
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
types: [opened, synchronize, reopened]
Le pull request eseguono le fasi 1-3 (validazione, test, build). Le merge su develop fanno il deployment in staging. Le merge su main fanno il deployment in produzione. Questo mantiene il feedback delle PR veloce, garantendo al contempo che i deployment avvengano solo da rami protetti.
Fase 1: Validazione
La validazione rileva le incongruenze di formattazione e gli errori di tipo prima che i test vengano eseguiti. Questi controlli sono veloci (meno di 30 secondi) e catturano i problemi più comuni.
jobs:
validate:
name: Valida
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Configura Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- name: Installa dipendenze
run: npm ci
- name: Controlla formattazione
run: npx prettier --check "src/**/*.{ts,tsx,js,jsx,json,css}"
- name: Lint
run: npx eslint src/ --max-warnings 0
- name: Controllo tipi
run: npx tsc --noEmit
La Regola --max-warnings 0
ESLint distingue tra errori (che fanno fallire il processo) e avvisi (che non lo fanno). Senza --max-warnings 0, i team accumulano centinaia di avvisi che tutti ignorano. Trattare gli avvisi come errori in CI costringe il team a risolverli o a disabilitare esplicitamente la regola. Nessuna via di mezzo.
Formattazione come Controllo CI, Non Solo un Suggerimento
Eseguire Prettier in CI (con --check, non --write) garantisce una formattazione coerente senza fare affidamento sul fatto che ogni sviluppatore abbia l'estensione dell'editor giusta. Se la formattazione fallisce in CI, lo sviluppatore esegue npx prettier --write . localmente e commette la correzione. Questo non è negoziabile — i dibattiti sulla formattazione finiscono quando uno strumento prende le decisioni.
Fase 2: Testing
Il testing è la spina dorsale della pipeline. Divido i test in job paralleli basati sul tipo per un feedback più rapido.
unit-tests:
name: Test Unitari
runs-on: ubuntu-latest
needs: validate
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run unit tests
run: npx vitest run --coverage --reporter=verbose
- name: Carica report di copertura
if: always()
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: coverage/
integration-tests:
name: Test di Integrazione
runs-on: ubuntu-latest
needs: validate
services:
postgres:
image: postgres:16
env:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: testdb
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis:7
ports:
- 6379:6379
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Esegui migrazioni database
run: npx prisma migrate deploy
env:
DATABASE_URL: postgresql://test:test@localhost:5432/testdb
- name: Esegui test di integrazione
run: npx vitest run --config vitest.integration.config.ts
env:
DATABASE_URL: postgresql://test:test@localhost:5432/testdb
REDIS_URL: redis://localhost:6379
Container di Servizio per i Test di Integrazione
I container di servizio di GitHub Actions sono sottoutilizzati. Invece di simulare il tuo database nei test di integrazione (che testano la simulazione, non il tuo codice), avvia un'istanza PostgreSQL reale. Il blocco services gestisce il ciclo di vita — il container si avvia prima dei tuoi test e si ferma dopo.
Questo aggiunge circa 15-20 secondi al job per l'avvio del container, ma la fiducia che ottieni testando contro un database reale ne vale la pena.
Esecuzione Parallela dei Test
I test unitari e i test di integrazione vengono eseguiti in parallelo (entrambi needs: validate, non needs: unit-tests). Questo riduce il tempo totale della pipeline. Se i tuoi test unitari richiedono 2 minuti e i test di integrazione 4 minuti, l'esecuzione parallela significa che aspetti 4 minuti invece di 6.
Fase 3: Build
La fase di build convalida che il progetto compili e produca artefatti deployabili.
build:
name: Build
runs-on: ubuntu-latest
needs: [unit-tests, integration-tests]
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Costruisci applicazione
run: npm run build
env:
NODE_ENV: production
- name: Carica artefatti di build
uses: actions/upload-artifact@v4
with:
name: build-output
path: dist/
retention-days: 7
Caching del Build
Per i deployment basati su Docker, il caching dei layer accelera drasticamente i build:
build-docker:
name: Costruisci Immagine Docker
runs-on: ubuntu-latest
needs: [unit-tests, integration-tests]
steps:
- uses: actions/checkout@v4
- name: Configura Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Accedi al Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Costruisci e pusha
uses: docker/build-push-action@v6
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: |
ghcr.io/${{ github.repository }}:${{ github.sha }}
ghcr.io/${{ github.repository }}:latest
cache-from: type=gha
cache-to: type=gha,mode=max
Il cache-from: type=gha utilizza la cache di GitHub Actions per memorizzare i layer Docker tra le esecuzioni. Per un'applicazione Node.js tipica, questo riduce il tempo di build da 3-4 minuti a 30-60 secondi per le modifiche solo alle dipendenze.
Best Practice Dockerfile per CI
La struttura del Dockerfile influisce direttamente sull'efficienza della cache di build. Ordina i layer dal meno frequentemente modificato al più frequentemente modificato:
FROM node:20-alpine AS base
# Dipendenze di sistema (cambiano raramente)
RUN apk add --no-cache libc6-compat
# Manifest dei pacchetti (cambiano quando cambiano le dipendenze)
WORKDIR /app
COPY package.json package-lock.json ./
# Installa dipendenze (cached a meno che i manifest non cambino)
FROM base AS deps
RUN npm ci --production
FROM base AS build
RUN npm ci
COPY . .
RUN npm run build
# Immagine di produzione
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
COPY --from=build /app/package.json ./
EXPOSE 3000
CMD ["node", "dist/server.js"]
I build multi-stage mantengono l'immagine finale piccola (nessuna dipendenza di sviluppo, nessun codice sorgente), e l'ordinamento dei layer assicura che npm ci venga eseguito solo quando package.json o package-lock.json cambiano.
Fase 4: Deployment in Staging
Il deployment in staging avviene automaticamente sulle merge al ramo develop. L'ambiente di staging dovrebbe rispecchiare la produzione il più fedelmente possibile — stessa infrastrutt
Progetti Correlati
RestoHub
I ristoranti smettono di perdere il 30% con Uber Eats — ottengono ordini, menu, sito web e sistema fedeltà in un'unica piattaforma. Un'esperienza completa stile Uber Eats, ma il ristorante tiene ogni centesimo.
TakeCare
Un solo infermiere ora monitora 250 pazienti da remoto — sostituendo telefonate manuali e visite domiciliari nei principali ospedali del Quebec. Attiva al Jewish General, al CHUM e al Douglas Mental Health Institute.