GitHub Actions per il deploy di applicazioni Laravel su VPS unmanaged

GitHub Actions per il deploy di applicazioni Laravel su VPS unmanaged

Il 25 novembre 2024 sono stato ingaggiato come subentrante tecnico da un'azienda modenese attiva nel settore distribuzione di attrezzatura specializzata per fisioterapisti e strutture sanitarie private - fatturato annuo di circa 5,8 milioni di euro, 17 dipendenti, un e-commerce B2B in Laravel 10 che serve circa 2.400 clienti professionisti attivi con ordini ricorrenti. Il precedente freelance che aveva costruito e gestito la piattaforma per cinque anni era improvvisamente uscito di scena senza passaggio di consegne, e il proprietario dell'azienda aveva ottenuto accesso tecnico alla macchina di produzione (un Hetzner CPX31 a Helsinki) solo grazie a un vecchio accesso residuo di emergenza dimenticato nei log. La situazione tecnica che ho trovato dopo la prima giornata di audit era la seguente: repository GitLab self-hosted sul server di produzione stesso (sì, sulla stessa macchina che lo stava servendo), deploy in produzione eseguito via uno script bash di 120 righe invocato a mano via SSH dal freelance dopo ogni modifica, con passaggi che includevano upload FTP di singoli file specifici, modifica diretta del .env in produzione a mano con nano, esecuzione manuale di migration database, riavvio manuale del PHP-FPM. Zero testing automatico, zero staging, zero rollback procedure. Il freelance deployava in media una volta alla settimana, sempre di venerdì pomeriggio (cosa che avrebbe dovuto già suggerire qualcosa), e ogni deploy comportava un downtime effettivo di 8-15 minuti durante i quali il sito restituiva errori HTTP 500.

La priorità immediata che mi ha dato il proprietario era costruire una pipeline CI/CD strutturata con GitHub Actions - il team interno aveva appena migrato tutti i repository verso GitHub Cloud come parte della modernizzazione post-subentro - che permettesse deploy controllati, sicuri, rapidi, senza il livello di rischio del sistema manuale precedente. In quattro giornate di lavoro distribuite in due settimane ho costruito la pipeline end-to-end, includendo test automatici, analisi statica, build dell'artifact, deploy via SSH con pattern di blue-green semplificato, rollback automatico in caso di failure post-deploy, notifiche Slack al team per ogni deploy. Nei sei mesi successivi al go-live della pipeline, il team (che nel frattempo era cresciuto a tre sviluppatori interni che prima non esistevano) ha eseguito 148 deploy in produzione - circa 6 a settimana contro l'uno originale - con zero downtime utente misurato e zero rollback necessari. Il costo consulenziale dell'intervento è stato 3.200 euro. Il valore stimato dal proprietario sulla velocità di delivery delle nuove feature è di circa 45.000 euro l'anno in ore di sviluppo liberate dalla burocrazia del deploy manuale.

Questo articolo descrive il pattern concreto con cui costruisco pipeline GitHub Actions per deploy di Laravel su VPS unmanaged (Hetzner, Digital Ocean, OVH, Contabo) in contesti PMI italiane, basato sull'esperienza di circa 40 progetti simili negli ultimi cinque anni. Il principio guida è uno: una pipeline CI/CD per PMI non deve essere "Netflix-grade" - deve essere affidabile, comprensibile, modificabile senza terrore dal team interno. Le pipeline sovraingegnerizzate sono un anti-pattern tipico quanto le pipeline assenti.

Perché GitHub Actions è la scelta ottimale per PMI italiane con VPS unmanaged

La scelta dell'orchestratore CI/CD per una PMI italiana nel 2026 è un tema relativamente settlato ma che vale la pena esplicitare. Le alternative principali sono GitHub Actions (se il codice è su GitHub), GitLab CI (se il codice è su GitLab self-hosted o cloud), CircleCI, Jenkins self-hosted. La mia raccomandazione di default per PMI che non hanno preferenze pregresse è GitHub Actions, per quattro motivi pratici. Primo, zero-maintenance dell'infrastruttura CI: runner cloud gestiti da GitHub, zero server da amministrare dall'azienda, scalabilità automatica. Secondo, integrazione nativa con il repository: segreti gestiti nel UI di GitHub, protezioni di branch integrate, review policy nativa - nessuna configurazione aggiuntiva. Terzo, catalogo enorme di action riusabili: decine di migliaia di GitHub Actions pubbliche, documentate e mantenute, coprono la maggioranza dei casi d'uso senza dover scrivere bash da zero. Quarto, costo contenuto per volumi PMI: 2000 minuti/mese gratuiti per organizzazioni nei piani Team, oltre si paga circa 0,008 dollari al minuto - per una PMI tipica con 100-300 deploy al mese il costo mensile è inferiore a 40 dollari.

Il tema critico che distingue una pipeline PMI da una enterprise è la semplicità di mantenimento nel tempo. Una pipeline elegante che usa tre stage separati, matrix di test su cinque versioni di PHP, build cache sofisticata e deploy tramite canary release progressive sembra impressionante in un blog post, ma quando due anni dopo serve modificarla e nessuno del team si ricorda come funziona, l'intera costruzione diventa un debito tecnico. La pipeline che costruisco tipicamente per PMI italiane sta in un singolo file YAML di 150-250 righe, è comprensibile da uno sviluppatore PHP senior in 15 minuti di lettura, e copre il 90% delle esigenze reali senza sofisticazioni superflue. La documentazione ufficiale di GitHub Actions copre in dettaglio ogni aspetto del linguaggio YAML e delle action disponibili, ed è ottima come reference operativo.

Se stai gestendo un subentro tecnico su un'applicazione PHP con processo di deploy manuale o improvvisato e vuoi un'analisi indipendente sulla costruzione di una pipeline moderna e sostenibile, nel mio profilo professionale trovi il dettaglio degli interventi di modernizzazione CI/CD che ho condotto in contesti PMI italiane post-subentro, sempre con approccio pragmatico e calibrato sul team reale che dovrà poi mantenere la pipeline.

La pipeline end-to-end: test, build, deploy, rollback in un singolo file YAML

La struttura operativa della pipeline che ho costruito sul cliente modenese - replicabile praticamente identica in altri contesti Laravel - si articola in cinque job sequenziali con dipendenze esplicite fra di loro. Il file vive in .github/workflows/deploy.yml nel repository dell'applicazione. Trigger di attivazione: push sul branch main (deploy automatico in produzione) e manual dispatch via UI per deploy on-demand di specifici tag.

Job 1 - Test automatici. Esegue PHPUnit/Pest sull'intera suite di test, Laravel Dusk per smoke test end-to-end sui path critici, composer audit per vulnerabilità note nelle dipendenze. Durata tipica: 4-8 minuti. Il job fallisce su qualunque test rosso o vulnerabilità high-severity nelle dipendenze.

Job 2 - Analisi statica. Esegue PHPStan a livello 8, PHP CS Fixer in dry-run mode per controllo di stile, Rector in dry-run per identificare upgrade path obsoleti. Durata tipica: 2-4 minuti. Il job fallisce su errori statici nuovi (confronta con baseline memorizzata nel repository).

Job 3 - Build dell'artifact. Installa le dipendenze composer con --no-dev --optimize-autoloader, builda gli asset frontend con npm run build, genera l'artifact finale come archivio tar.gz che include il codice ottimizzato, gli asset prodotti, e un manifest con la commit SHA e il timestamp. L'artifact viene caricato come GitHub Actions artifact per riferimento successivo. Durata: 3-5 minuti.

Job 4 - Deploy blue-green semplificato. Il cuore della pipeline e la parte più critica. Il deploy si connette al VPS di produzione via SSH usando una chiave privata dedicata memorizzata nei GitHub Secrets dell'organizzazione, crea una nuova directory versionata sul filesystem (/var/www/app-deployments/20261217-104532-abc123/), scompatta l'artifact dentro quella directory, esegue migration database con php artisan migrate --force, ricostruisce le cache di route/config/view Laravel, aggiorna il symlink atomico /var/www/app-current per puntare alla nuova directory, fa reload del PHP-FPM. Durata: 2-4 minuti. Zero-downtime perché il symlink swap è atomico e PHP-FPM legge il nuovo percorso senza interruzione delle richieste in corso.

Job 5 - Smoke test post-deploy e rollback automatico. Dopo 60 secondi di stabilizzazione, esegue una serie di smoke test HTTP contro il dominio di produzione: verifica che l'homepage risponda 200, che l'endpoint di login sia raggiungibile, che il database sia accessibile dall'applicazione via query di test. Se uno qualsiasi di questi test fallisce, il job esegue automaticamente rollback cambiando il symlink alla directory della release precedente e fa reload di PHP-FPM. Il rollback completo in caso di failure richiede tipicamente 45-60 secondi, e manda alert Slack al team con dettagli del deploy fallito.

Secrets management: come evitare di committare credenziali nella pipeline

Un aspetto critico di qualsiasi pipeline CI/CD è la gestione dei secrets necessari per il deploy - chiavi SSH private per connettersi al server, password del database, API key di servizi esterni, token di notifica Slack. Committerli nel repository è inaccettabile per le ragioni che ho descritto nel mio articolo dedicato ai pattern sicuri per la gestione dei file .env in produzione Laravel e Symfony. La soluzione nativa GitHub è usare i GitHub Secrets - variabili cifrate memorizzate nel UI del repository o dell'organizzazione, accessibili dai workflow come variabili di environment ma mai esposte nei log pubblici.

Lo standard che applico è il seguente. Per ogni ambiente target (staging, produzione) definisco un set dedicato di secrets: SSH_PRIVATE_KEY_PROD, SSH_HOST_PROD, SSH_USER_PROD, DEPLOY_PATH_PROD, SLACK_WEBHOOK_DEPLOY. Le chiavi SSH sono generate una volta al setup, con il comando ssh-keygen -t ed25519 -f deploy_key -N '' - chiave dedicata esclusivamente al deploy, non riutilizzata altrove, con commento che ne identifica l'uso. La chiave pubblica viene aggiunta in ~/.ssh/authorized_keys dell'utente di deploy sul server con restrizioni (command="/usr/local/bin/deploy-runner.sh",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty) - la chiave può eseguire solo un specifico script, niente altro. Questo pattern limita drasticamente la superficie di attacco in caso di compromissione dei GitHub Secrets: un attaccante che ottiene la chiave non può aprire una shell interattiva sul server, può solo eseguire il deploy. Lo script deploy-runner.sh è un wrapper minimale che gestisce solo le operazioni legittime di deploy (unpack artifact, migration, symlink update) e logga ogni invocazione.

Per ambienti con requisiti di sicurezza più stretti (banche, fintech regolamentate, sanità), uso il meccanismo Environment Protection Rules di GitHub Actions - che richiede approvazione manuale esplicita di un reviewer autorizzato prima che il job di deploy in produzione possa partire. Il CTO o un security officer riceve una notifica quando un deploy è in attesa di approvazione, revisiona il diff, e autorizza o rigetta la richiesta. Questo aggiunge un gate umano al processo automatico, compatibile con le policy di compliance che richiedono approvazione esplicita per modifiche a sistemi critici.

Database migration in produzione: i pattern per evitare il disastro

Il punto di maggior rischio di qualunque pipeline CI/CD è l'esecuzione delle database migration - le modifiche allo schema del database in produzione che accompagnano il nuovo codice. Una migration scritta male può corrompere dati, saturare il database durante l'esecuzione su tabelle grosse, o rompere temporaneamente la compatibilità fra vecchia applicazione ancora in esecuzione e nuovo schema. Gli errori in questa fase sono i più costosi da recuperare.

Il pattern operativo che applico è basato su tre principi di disciplina del developer e uno di disciplina della pipeline. Primo principio: ogni migration deve essere backwards-compatible con la versione precedente dell'applicazione per almeno una release. Significa che prima di rimuovere una colonna o una tabella, serve una release intermedia che smette di usarla; prima di rinominarla, serve una release intermedia che supporta entrambi i nomi. Questo pattern elimina il rischio di incompatibilità durante il deploy blue-green. Secondo principio: migration che modificano grosse tabelle vanno fatte con tooling dedicato, non con la migration system di Laravel/Doctrine. Per tabelle sotto il milione di record, la migration Laravel nativa è OK. Per tabelle grosse uso pt-online-schema-change per MySQL e pg_repack per PostgreSQL, che eseguono le modifiche senza lock. Terzo principio: ogni migration ha un piano di rollback esplicito documentato. Il metodo down() del file migration non è sufficiente - serve un documento separato che descriva come annullare l'operazione se il deploy viene rollbackato (attenzione: rollback di una migration che aveva già operato su dati reali può essere impossibile). Il principio di disciplina della pipeline è la verifica pre-deploy: il job di build include una dry-run della migration contro una copia recente del database di produzione per catturare errori sintattici o problemi evidenti prima del deploy vero.

La gestione avanzata delle migration in contesti di alto carico beneficia dei pattern di caching multilivello Laravel per strategie di alto traffico che ho descritto in un articolo dedicato, dove l'interazione fra cache e schema changes è un tema non banale.

Monitoring post-deploy e chiusura del loop con Slack/Sentry

La pipeline non finisce quando il deploy termina - finisce quando abbiamo evidenza che il deploy non ha rotto nulla in produzione. Gli smoke test sintetici del Job 5 coprono i casi ovvi, ma non catturano degradazioni sottili (performance leggermente peggiori, errori che emergono solo su casi edge, problemi che si manifestano solo dopo qualche ora sotto carico reale). Il pattern operativo che uso integra la pipeline con tool di observability già presenti in organizzazione.

La configurazione che applico include tre canali di monitoring post-deploy. Primo canale: Sentry o equivalente per tracking errori applicativi. La pipeline registra ogni deploy in Sentry tramite la loro API (@getsentry/action-release) - ogni errore successivo viene associato alla release che lo ha introdotto, e Sentry può inviare alert automatici se il error rate post-deploy aumenta oltre soglia rispetto al baseline. Secondo canale: Slack per notifiche sincronizzate al team - ogni deploy produce un messaggio Slack con dettaglio commit, autore, link al diff, e un pulsante "rollback manuale" che è un workflow_dispatch che esegue il rollback in un click. Questo fornisce visibilità continua al team senza richiedere che il team debba andare a cercare lo stato del deploy. Terzo canale: monitoring infrastrutturale (Grafana/Prometheus o New Relic) con annotation automatica di ogni deploy sul timeline delle metriche - se dopo un deploy il tempo di risposta peggiora o il consumo di memoria cresce, il team vede immediatamente la correlazione temporale.

Il risultato finale dell'intervento sul cliente modenese, misurato a sei mesi dal go-live della pipeline, è stato il seguente. Frequenza di deploy aumentata da circa 4 al mese (solo il venerdì dal freelance) a circa 24 al mese (distribuiti su tutti i giorni della settimana lavorativa). Zero downtime utente misurato su 148 deploy consecutivi - la strategia blue-green con symlink atomico ha funzionato come previsto. Zero rollback manuali eseguiti: tutti i rollback (5 in sei mesi) sono stati automatici tramite il Job 5 di smoke test post-deploy. Tempo medio di deploy end-to-end (dal push al merge ad applicazione funzionante in produzione): 14 minuti (contro i 45-60 minuti del processo manuale precedente). Tempo medio di recovery in caso di deploy fallito: 90 secondi (contro le 30+ minuti del processo manuale precedente che richiedeva intervento SSH interattivo). Costo operativo GitHub Actions: circa 22 dollari al mese, invisibile nel budget aziendale. Costo consulenziale dell'intervento: 3.200 euro una tantum. Il team interno, che al tempo del subentro non esisteva, è stato assunto e formato nei mesi successivi, e oggi mantiene autonomamente la pipeline senza mio intervento - un indicatore di successo più significativo di qualunque metrica di performance tecnica.

Se stai gestendo o subentrando su un'applicazione PHP Laravel con processo di deploy manuale, script bash tradizionali o assenza totale di CI/CD strutturato, l'implementazione di una pipeline GitHub Actions moderna è uno dei primi interventi che produce ROI visibile nel giro di settimane, sia in velocità di sviluppo sia in stabilità operativa. L'investimento è contenuto (3-5 giornate di consulenza per un setup completo), i benefici sono strutturali e si ripagano nel tempo attraverso ogni singolo deploy che non richiede più intervento manuale. Se vuoi confrontarti sul tuo caso specifico con una proposta di implementazione calibrata sulla dimensione del tuo team e sul tuo stack tecnologico specifico, contattami per una consulenza iniziale: in una sessione di analisi guidata produciamo insieme un disegno dettagliato della pipeline adatta al tuo contesto, con stime realistiche di tempo e tempi di setup.

Ultima modifica: