1,691 25/03/2026 07/04/2026 11 min

In grafica, il problema non è quasi mai il codice. È la ripetibilità del rilascio.

Un sito con anteprime SVG, export PNG e librerie di rendering può passare in staging e rompersi in produzione dopo un riavvio. Il sintomo è classico: file generati diversi, asset mancanti, cache incoerente, dipendenze cambiate senza volerlo.

Qui ha senso confrontare due approcci. Il primo è pipeline minima con artifact immutabili. Il secondo è deploy diretto con rollback rapido. Entrambi funzionano. Risolvono problemi diversi.

Note: l’esempio sotto usa una web app grafica con export lato server, build statica e servizio systemd. Non è un tutorial generico. Il punto è fermare il drift tra macchina di build e macchina di produzione.

Prerequisiti

  • GitLab CI attivo su un progetto con runner Linux.
  • Un VPS o server con systemd e una directory di deploy, per esempio /srv/grafica.
  • Uno script di build che produca un artefatto deterministico, per esempio dist.tar.zst.
  • Accesso SSH con chiave e utente dedicato al deploy.
  • Un servizio web o worker che legga i file rilasciati da una directory stabile.

Warning: se la build dipende da font installati a mano sul server, sei già dentro l’environment drift. La pipeline lo scoprirà troppo tardi.

Step 1: definire il flusso minimo di CI

Prima si decide cosa produrre. Poi si decide come distribuirlo. L’artefatto deve contenere tutto ciò che serve al runtime, o quasi tutto.

Per una web app grafica, un flusso minimale è questo:

  • installazione dipendenze;
  • build degli asset;
  • archiviazione dell’output come artifact;
  • deploy dell’artifact sul server;
  • switch atomico del symlink;
  • rollback sul release precedente.

Una pipeline essenziale in GitLab può essere così:

stages:
  - build
  - deploy

variables:
  RELEASES_DIR: /srv/grafica/releases
  CURRENT_DIR: /srv/grafica/current
  ARTIFACT_NAME: dist.tar.zst

build:
  stage: build
  image: node:20-bookworm
  script:
    - npm ci
    - npm run build
    - tar -I 'zstd -19' -cf $ARTIFACT_NAME dist
  artifacts:
    name: "$CI_COMMIT_SHORT_SHA"
    paths:
      - dist.tar.zst
    expire_in: 7 days
  only:
    - main

deploy:
  stage: deploy
  image: alpine:3.20
  needs:
    - job: build
      artifacts: true
  before_script:
    - apk add --no-cache openssh-client rsync zstd
    - eval $(ssh-agent -s)
    - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
    - mkdir -p ~/.ssh
    - chmod 700 ~/.ssh
    - ssh-keyscan -H "$DEPLOY_HOST" >> ~/.ssh/known_hosts
  script:
    - scp dist.tar.zst $DEPLOY_USER@$DEPLOY_HOST:/tmp/$CI_COMMIT_SHORT_SHA.tar.zst
    - ssh $DEPLOY_USER@$DEPLOY_HOST 'bash -s' << 'EOF'
      set -euo pipefail
      RELEASES_DIR=/srv/grafica/releases
      CURRENT_DIR=/srv/grafica/current
      RELEASE_ID='"$CI_COMMIT_SHORT_SHA"'
      mkdir -p "$RELEASES_DIR/$RELEASE_ID"
      tar -I zstd -xf "/tmp/$RELEASE_ID.tar.zst" -C "$RELEASES_DIR/$RELEASE_ID"
      ln -sfn "$RELEASES_DIR/$RELEASE_ID/dist" "$CURRENT_DIR"
      EOF
  only:
    - main

Questa versione è volutamente semplice. Però già elimina il problema principale: il server non ricostruisce nulla. Riceve un pacchetto già pronto.

Step 2: scegliere artifact o deploy diretto

Qui la differenza pratica è importante.

Approccio A: artifact immutabili

La build avviene in CI, in un ambiente controllato. Il risultato viene impacchettato e distribuito. In produzione non si installa nulla: si estraggono file e si punta un symlink alla release nuova.

Vantaggi:

  • ripetibilità alta;
  • stesso output tra staging e produzione;
  • rollback semplice: basta ripuntare il symlink;
  • meno dipendenze sul server;
  • debug più facile, perché ogni release è identificabile.

Limiti:

  • artifact più pesante;
  • serve disciplina sul packaging;
  • se il runtime richiede binari nativi o librerie di sistema, vanno inclusi o fissati con attenzione.

Approccio B: deploy diretto con rollback

Qui il server riceve i sorgenti o un checkout del repository e lancia la build in loco. È più rapido da impostare. È anche più fragile.

Vantaggi:

  • meno passaggi iniziali;
  • più semplice se il server è già l’ambiente di build;
  • utile per applicazioni piccole o tool interni.

Limiti:

  • drift quasi inevitabile nel tempo;
  • build non riproducibile se cambiano pacchetti, versioni o font;
  • rollback meno affidabile se il server ha già modificato lo stato;
  • più rischio di differenze tra macchine.

Regola pratica: se un rilascio fallito costa più di qualche minuto di setup, conviene l’artifact immutabile. Se invece il servizio è piccolo, poco critico e il rollback serve solo come rete di sicurezza, il deploy diretto può bastare.

Step 3: costruire un artifact davvero utile

Un artifact non deve essere solo un archivio di file. Deve essere una unità di rilascio. In pratica, deve contenere:

  • asset compilati;
  • manifest o hash di verifica;
  • eventuali file statici di runtime;
  • versione dell’app;
  • script di avvio o configurazione minima.

Per esempio, se la tua app genera rendering server-side, puoi impacchettare:

  • dist/ con frontend statico;
  • server/ con bundle Node o binario;
  • assets/ con template e icone;
  • release.json con commit, data e checksum.

Un esempio di file di versione:

{
  "commit": "a1b2c3d4",
  "pipeline_id": 1842,
  "built_at": "2026-03-25T10:15:00Z",
  "node": "20.11.1"
}

Questo aiuta quando devi capire quale release è in esecuzione. Se un export PNG fallisce, non cerchi nel buio. Sai esattamente cosa è stato distribuito.

Step 4: deploy atomico sul server

Il deploy diretto via copia nella directory attiva è una cattiva idea. È la ricetta perfetta per file parziali e stati intermedi. Meglio usare release versionate e un symlink stabile.

Struttura consigliata:

/srv/grafica/
  releases/
    a1b2c3d4/
    e5f6g7h8/
  current -> /srv/grafica/releases/a1b2c3d4/dist
  shared/
    logs/
    uploads/

Il flusso è questo:

  1. carichi il pacchetto in una directory temporanea;
  2. estrai in una nuova release;
  3. verifichi i file;
  4. agganci il symlink current alla release nuova;
  5. riavvii il servizio solo se necessario.

Con systemd, il restart può essere minimale:

[Unit]
Description=Grafica App
After=network.target

[Service]
Type=simple
User=grafica
WorkingDirectory=/srv/grafica/current
ExecStart=/usr/bin/node server/index.js
Restart=always
Environment=NODE_ENV=production

[Install]
WantedBy=multi-user.target

Se il servizio legge il codice o i file dalla directory current, il cambio di release è quasi istantaneo. Se qualcosa va male, torni alla release precedente con un solo comando.

Step 5: rollback rapido e affidabile

Il rollback non deve essere un evento eccezionale. Deve essere un’operazione prevista.

La forma più semplice è tenere le ultime N release e un file che indichi la release attiva. In caso di problema, si punta il symlink alla release precedente e si riavvia il servizio.

Esempio di script di rollback:

#!/usr/bin/env bash
set -euo pipefail

RELEASES_DIR=/srv/grafica/releases
CURRENT_LINK=/srv/grafica/current
PREVIOUS_RELEASE=$(ls -1t "$RELEASES_DIR" | sed -n '2p')

if [ -z "$PREVIOUS_RELEASE" ]; then
  echo "Nessuna release precedente disponibile"
  exit 1
fi

ln -sfn "$RELEASES_DIR/$PREVIOUS_RELEASE/dist" "$CURRENT_LINK"
systemctl restart grafica

Questo approccio è efficace solo se le release sono davvero immutabili. Se la directory viene toccata dopo il deploy, il rollback perde valore.

Tip: conserva almeno 3 release. Due sono il minimo operativo, tre sono più realistiche. Una release buona, una precedente e una di emergenza riducono molto il rischio di restare bloccati.

Step 6: gestire il drift tra build e produzione

Il drift nasce quando l’ambiente di produzione cambia senza passare dalla pipeline. Può succedere per tanti motivi:

  • aggiornamento di una libreria di sistema;
  • installazione manuale di un font o di un pacchetto;
  • modifica di un file di configurazione fuori Git;
  • cambio di versione di Node, Python, ImageMagick o Ghostscript;
  • cache persistenti non invalidate.

La contromisura è separare chiaramente tre cose:

  • build environment: dove compili e pacchetti;
  • runtime environment: dove esegui;
  • shared state: dati persistenti, upload, log.

Se la tua app grafica ha bisogno di font specifici per esportare correttamente i PDF, non installarli “a mano” sul server e basta. Versionali o includili nel pacchetto, oppure usa un container o una base immutabile. Altrimenti il prossimo riavvio potrà cambiare il risultato visivo senza che il codice sia cambiato.

Un controllo utile è salvare hash e versioni all’interno dell’artifact:

sha256sum dist.tar.zst > dist.tar.zst.sha256
node --version > build-node-version.txt
npm ls --prod --depth=0 > dependencies.txt

Così puoi confrontare rapidamente due release e capire se il problema è nel codice o nell’ambiente.

Step 7: deploy diretto con rollback, quando ha senso davvero

Il deploy diretto non è sbagliato in assoluto. È solo più adatto a certi contesti.

Ha senso quando:

  • il servizio è interno e non critico;
  • il server è già l’ambiente di esecuzione e non vuoi duplicarlo in CI;
  • il runtime è semplice e stabile;
  • la build è veloce e poco dipendente da componenti esterni;
  • hai bisogno di iterare rapidamente durante una fase iniziale.

In questo modello, la pipeline può limitarsi a fare SSH sul server e lanciare una sequenza controllata:

stages:
  - deploy

deploy:
  stage: deploy
  image: alpine:3.20
  before_script:
    - apk add --no-cache openssh-client
    - eval $(ssh-agent -s)
    - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
    - mkdir -p ~/.ssh
    - ssh-keyscan -H "$DEPLOY_HOST" >> ~/.ssh/known_hosts
  script:
    - ssh $DEPLOY_USER@$DEPLOY_HOST 'bash -s' < scripts/deploy-and-restart.sh
  only:
    - main

Lo script sul server può fare checkout, installazione, test e avvio. Ma qui devi accettare il rischio: il server è parte della build, quindi il suo stato influenza il risultato.

Per limitare i danni, almeno:

  • fissa le versioni dei pacchetti;
  • usa lockfile;
  • pulisci le directory temporanee;
  • salva una release precedente prima di aggiornare;
  • verifica il servizio prima di dichiarare il deploy riuscito.

Step 8: esempio concreto di rollback su deploy diretto

Se vuoi restare sul deploy diretto, il rollback deve essere automatico o quasi. Un pattern utile è questo:

  1. salvi lo stato corrente;
  2. esegui il deploy nuovo;
  3. lanci un health check;
  4. se fallisce, torni indietro.

Esempio:

#!/usr/bin/env bash
set -euo pipefail

APP_DIR=/srv/grafica/app
BACKUP_DIR=/srv/grafica/backups
STAMP=$(date +%Y%m%d%H%M%S)

mkdir -p "$BACKUP_DIR"
cp -a "$APP_DIR" "$BACKUP_DIR/$STAMP"

cd "$APP_DIR"
git fetch origin main
git reset --hard origin/main
npm ci
npm run build
systemctl restart grafica

if ! curl -fsS http://127.0.0.1:3000/health; then
  echo "Health check fallito, rollback"
  rsync -a --delete "$BACKUP_DIR/$STAMP/" "$APP_DIR/"
  systemctl restart grafica
  exit 1
fi

Funziona, ma è più fragile di un artifact immutabile. Il backup può non bastare se il deploy modifica anche database, cache o migrazioni. Per questo il rollback diretto va bene solo se il perimetro del cambiamento è piccolo.

Step 9: cosa scegliere in pratica

La scelta dipende dal livello di rischio e dalla complessità della tua app.

Scegli artifact immutabili se:

  • hai export grafici o rendering sensibili a font e librerie;
  • vuoi release ripetibili e auditabili;
  • hai più ambienti e vuoi lo stesso output ovunque;
  • ti serve rollback affidabile in pochi secondi;
  • stai già vedendo differenze inspiegabili tra staging e produzione.

Scegli deploy diretto con rollback se:

  • il progetto è piccolo o in fase di prototipo;
  • il server è già standardizzato e controllato;
  • il costo di preparare artifact e storage non vale il guadagno;
  • accetti un po’ di drift in cambio di velocità operativa.

In altre parole: l’artifact è la scelta giusta quando la ripetibilità conta più della semplicità iniziale. Il deploy diretto è la scelta giusta quando vuoi muoverti in fretta e il rischio è contenuto.

Step 10: una configurazione consigliata per partire bene

Se il caso d’uso è una web app grafica con export server-side, la configurazione che in genere regge meglio è questa:

  • build in GitLab CI dentro un’immagine fissata;
  • artifact compresso con checksum;
  • deploy via SSH su release directory versionata;
  • symlink current come punto stabile;
  • health check dopo il cambio release;
  • conservazione delle ultime 3 o 5 release;
  • rollback con semplice ripuntamento del symlink.

Questa combinazione riduce il drift senza complicare troppo la pipeline. E soprattutto rende il problema visibile: se qualcosa si rompe, sai dove guardare.

Conclusione

Il vero vantaggio di GitLab CI non è “fare deploy”. È rendere il rilascio una procedura controllata.

Con artifact immutabili, il server smette di costruire e inizia solo a eseguire. Con il deploy diretto, puoi andare veloce ma devi accettare più variabilità e investire in rollback.

Per progetti grafici, dove font, librerie e output visivo sono delicati, la pipeline minima con artifact è spesso la scelta più solida. Il deploy diretto resta utile, ma solo se il rischio è basso e il rollback è davvero pronto all’uso.

Se vuoi una regola finale molto semplice: quando il risultato deve essere identico, distribuisci un artifact; quando il risultato può tollerare variazioni e ti serve rapidità, fai deploy diretto con rollback.