Vai al contenuto

Backups

Backup gratis di Supabase con una GitHub Action, e il tranello

Un backup di Supabase con una GitHub Action è gratis e va bene per molte app. Il workflow, la connection string giusta e l'egress di ogni esecuzione.

Vlad Tkachenko16 min di lettura
Un database in un contenitore Supabase, un tubo che passa da un contatore con l'ago nella fascia ambra, e una fila di file notturni.

In breve

  • Una GitHub Action di backup di Supabase esegue la CLI di Supabase secondo un orario e fa il commit del dump in un repository privato. Non costa nulla, e per molte app è la risposta giusta.
  • Ogni esecuzione scarica tutto il tuo database, e Supabase lo conta come egress. Il piano gratuito include 5 GB al mese per tutta l'organizzazione, e un dump giornaliero di un database vicino al limite di 500 MB ne consuma circa tre volte tanto.
  • L'esempio della stessa Supabase si collega con una stringa che i runner di GitHub non riescono a raggiungere. Metti nel secret la stringa del Session pooler, e ripristina una copia prima di fidarti delle altre.

Il tuo progetto Supabase è sul piano gratuito, quindi niente viene copiato da nessuna parte, oppure è su Pro e ogni copia che fa vive dentro l'account che hai paura di perdere. In un thread proprio su questo, qualcuno ha detto che la risposta è una GitHub Action di backup di Supabase: un piccolo file in un repository che esporta il tuo database ogni notte e non costa nulla.

Il consiglio era buono. Supabase pubblica il workflow in prima persona, e per molte app è la cosa da mettere in piedi questa settimana. Ecco la parte che quei thread lasciano fuori: non costa denaro, eppure ha un prezzo. Ogni esecuzione scarica tutto il tuo database, e Supabase misura quel download.

Pensalo come i giga della tua offerta telefonica. Il piano ha una quota mensile, tutto quello che fa la tua app ci attinge, e un backup è un download pesante sulla stessa offerta. Sul piano gratuito, una copia notturna di un database vicino al suo limite di dimensione si mangia la quota di tutto il mese in una decina di giorni. Questo, e una connection string che GitHub non riesce a raggiungere, sono le due cose tra te e un backup che funziona, e nessuna delle due sta sulla pagina di Supabase.

Puoi fare il backup di Supabase gratis con una GitHub Action?

Sì, e Supabase documenta come. La sua pagina sui backup automatici con GitHub Actions fornisce un workflow che installa la CLI di Supabase, esporta ruoli, schema e dati in tre file e ne fa il commit nel repository secondo un orario.

Nemmeno GitHub ti fa pagare, entro certi limiti. Un repository privato su GitHub Free riceve 2.000 minuti di Actions al mese, e un dump notturno di un database piccolo ne usa una piccola parte.

Quello che ottieni è una copia che esce da Supabase ogni notte e atterra sui server di un'altra azienda. È proprio ciò che i backup di Supabase non possono darti, perché le copie giornaliere di un piano a pagamento restano dentro l'account che proteggono.

È la risposta giusta quando valgono tre cose. La tua app vive già in un repository GitHub, come succede se hai attivato la sincronizzazione con GitHub in Lovable o in un builder simile. Modificare un file YAML non ti preoccupa. E il tuo database è piccolo rispetto alla quota di egress del tuo piano. Le prime due le sai già. La terza è aritmetica, e ha una sezione tutta sua più sotto.

Il workflow, con l'orario e il secret

Salva questo come .github/workflows/supabase-backup.yml in un repository privato che non contenga nient'altro:

name: supabase-backup

on:
  schedule:
    - cron: '17 3 * * *' # ogni giorno alle 03:17 UTC
  workflow_dispatch:

jobs:
  backup:
    runs-on: ubuntu-latest
    permissions:
      contents: write
    env:
      SUPABASE_DB_URL: ${{ secrets.SUPABASE_DB_URL }}
    steps:
      - uses: actions/checkout@v7
      - uses: supabase/setup-cli@v3
        with:
          version: latest
      - name: Back up roles
        run: supabase db dump --db-url "$SUPABASE_DB_URL" -f roles.sql --role-only
      - name: Back up schema
        run: supabase db dump --db-url "$SUPABASE_DB_URL" -f schema.sql
      - name: Back up data
        run: >
          supabase db dump --db-url "$SUPABASE_DB_URL" -f data.sql --use-copy --data-only
          -x "storage.buckets_vectors" -x "storage.vector_indexes"
      - uses: stefanzweifel/git-auto-commit-action@v7
        with:
          commit_message: Supabase backup

È il workflow di Supabase con tre modifiche.

  • Gira secondo l'orario e quando premi il pulsante, e in nessun altro momento. La versione di Supabase gira anche a ogni push e a ogni pull request su main. Ogni esecuzione è un download completo del tuo database, quindi in un repository che contiene anche la tua app ogni modifica che pubblichi costerebbe la quota di un backup.
  • Gira alle 03:17, fuori dall'ora esatta. GitHub dice che le esecuzioni programmate possono essere ritardate all'inizio di ogni ora, e che con un carico sufficiente alcuni job in coda vengono scartati. L'esempio di Supabase usa 0 0 * * *, che cade allo scoccare dell'ora. Gli orari sono UTC a meno che tu non aggiunga un timezone, e va bene qualsiasi minuto diverso da 0.
  • La riga dei dati corrisponde alla guida attuale di Supabase su backup e ripristino, che aggiunge due esclusioni -x assenti dalla pagina del workflow. Così i file che conservi sono quelli per cui sono scritte le istruzioni di ripristino di Supabase.

Poi il secret. Nel repository apri Settings, poi Secrets and variables, poi Actions, e aggiungi un secret di repository chiamato SUPABASE_DB_URL. Quello che ci metti dentro decide se tutto questo funziona, quindi ha la prossima sezione tutta per sé.

Una volta salvato, avvia il workflow a mano dalla scheda Actions, così la prima esecuzione avviene mentre guardi. workflow_dispatch è la riga che ti dà il pulsante Run workflow.

Quale connection string va nel secret?

La stringa del Session pooler, dal pulsante Connect in cima al tuo progetto Supabase. Usala anche se la pagina del workflow di Supabase mostra la connessione diretta nel suo esempio.

Il motivo è IPv6, il tipo più recente di indirizzo internet. La connessione diretta di Supabase, l'host che comincia con db. e finisce con supabase.co, usa IPv6 per impostazione predefinita, e la stessa pagina mette GitHub Actions tra i servizi che accettano solo IPv4. Il runner riceve un indirizzo verso cui non ha strada, e il primo dump fallisce prima di scrivere un solo byte. L'errore dice che il nome host non si è potuto tradurre, o che la rete non è raggiungibile, spesso con accanto un lungo indirizzo pieno di due punti. Quell'indirizzo è quello IPv6. Sembra una password sbagliata o un database giù, e la vera causa è una rete su cui il runner non si trova.

La guida di Supabase su backup e ripristino dice di usare per impostazione predefinita la stringa del Session pooler, e quella diretta solo se la tua rete supporta IPv6 o se paghi l'add-on IPv4. Riconosci la stringa del pooler da tre cose: il nome utente è postgres. seguito dal riferimento del tuo progetto, l'host finisce con pooler.supabase.com e la porta è 5432.

Il runner non ha strada verso l'indirizzo IPv6 della connessione diretta. Il Session pooler risponde su IPv4 e raggiunge lo stesso database.

La password al suo interno è la password del tuo database, quella impostata quando è stato creato il progetto, non una chiave API. Se nessuno l'ha annotata, Supabase ti permette di reimpostarla in Database Settings; qualsiasi altra cosa che si collega con quella vecchia smette di funzionare finché non le dai quella nuova.

Quella stringa può leggere e modificare ogni riga del tuo database. Vive nel secret e da nessun'altra parte: mai nel file del workflow, mai in un messaggio di commit e mai incollata in un assistente di IA insieme all'errore di cui volevi chiedere.

Un backup di Supabase conta contro il mio egress?

Sì, tutto quanto. Supabase conta come egress, cioè traffico in uscita, i dati che uno qualsiasi dei suoi servizi manda all'esterno, e un dump attraverso il pooler viene registrato come Shared Pooler Egress nella stessa quota del tuo traffico API, dei download di file e di tutto il resto che la tua app serve.

Torniamo all'offerta telefonica. La quota si azzera a ogni mese di fatturazione, ogni servizio che la tua app usa ci attinge, e un backup è un altro download pesante. È anche un piano famiglia: Supabase applica la quota a tutta la tua organizzazione, quindi tutti i progetti che contiene condividono la stessa quota.

Il 26 settembre 2026, la pagina dei prezzi di Supabase dava al piano gratuito 5 GB di egress al mese e un limite di 500 MB per il database di ogni progetto, e al piano Pro 250 GB di egress. Sul piano gratuito ci sono altri 5 GB di cached egress, che coprono i file serviti dalla CDN di Supabase, e un backup non li tocca mai. Gli altri limiti del piano gratuito, e cosa fa ognuno quando lo superi, hanno un articolo tutto loro.

Superare la quota sul piano gratuito non produce una fattura. Supabase ti avvisa e ti concede un periodo di tolleranza, e se continui a superarla limita ogni progetto dell'organizzazione. Secondo Supabase questo può voler dire richieste API respinte con un errore 402, un database messo in sola lettura o progetti in pausa. Per un'app con dei clienti, è un disservizio che ha avviato il tuo backup.

Vale per qualsiasi backup che esce da Supabase, compreso quello che vendiamo noi. Una copia tenuta fuori dall'account deve essere scaricata per arrivarci.

Ogni quanto dovrebbe girare il backup?

Tanto spesso quanto la tua quota può pagare, e tutto si riduce a una sola moltiplicazione: la dimensione del database per le esecuzioni del mese.

Supabase mostra la dimensione del tuo database nel report Database, sotto Observability nel tuo progetto. Un dump non è esattamente di quella dimensione. Lascia fuori gli indici, che vengono ricostruiti dalle loro definizioni, e scrive ogni valore come testo. È abbastanza vicino per pianificare, ed è il numero che puoi andare a vedere.

Dimensione del databaseGiornaliero (30 esecuzioni)Due volte a settimanaSettimanale
50 MB1,5 GB0,4 GB0,2 GB
100 MB3 GB0,9 GB0,4 GB
250 MB7,5 GB2,2 GB1,1 GB
500 MB15 GB4,3 GB2,2 GB

Un mese ha circa 4,3 settimane, ed è da lì che vengono le ultime due colonne.

Sul piano gratuito, le due cifre giornaliere in fondo consumano più della quota di tutto il mese prima che la tua app abbia risposto a una sola richiesta. Un orario giornaliero consuma tutti i 5 GB intorno ai 160 MB, e il traffico della tua app esce dalla stessa quota, quindi guarda l'egress del mese scorso nella pagina Usage della tua organizzazione prima di scegliere. Il settimanale ci sta con qualsiasi dimensione consentita dal piano gratuito.

Un database al limite di 500 MB del piano gratuito, con backup giornaliero e settimanale. La linea tratteggiata sono i 5 GB di egress del piano gratuito per il mese.

La riga del cron è l'unica cosa che cambi:

17 3 * * *      ogni giorno alle 03:17 UTC
17 3 * * 1,4    il lunedì e il giovedì
17 3 * * 0      la domenica

Su Pro l'aritmetica quasi smette di contare. 250 GB al mese pagano un dump giornaliero di un progetto fino a circa 8 GB, che è lo spazio su disco che il piano Pro include per progetto.

Perché il mio workflow fallisce con un errore di versione di pg_dump?

Perché il pg_dump che ha eseguito è più vecchio del tuo database. Succede solo a un workflow che chiama pg_dump direttamente invece che attraverso la CLI di Supabase. La CLI si porta il suo pg_dump, ed è uno dei motivi per cui il workflow qui sopra la usa.

Il runner ubuntu-latest di GitHub arriva con PostgreSQL 16 installato. I progetti Supabase possono girare su Postgres 17, e Postgres documenta che pg_dumpnon esporta da un server di una versione principale più recente della sua, e preferisce rifiutarsi piuttosto che rischiare un file difettoso. L'esecuzione si ferma con:

pg_dump: error: aborting because of server version mismatch
pg_dump: detail: server version: 17.…; pg_dump version: 16.…

La versione del tuo progetto è sotto Project Settings, poi General, se vuoi confermarla. Le tue credenziali non c'entrano.

La soluzione sono due passaggi. Aggiungi il repository dei pacchetti di PostgreSQL stesso, così il runner può installare il client della versione 17, poi chiama quel client con il suo percorso completo:

- name: Install the Postgres 17 client
  run: |
    sudo apt-get install -y postgresql-common
    sudo /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -y
    sudo apt-get install -y postgresql-client-17
- name: Back up
  run: /usr/lib/postgresql/17/bin/pg_dump "$SUPABASE_DB_URL" …

Tieni dopo la connection string gli argomenti che avevi già. Il percorso completo conta: su Ubuntu il semplice pg_dump è un wrapper che sceglie una versione al posto tuo, e sul runner c'è già un'installazione di PostgreSQL 16 da scegliere.

Anche il pg_dump della CLI ha una versione, e non viene dal tuo progetto. Senza un supabase/config.toml accanto al workflow, come in un repository che non contiene altro, la CLI usa pg_dump 17. Su un progetto ancora su Postgres 15, il data.sql che scrive contiene allora SET transaction_timeout = 0, un'impostazione che Postgres 15 non ha, e il caricamento si ferma su quella riga in qualsiasi database Postgres 15, compreso il progetto da cui viene il file. L'abbiamo riprodotto il 4 ottobre 2026 con pg_dump 17.11 e Postgres 15.19. Se il tuo progetto è su 15, aggiungi una riga al blocco env del job così la CLI usa il pg_dump corrispondente:

env:
  SUPABASE_DB_URL: ${{ secrets.SUPABASE_DB_URL }}
  SUPABASE_DB_MAJOR_VERSION: '15'

Il backup in sé finisce senza errori in entrambi i casi, quindi senza quella riga la differenza si vede solo il giorno in cui ricarichi il file.

Posso fare il commit del backup nel mio repository?

In uno privato, sì. È quello che fa il workflow di Supabase, e la sua pagina dice due volte di non fare mai il backup dei dati in un repository pubblico.

Tre cose su un repository come casa di un database, nessuna delle quali sta su quella pagina:

  • Chiunque può leggere il repository può leggere i tuoi utenti. data.sql contiene ogni riga: indirizzi email, nomi, qualsiasi cosa tengano le tue tabelle, e anche i tuoi account. Per questo il workflow ha un repository tutto suo senza nessun altro dentro, e non quello in cui vive il codice della tua app, che il tuo builder magari sincronizza e in cui un giorno potresti invitare un freelance.
  • La copia di ogni notte resta nella cronologia. Git conserva ogni versione di ogni file. Quando qualcuno tra i tuoi clienti ti chiede di cancellare il proprio account, la sua riga resta in ogni commit precedente di quel repository finché non ne riscrivi la cronologia.
  • Smette di funzionare a 100 MiB. GitHub avvisa sopra i 50 MiB e blocca i file oltre i 100 MiB, e la stessa pagina dice che Git non è pensato per gestire file SQL grandi. La notte in cui data.sql supera quella soglia, il passaggio di commit fallisce, e ogni esecuzione successiva fallisce allo stesso modo.

Quando ti avvicini, lo stesso workflow può caricare i tre file in un bucket di storage tuo invece di farne il commit. Oppure è il punto in cui farlo da te ha smesso di essere economico, ed è lì che comincia l'ultima sezione.

Cosa non copia il workflow?

I tuoi file caricati. Supabase Storage tiene ogni file fuori dal database e una riga che lo descrive dentro, quindi il dump porta con sé la riga e non l'immagine. Fare il backup di Storage è un lavoro a parte, su qualsiasi strada.

I tuoi utenti invece ci sono. Il dump dei dati attraversa lo schema auth in cui vivono i tuoi account, e cercare auth.users in data.sql te li mostra. Il progetto intorno al database no: Edge Functions, impostazioni dei provider di autenticazione, chiavi API e secret sono configurazione e non dati, e l'elenco di ciò che non c'è in nessuna delle due copie è quello da leggere prima che ti serva.

Un'esecuzione verde vuol dire che il backup ha funzionato?

Vuol dire che il job è finito senza errori, che è un'affermazione più piccola. Tre risultati molto diversi finiscono con la stessa spunta verde:

  1. Un buon dump del progetto giusto.
  2. Un dump del progetto sbagliato, perché nel secret c'è la stringa di una copia di staging.
  3. Un dump senza nessuna riga, perché qualcuno ha modificato la riga dei dati e --data-only è sparito, il che la trasforma di nuovo nel dump dello schema.

Un risultato non lascia alcuna traccia. Un'esecuzione programmata che GitHub scarta sotto carico non compare come fallimento, perché non c'è nessuna esecuzione che possa fallire. E quando un'esecuzione fallisce davvero, GitHub manda la notifica alla persona che ha modificato per ultima la riga del cron. Se te l'ha configurato un freelance, l'email sul tuo backup arriva nella sua casella.

Entrambi i file sono usciti da un'esecuzione finita senza errori. Nulla nell'esecuzione ti dice quale dei due hai.

Solo un ripristino li distingue. Riproduci i tre file in un progetto Supabase usa e getta o in uno locale, confronta il numero di righe delle tabelle che ti interessano con quelle vere ed entra come un utente vero. Come ripristinare un backup di Supabase ha i comandi, nell'ordine indicato da Supabase. Fallo una volta adesso, e di nuovo ogni volta che cambi il workflow.

Puoi anche lasciare il ripristino e il conteggio a ogni esecuzione. Abbiamo pubblicato una versione di questo workflow come una GitHub Action open source, Reeve-page/supabase-backup-action. Fa gli stessi tre dump, conserva la copia come artefatto del workflow o in un bucket S3 o R2, poi la riproduce in un database Supabase usa e getta sul runner e confronta il numero di righe di ogni tabella con il file. L'esecuzione diventa verde solo quando ogni tabella è tornata con le righe che il file contiene:

- uses: Reeve-page/supabase-backup-action@v1
  with:
    db-url: ${{ secrets.SUPABASE_DB_URL }}

È gratuita e con licenza MIT. Ogni esecuzione scarica comunque l'intero database, quindi il calcolo dell'egress qui sopra vale identico, e provare l'accesso resta compito tuo.

Quando dovrei smettere di farlo per conto mio?

Quando l'aritmetica non torna più, quando il file non ci sta più nel repository o quando nessuno ripristina le copie. Ne basta una.

  • Il tuo database ha superato la quota gratuita. Pro porta l'egress a 250 GB e aggiunge i backup giornalieri di Supabase, conservati sette giorni, che si ripristinano con un pulsante. Lascia il workflow attivo accanto a loro, perché quelle copie vivono dentro l'account.
  • data.sql si avvicina ai 100 MiB. Spostarlo in un bucket vuol dire più YAML, e più cose che qualcuno deve tenere in funzione.
  • Nessuno ha mai ripristinato una copia. Il workflow continuerà a scrivere file, che si aprano o no.
  • I tuoi utenti caricano file. Il workflow non li raggiunge, e il job che li raggiunge è un secondo job da tenere in vita.

Dove si inserisce Reeve Care

Care fa la copia secondo un orario, la tiene fuori dal tuo account Supabase e controlla ogni copia prima che conti.

  • Ogni giorno con Care, e più spesso con i piani superiori, senza niente nel tuo repository e senza connection string in una CI.
  • Riletta e contata. Ogni copia viene aperta e contata rispetto a ciò che ci è entrato, e la data nella tua dashboard è quella dell'ultima copia che ha superato il controllo, mai quella dell'ultima esecuzione.
  • I tuoi account ci sono, e anche i tuoi file caricati arrivano non appena colleghi una credenziale di Storage.
  • Ripristinare è un pulsante, e prima di sostituire qualsiasi cosa viene fatta una copia dello stato attuale.
  • Ogni copia si scarica come zip con schema.sql, data.sql e roles.sql, gli stessi tre file che fa questo workflow, più il numero di righe di ogni tabella.

Care conserva una copia del tuo database Supabase. La copia, il controllo e il ripristino sono disegnati passo per passo nella pagina sui backup di Supabase, e cosa include ogni piano è nella pagina dei prezzi.

Cosa fare questa settimana

Cosa fare

  • Controlla la dimensione del tuo database e l'egress del mese scorso, e scegli l'orario dalla tabella prima di scrivere la riga del cron.
  • Crea un repository privato che contenga solo il workflow, e metti la stringa del Session pooler nel suo secret SUPABASE_DB_URL.
  • Se hai cominciato dall'esempio di Supabase, cancella i trigger di push e pull request e sposta il cron fuori dall'ora esatta.
  • Avvialo una volta a mano dalla scheda Actions, poi cerca in data.sql auth.users e una tabella che sai avere delle righe.
  • Ripristina una copia in un progetto usa e getta ed entra come un utente vero.
  • Copia i tuoi file di Storage con un job a parte.

Prima di chiudere questa scheda, apri in Supabase la pagina Usage della tua organizzazione e leggi l'egress del mese scorso. Quella cifra, accanto alla dimensione del tuo database, sceglie la tua riga del cron. E se stai ancora confrontando la strada gratuita con quelle a pagamento, i quattro tipi di strumento di backup per Supabase sono messi a confronto uno accanto all'altro.

FAQ

Posso fare il backup di Supabase gratis?

Sì. Supabase documenta un workflow di GitHub Actions che installa la sua CLI, esporta ruoli, schema e dati in tre file secondo un orario e ne fa il commit nel repository. Un repository privato su GitHub Free riceve 2.000 minuti di Actions al mese, e un dump notturno di un database piccolo ne usa una piccola parte. Quello che il backup consuma davvero è la tua quota di egress su Supabase, perché ogni esecuzione scarica l'intero database.

Ogni quanto dovrebbe girare un cron di backup di Supabase?

Tanto spesso quanto la tua quota di egress può pagare. Moltiplica la dimensione del database per le esecuzioni del mese: un database da 100 MB esportato ogni giorno sposta circa 3 GB, uno da 500 MB circa 15 GB. Il piano gratuito include 5 GB per tutta l'organizzazione, condivisi con il traffico della tua app. Metti il job su un minuto che non sia allo scoccare dell'ora, perché GitHub dice che le esecuzioni programmate all'inizio di ogni ora possono essere ritardate, e alcune scartate.

Un backup di Supabase conta contro il mio egress?

Sì. Supabase conta come egress i dati che uno qualsiasi dei suoi servizi manda all'esterno, e un dump attraverso il connection pooler viene registrato come Shared Pooler Egress, nella stessa quota del tuo traffico API. La quota appartiene a tutta l'organizzazione, non a un singolo progetto. Sul piano gratuito, superarla più volte porta a restrizioni su ogni progetto dell'organizzazione.

Perché il mio workflow di backup fallisce alla prima esecuzione?

Ci sono due errori tipici di questa configurazione. Il primo è la connection string: la connessione diretta di Supabase usa IPv6 a meno che tu non paghi l'add-on IPv4, e Supabase mette GitHub Actions tra i servizi che raggiungono solo IPv4, quindi usa la stringa del Session pooler. Il secondo colpisce i workflow che chiamano pg_dump direttamente: il client PostgreSQL 16 del runner si rifiuta di esportare un progetto su Postgres 17 e si ferma con aborting because of server version mismatch.

Posso fare il commit del mio backup di Supabase nel mio repository GitHub?

In uno privato sì, ed è quello che fa il workflow di Supabase. In uno pubblico mai, e Supabase lo dice due volte nella stessa pagina. Dagli un repository tutto suo che nessun altro possa leggere, perché il file dei dati contiene gli indirizzi email dei tuoi utenti. E mettiti in conto di spostarlo quando il database cresce: GitHub avvisa sopra i 50 MiB e rifiuta i file oltre i 100 MiB.

Come faccio a sapere se il file di backup è buono?

Ripristinalo. Un'esecuzione verde significa che il job è finito senza errori, e un dump del progetto sbagliato o un dump senza nessuna riga finiscono allo stesso modo. Riproduci i tre file in un progetto Supabase usa e getta o in uno locale, confronta il numero di righe delle tabelle che ti interessano ed entra come un utente vero. È questo il controllo che ti dice se il file si apre.

Scritto da

Vlad Tkachenko

Fondatore di Reeve

Passo le giornate a guardare app costruite con Lovable, Bolt, v0, Cursor e Replit, e la breve lista di errori che vi ricompaiono di continuo.

Altro sull'autore

Da leggere dopo

Tutti gli articoli

Non sai come sta messa la tua app?

Esegui una scansione gratuita e ottieni un voto chiaro da A a F in una ventina di secondi. Senza account e senza carta.

Scansiona la tua app gratis

Controllo esterno automatizzato, non un audit completo. L'assenza di risultati non è una garanzia di sicurezza.