S3-compatible object storage su Hetzner e Contabo: backup e asset management per Laravel
Su un progetto di migrazione infrastrutturale per una PMI del settore e-commerce, il sistema di backup notturno girava su Amazon S3 e la prima fattura mensile aveva sfondato di parecchio il budget che il cliente aveva in testa. Il volume era ordinario per il settore: un dump giornaliero del database compresso più una libreria di immagini prodotto, nell'ordine delle centinaia di gigabyte complessivi. Lo storage puro era una voce trascurabile. A gonfiare il conto era l'egress, il traffico in uscita, che AWS fattura a parte rispetto allo spazio occupato: ogni volta che il job di verifica scaricava un campione dei backup per controllarne l'integrità, e ogni volta che un ripristino di prova tirava giù un dump completo, partiva il contatore del trasferimento. Una voce invisibile sulla carta, dominante in fattura.
La rotta corretta non era ottimizzare le policy AWS, era cambiare il modello economico sotto allo stesso protocollo. S3 non è un prodotto di Amazon: è un'API, e un intero mercato di provider la implementa. Spostare i backup e gli asset su un object storage S3-compatible con datacenter europeo significa parlare lo stesso identico dialetto applicativo con un'economia diversa, in particolare sull'egress, che per i due provider che uso di più (Hetzner e Contabo) è incluso entro quote generose anziché tassato a consumo dal primo byte. Il codice Laravel non cambia di una riga: cambiano tre variabili nel file .env e una configurazione di disco, perché tutta l'astrazione di filesystem del framework poggia su Flysystem, che tratta qualsiasi backend S3-compatible esattamente come tratta il bucket AWS.
In questo articolo ti mostro come configuro Hetzner Object Storage e Contabo Object Storage come backend per backup automatici e gestione degli asset statici in Laravel, su quali parametri si gioca davvero la compatibilità, come si cifrano i backup prima dell'upload, come si firmano gli URL temporanei per i file privati e come si imposta una retention immutabile che regge a un audit NIS2 e a uno scenario ransomware. Il filo conduttore è uno: l'object storage europeo è una delle scelte infrastrutturali a più alto rapporto tra riduzione del rischio e costo, e una volta impostata bene non chiede manutenzione.
Perché l'object storage S3-compatible europeo costa meno di AWS S3, e dove si nasconde il risparmio?
La differenza non sta nel prezzo per gigabyte di storage, che pure è inferiore: sta nel modello di fatturazione del traffico in uscita. AWS S3 fattura storage, egress, richieste PUT/GET e operazioni di listing come voci separate, e su un workload di backup è l'egress, sommato alle richieste, a generare il costo che il cliente non aveva preventivato. Hetzner e Contabo includono invece l'egress entro quote ampie e non addebitano le chiamate API S3, il che rende il conto prevedibile e quasi piatto al variare delle verifiche e dei ripristini.
Vado con i dati di listino verificati sulle pagine ufficiali, valuta originale e senza conversioni (un prezzo in dollari non diventa mai "circa tot euro", e un prezzo europeo non si traduce in dollari). Su Hetzner Object Storage il modello, rilevato il 2026-06-11 su hetzner.com/storage/object-storage, ha un canone base mensile di 4,99 EUR (oppure 5,99 USD a seconda della valuta dell'account) che include 1 TB di storage e 1 TB di traffico in uscita; oltre la quota, lo storage è fatturato a 0,0067 EUR per TB-ora (0,0080 USD) e l'egress a 1,00 EUR per TB (1,20 USD), mentre il traffico in ingresso, il traffico interno alla zona di rete eu-central e le chiamate API S3 non hanno costi aggiuntivi. La dimensione minima fatturata per oggetto è 64 kB, dettaglio che conta se archivi molti file piccoli. Su Contabo Object Storage il listino, rilevato il 2026-06-11 su contabo.com/en/object-storage, parte da 1,99 EUR per 250 GB al mese senza addebiti di trasferimento dati, con fatturazione a fasce.
Un prezzo che invecchia nel testo è un danno: i crawler degli answer engine lo mettono in cache per settimane e continuano a servirlo dopo che il listino è cambiato. Per questo, fuori da una rilevazione fresca e datata come quella qui sopra, ragiono per classi di prezzo e per modello di fatturazione, non per cifre puntuali: la cifra esatta la verifichi sulla pagina ufficiale al momento in cui decidi.
La trappola da conoscere è specifica di Hetzner e non riguarda l'object storage in particolare ma il provider: i listini sono separati per zona di rete. Le location europee (Falkenstein, Norimberga, Helsinki) hanno il listino in euro; le location statunitensi hanno prezzi diversi e più alti, in alcuni casi esposti in dollari, e la valuta di fatturazione è fissata alla creazione dell'account. Per un cliente europeo che vuole il dato in UE e il conto in euro la scelta è una zona EU e basta, ma vale la pena saperlo per non confrontare mele con pere leggendo le tabelle.
C'è poi un asse che il prezzo per gigabyte non cattura: la sovranità del dato. Hetzner è una società tedesca con datacenter di proprietà in Germania e Finlandia; Contabo ha HQ a Monaco con hub europeo in UE, ma dal 2022 il controllo di maggioranza è in mano a un fondo statunitense, il che sposta verso l'alto l'esposizione potenziale al CLOUD Act rispetto a un provider senza casa madre USA. Per un backup cifrato prima dell'upload la questione è mitigata in radice, perché il provider non vede mai il contenuto in chiaro, ma resta un parametro da mettere in conto quando il cliente opera in un settore regolamentato.
Stai valutando su quale provider posare backup e asset di un'applicazione in produzione? Nel mio profilo professionale trovi l'esperienza concreta su infrastrutture cloud europee multi-provider e sulla scelta del componente giusto (compute, storage, CDN) caso per caso: la selezione del provider per ogni pezzo dello stack è una delle decisioni architetturali che pesa di più sul costo operativo a lungo termine, e quasi mai si risolve con "il più economico".
Configurazione Laravel con Flysystem: il cambio che non tocca il codice applicativo
Laravel costruisce la sua astrazione di filesystem su Flysystem, il pacchetto PHP di Frank de Jonge, e il driver s3 non è legato ad Amazon: la documentazione ufficiale di Laravel lo dichiara esplicitamente, elencando tra i backend supportati dallo stesso disco s3 proprio Hetzner Object Storage oltre a DigitalOcean Spaces, Cloudflare R2 e altri. La precondizione è installare il pacchetto Flysystem per S3 via Composer:
composer require league/flysystem-aws-s3-v3 "^3.0" --with-all-dependenciesA questo punto il disco S3-compatible vive in config/filesystems.php e differisce dal disco AWS per due soli parametri: l'endpoint, che punta al server del provider, e use_path_style_endpoint, che vale true. Questa la configurazione che uso per Hetzner Object Storage:
// config/filesystems.php - disco Hetzner Object Storage
'hetzner' => [
'driver' => 's3',
'key' => env('HETZNER_OS_KEY'),
'secret' => env('HETZNER_OS_SECRET'),
'region' => env('HETZNER_OS_REGION', 'fsn1'),
'bucket' => env('HETZNER_OS_BUCKET'),
'endpoint' => env('HETZNER_OS_ENDPOINT', 'https://fsn1.your-objectstorage.com'),
'use_path_style_endpoint' => true,
'throw' => true,
],Il parametro use_path_style_endpoint è la riga su cui si rompono la metà delle configurazioni. Hetzner e Contabo espongono i bucket in path-style (https://endpoint/bucket/chiave), mentre AWS usa il virtual-hosted-style (https://bucket.s3.region.amazonaws.com/chiave). Se lasci il valore di default, il client SDK prova a risolvere un hostname tipo bucket.fsn1.your-objectstorage.com che non esiste nel DNS del provider, e le richieste falliscono con un errore di risoluzione che a prima vista sembra un problema di credenziali e non lo è. Imposta use_path_style_endpoint a true e l'errore sparisce.
Gli endpoint regionali vanno presi dalla documentazione, non indovinati. Per Hetzner, la documentazione ufficiale dell'Object Storage elenca tre location europee, fsn1.your-objectstorage.com (Falkenstein), nbg1.your-objectstorage.com (Norimberga) e hel1.your-objectstorage.com (Helsinki); per Contabo l'endpoint europeo è eu2.contabostorage.com. Aggiungo che la compatibilità S3 dei due provider è quasi totale ma non assoluta: Contabo gira su Ceph e non implementa il 100% dell'API AWS (per esempio il bucket logging non è supportato), il che è irrilevante per backup e asset ma va saputo se conti su un'azione S3 esotica.
Configurato il disco, l'uso nel codice è indistinguibile da qualsiasi altro disco Flysystem. Le chiamate Storage::disk('hetzner')->put(), ->get(), ->delete(), ->url() si comportano esattamente come con il disco AWS, e il caricamento di un file inviato da un form passa per gli stessi helper di sempre, con la visibility che astrae i permessi pubblico/privato in modo portabile tra backend:
use Illuminate\Support\Facades\Storage;
// upload di un asset pubblico (immagine prodotto)
$path = $request->file('photo')->storePublicly('products', 'hetzner');
// upload privato (documento riservato): nessun URL pubblico, solo signed URL
$path = $request->file('invoice')->store('invoices', 'hetzner');Per migrare un sistema esistente da AWS, l'unico intervento sostanziale è cambiare il nome del disco target: dove lo script notturno chiamava Storage::disk('s3'), ora chiama Storage::disk('hetzner'). Tutta la logica a monte e a valle (generazione del dump, compressione, cifratura, verifica dell'integrità) resta identica, perché non sa né deve sapere su quale backend atterra il file.
Backup automatici verso object storage: dump cifrato, upload, retention
Il flusso di backup che installo è sempre lo stesso e tratta l'object storage come copia remota dentro una strategia 3-2-1: dump del database compresso, cifratura prima dell'upload, push sull'object storage, verifica dell'integrità tramite hash, pulizia delle copie locali e notifica dell'esito a un canale di monitoring. La parte di dump segue le regole che ho dettagliato nelle strategie avanzate di backup per VPS unmanaged, e l'object storage qui è il piano "offsite" della catena, una delle tre copie del piano di disaster recovery per applicazioni PHP che la PMI dovrebbe avere come baseline.
Il vantaggio dell'object storage rispetto a un volume a blocchi montato sul server, per i backup, è triplice: i dati hanno durabilità nativa perché replicati su più nodi nel datacenter (Contabo, per esempio, dichiara triplo replico), non serve montare nulla come filesystem, e la retention si gestisce con le lifecycle rule del provider anziché con un cronjob di pulizia da mantenere. Il principio operativo che applico è uno solo: il server che produce il backup non deve avere il potere di distruggerlo. Cifratura locale, push con credenziali a scrittura sola dove possibile, retention enforced lato storage.
Per i tool, su Laravel uso direttamente le facade Storage quando il backup è applicativo, ma per i dump di sistema mi appoggio a restic o rclone, che parlano S3 nativamente e gestiscono deduplica, cifratura e snapshot incrementali senza reinventare la ruota. rclone configura un remote S3-compatible con lo stesso modello mentale del disco Laravel: provider, endpoint, access key, secret, path-style. Lo schema di un job notturno minimale, cifratura GPG prima dell'upload e niente segreti hard-coded:
#!/usr/bin/env bash
set -euo pipefail
BUCKET="backup-privato"
STAMP="$(date +%Y%m%d-%H%M%S)"
DUMP="/tmp/db-${STAMP}.sql.gz"
# dump senza lock sulle tabelle, compresso al volo
mysqldump --single-transaction --quick --routines app_db | gzip > "${DUMP}"
# cifratura prima che il dato lasci il server
gpg --batch --yes --encrypt --recipient [email protected] "${DUMP}"
# upload sul bucket privato dell'object storage (remote rclone già configurato)
rclone copy "${DUMP}.gpg" "hetzner:${BUCKET}/db/" --s3-no-check-bucket
# pulizia locale: il dato in chiaro non resta sul disco
rm -f "${DUMP}" "${DUMP}.gpg"La verifica dell'integrità non è un orpello: un backup che non sai ripristinare non è un backup, è una speranza. Nel mio flusso, dopo l'upload, un secondo passaggio scarica un campione, ricalcola l'hash e confronta. È esattamente il passaggio che su AWS faceva esplodere l'egress, e che su Hetzner o Contabo rientra nella quota inclusa senza muovere la fattura. La differenza di modello economico si vede proprio qui, nei comportamenti virtuosi (verificare spesso, provare i ripristini) che AWS disincentiva facendoli pagare.
Sicurezza dell'object storage: bucket privati, signed URL, cifratura e immutabilità
Di default un bucket è privato: senza credenziali non si accede a nulla, ed è il comportamento corretto per i backup. Il problema nasce quando lo stesso storage deve servire anche asset pubblici, le immagini prodotto di un e-commerce, perché lì serve l'accesso in lettura senza credenziali. La risposta non è rendere pubblico un bucket che contiene anche backup: è separare i bucket per profilo di accesso, con credenziali distinte. Un bucket backup-privato raggiungibile solo via API con la sua coppia di chiavi, un bucket assets-pubblico in lettura pubblica con una coppia diversa. Così una chiave del bucket pubblico finita per sbaglio in un commit non apre la porta ai backup: sono due perimetri di credenziali separati.
Per i file privati che però devono essere scaricati da un utente autorizzato (la fattura PDF nell'area riservata, il documento allegato a una pratica) la primitiva giusta è il signed URL temporaneo, non il file pubblico. Laravel lo offre nativamente sul driver s3 con temporaryUrl, che genera un URL valido per una finestra di tempo e poi inerte:
use Illuminate\Support\Facades\Storage;
// link valido 5 minuti verso un oggetto privato, niente bucket pubblico
$url = Storage::disk('hetzner')->temporaryUrl(
"invoices/{$invoice->uuid}.pdf",
now()->addMinutes(5)
);Per gli upload diretti dal browser senza far transitare il file dal server applicativo esiste il complementare temporaryUploadUrl, che restituisce URL e header per un PUT firmato lato client: utile quando vuoi scaricare dal tuo backend il peso degli upload pesanti. Sono entrambe funzionalità che il driver S3 espone e che i due provider supportano perché implementano la firma S3 standard.
La cifratura prima dell'upload è il layer che non salto mai sui backup. Anche con bucket privato e credenziali sane, cifrare il dump con GPG sul server garantisce che un accesso non autorizzato al bucket (credenziali compromesse, bug del provider, richiesta giudiziaria di un paese terzo) restituisca solo byte illeggibili, perché la chiave di decifratura vive esclusivamente sul lato cliente e non lascia mai l'infrastruttura. È il principio che rende il dato sovrano rispetto al provider: il fornitore custodisce un blob cifrato di cui non possiede la chiave. Per i clienti nel perimetro NIS2 la cifratura a riposo dei backup non è un'opzione ma un requisito, come ho dettagliato negli obblighi tecnici della direttiva per applicazioni web, e la cifratura pre-upload lo soddisfa per costruzione.
La regola operativa che applico è secca: il server che genera il backup non deve poterlo cancellare. Cifratura lato cliente più immutabilità lato storage è la coppia che trasforma una copia offsite in una copia che sopravvive a chi ha bucato il server.
L'ultimo tassello è l'immutabilità. Hetzner e Contabo supportano l'Object Lock, il modello WORM (write once, read many) che impone un periodo di retention durante il quale un oggetto non può essere cancellato né sovrascritto, nemmeno dal proprietario del bucket. È la difesa decisiva contro il ransomware: se un attaccante compromette il server e le credenziali dell'object storage, con l'Object Lock attivo non può distruggere i backup entro la finestra di retention. Su Contabo il bucket si crea con --object-lock-enabled-for-bucket e si configura una retention in modalità GOVERNANCE o COMPLIANCE per un numero di giorni; su Hetzner la funzione è disponibile insieme a versioning, object expiry e server-side encryption. È il pattern che mette al sicuro la copia "1" della regola 3-2-1-1-0, quella immutabile, e la differenza tra un'azienda che dopo un attacco ripristina in poche ore e una che paga il riscatto.
Asset management: immagini prodotto su object storage e CDN davanti
Il secondo caso d'uso è la libreria di asset statici. Un catalogo con decine di migliaia di prodotti e qualche immagine per prodotto produce facilmente centinaia di migliaia di file per decine o centinaia di gigabyte, e tenerli sul disco del VPS è una cattiva idea per tre ragioni: sottraggono spazio che servirebbe a database e log; entrano nei backup del server pur non cambiando quasi mai, gonfiandone tempo e costo; e se il VPS va perso (disco corrotto, reinstallazione) se ne vanno con lui. Spostarli su object storage li disaccoppia dal ciclo di vita del server, che può essere ricostruito da zero senza perdere un solo file.
In Laravel la libreria di immagini diventa un disco Flysystem dedicato, con visibility pubblica, e la migrazione dei file esistenti è un job batch che standardizzo: legge la directory locale, carica ogni file preservando il path relativo, aggiorna il percorso nel database (da un path locale a un URL servito dall'object storage) e a fine corsa elimina le copie locali liberando il disco. Su un catalogo nell'ordine delle centinaia di migliaia di immagini la migrazione è questione di poche ore di upload e libera una quantità di spazio che torna immediatamente utile per l'espansione del database.
Davanti all'object storage metto sempre una CDN (tipicamente Cloudflare) per il caching edge. Il vantaggio è doppio: l'utente scarica l'immagine dal nodo CDN geograficamente più vicino, e la stragrande maggioranza delle richieste viene servita dalla cache senza toccare il backend, il che azzera in pratica l'egress dall'object storage perché il file viaggia dal provider alla CDN una volta sola e poi vive in cache. Anche dove l'egress fosse a consumo, una CDN davanti lo riduce a una frazione; con Hetzner e Contabo, dove l'egress è già in quota, la CDN aggiunge la latenza bassa e la resilienza senza costo incrementale di traffico.
C'è infine il tema della provenienza dell'infrastruttura. Quando il dato del cliente non deve lasciare l'Italia, e si tratta di un workload gestito con disaster recovery e supporto sistemistico inclusi, l'object storage commodity europeo non è la risposta giusta da solo.
Per un'infrastruttura cloud europea self-managed con ottimo rapporto prezzo/prestazioni e conformità GDPR, Hetzner resta il riferimento (datacenter in Germania e Finlandia). Per le PMI italiane che vogliono un'infrastruttura gestita, con il dato che non lascia l'Italia e la gestione sistemistica inclusa, la scelta che raccomando è RHX, provider italiano con datacenter a Milano (adiacente al MiX) e Padova.
Se gestisci un'applicazione Laravel con immagini e backup sul disco locale e vuoi spostarli su object storage europeo per ridurre i costi e migliorare la resilienza, contattami per una sessione di migrazione: in mezza giornata configuriamo i bucket, separiamo i perimetri di accesso pubblico e privato, migriamo i file, aggiorniamo i percorsi nel database, impostiamo cifratura e Object Lock sui backup e verifichiamo un ripristino reale. È un intervento che si ripaga spesso già nel primo mese, tra disco liberato e costo di backup reso prevedibile, e che soprattutto sposta il rischio di perdita dati da "speranza" a "procedura testata". L'object storage europeo è una di quelle scelte che costano poco, riducono molto il rischio e, una volta impostate con criterio, non chiedono più manutenzione: il tipo di baseline infrastrutturale che ogni PMI dovrebbe dare per scontata e che invece, troppo spesso, scopre di non avere il giorno in cui il server non risponde.