SSD Nodes Learn 🎉 VPS da $5.50/mese
Guide Matt ConnorDi Matt Connor

ERPNext su VPS con Docker: guida al self-hosting

Scopri come gestire ERPNext su un VPS: stack Docker Compose da 11 container, TLS, email in uscita, versioni fissate e ripristino del backup testato.

Che cosa stai per eseguire

Gestire ERPNext in self-hosting su un VPS è un'attività operativa, non un'installazione eseguibile con un solo comando. Lo stack ufficiale Docker Compose comprende undici container e contiene il libro mastro generale e le anagrafiche clienti. Questo innalza il livello richiesto per tutto ciò che segue: un backup non è un backup finché non ne hai eseguito il ripristino e un tag di immagine non fissato è una migrazione dello schema in attesa di verificarsi.

Nel corso della guida ricorrono alcuni nomi. ERPNext è l'applicazione aziendale. Frappe è il framework Python sottostante. Bench è lo strumento da riga di comando che gestisce i siti ed è già installato nei container. Un sito è un tenant: un database MariaDB e una directory contenente i file caricati. Quasi tutti i comandi riportati qui vengono eseguiti bench all'interno del container backend su un sito specificato per nome.

Questa guida usa il repository frappe_docker, che corrisponde alla distribuzione gestita dal progetto. Ogni comando riportato di seguito è stato verificato rispetto a quel repository nell'agosto 2026. Se Docker Compose è una novità, eseguire Docker Compose su un VPS illustra i concetti presupposti da questa guida.

Di quante risorse VPS ha bisogno ERPNext?

ChartCommon published ERPNext sizing tiers (guidance, not a measurement)
The data behind this chart
[
  {
    "label": "Evaluation",
    "vcpu": 2,
    "ram_gb": 4,
    "disk_gb": 40
  },
  {
    "label": "Small production",
    "vcpu": 4,
    "ram_gb": 8,
    "disk_gb": 100
  },
  {
    "label": "Room to grow",
    "vcpu": 4,
    "ram_gb": 16,
    "disk_gb": 160
  }
]

Le indicazioni pubblicate partono da 2 vCPU e 4 GB di RAM prima ancora che acceda un singolo utente. Questo è il livello per la valutazione. Sono valori iniziali, non misurazioni ricavate da questa guida, e il numero effettivo dipende dal volume dei documenti. L'ultima riga non rappresenta affatto un minimo pubblicato. Indica approssimativamente il punto in cui la memoria smette di essere un aspetto a cui prestare attenzione.

Valutate con realismo i piani più piccoli. Un VPS da 1 GB o 2 GB avvia lo stack, ma poi si arresta al primo import o al primo report lungo, perché nove container a esecuzione prolungata, il buffer pool di MariaDB e un worker Python che genera un report non possono coesistere in quella quantità di memoria. L'arresto non avviene in modo ordinato. Il kernel out-of-memory killer arresta un container e docker inspect su quel container mostra quindi "OOMKilled": true con codice di uscita 137. Se un worker viene terminato durante un job, un documento inviato può rimanere con l'elaborazione in background eseguita solo parzialmente.

Per un'azienda che usa ERPNext ogni giorno, 8 GB di RAM, 4 vCPU e 100 GB di SSD rappresentano il limite minimo realistico. La RAM si esaurisce per prima. Il disco cresce più rapidamente del previsto, perché ogni allegato e ogni backup locale viene salvato sullo stesso volume del database.

Gli undici container e la funzione di ciascuno

Esegui docker compose ps quando lo stack è attivo e nove container sono in esecuzione. Altri due, configurator e create-site, eseguono il proprio lavoro una volta sola e terminano. Da qui deriva il totale di undici container.

  • backend esegue l'applicazione Frappe tramite gunicorn. Qui risiede bench.
  • frontend è nginx. Serve le risorse statiche e inoltra tutte le altre richieste al backend.
  • queue-short e queue-long sono worker RQ (Redis Queue). Eseguono job in background, come l'invio delle email, le importazioni e la generazione dei report.
  • scheduler esegue i job basati sull'orario, inclusi i report pianificati e i documenti con ripetizione automatica.
  • websocket è il processo socket.io alla base degli aggiornamenti in tempo reale nel browser.
  • db è MariaDB.
  • redis-cache e redis-queue sono due istanze Redis separate: una per la cache e una per la coda dei job.

Questa separazione è importante da comprendere, perché indica quale log consultare. Un'email bloccata è un problema del worker della coda, quindi docker compose logs -f queue-short è il comando corretto. Una pagina che viene caricata ma non aggiorna mai il contatore delle notifiche indica un problema del websocket. Consultare i log di backend per uno di questi problemi fa perdere tempo inutilmente.

Installare con i file Compose per la produzione, non con quelli dimostrativi

Il repository include pwd.yml e il README lo dichiara chiaramente: "Questa configurazione è destinata esclusivamente a valutazioni di breve durata. Non sarà possibile installare applicazioni personalizzate in questa configurazione." Usatela per valutare ERPNext per un pomeriggio. Non usatela per gestire l'operatività di un'azienda.

sudo apt update && sudo apt install -y git
curl -fsSL https://get.docker.com | bash
git clone https://github.com/frappe/frappe_docker
cd frappe_docker
mkdir -p ~/gitops
cp example.env ~/gitops/erpnext.env

Aprite ~/gitops/erpnext.env e modificate quattro valori. ERPNEXT_VERSION fissa il tag dell'immagine. DB_PASSWORD viene fornito come 123 nel file di esempio. SITES_RULE è la regola di routing di Traefik e LETSENCRYPT_EMAIL riceve gli avvisi relativi ai certificati.

ERPNEXT_VERSION=v16.32.1
DB_PASSWORD=<a long random password>
SITES_RULE=Host(`erp.example.com`)
LETSENCRYPT_EMAIL=ops@example.com

Ora generate un unico file Compose, quindi avviatelo.

docker compose --project-name erpnext \
  --env-file ~/gitops/erpnext.env \
  -f compose.yaml \
  -f overrides/compose.mariadb.yaml \
  -f overrides/compose.redis.yaml \
  -f overrides/compose.https.yaml \
  config > ~/gitops/erpnext.yaml

docker compose --project-name erpnext -f ~/gitops/erpnext.yaml up -d

config non avvia nulla. Unisce il file di base agli override e stampa il risultato con tutte le variabili già sostituite. Eseguite quindi il file generato. Questo passaggio aggiuntivo è utile: lo stack in esecuzione è descritto da un unico file che potete leggere e salvare nel repository, quindi non può cambiare senza che ve ne accorgiate quando qualcuno modifica il file env o esegue il pull del repository. come vengono uniti più file Docker Compose spiega in dettaglio le regole degli override.

Attendete l'avvio di db e l'uscita di configurator, che richiede alcuni secondi, quindi create il sito.

docker compose --project-name erpnext exec backend \
  bench new-site --mariadb-user-host-login-scope=% \
  --db-root-password '<your DB_PASSWORD>' \
  --install-app erpnext \
  --admin-password '<a strong admin password>' \
  erp.example.com

Verificate:

docker compose --project-name erpnext ps
docker compose --project-name erpnext exec backend bench --site erp.example.com list-apps

list-apps dovrebbe stampare frappe e erpnext con le rispettive versioni. Un ps integro mostra nove servizi nello stato running e nessuno nello stato restarting.

In questo passaggio si verificano spesso due problemi. --mariadb-user-host-login-scope=% non è facoltativo in Docker. Il container dell'applicazione raggiunge MariaDB attraverso la rete Docker, quindi si presenta come un host remoto; un utente del database limitato a localhost non può effettuare l'accesso da lì. La creazione del sito fallisce quindi con un errore di accesso negato di MariaDB che indica l'utente root. L'ambito % concede all'utente del nuovo sito l'accesso da qualsiasi host presente in quella rete privata.

Il secondo problema riguarda il nome del sito. Per impostazione predefinita, il frontend sceglie quale sito servire in base all'header HTTP Host. Di conseguenza, un sito creato come erpnext non è raggiungibile all'indirizzo erp.example.com, anche se entrambi esistono. Assegnate al sito il nome del dominio, come mostrato sopra, oppure impostate FRAPPE_SITE_NAME_HEADER nel file env sul nome del sito e generate nuovamente il file Compose.

HTTPS e condizioni necessarie per il funzionamento

L'override compose.https.yaml esegue Traefik sulla porta 443, reindirizza la porta 80 verso di esso e richiede i certificati a Let's Encrypt. TLS (transport layer security) impedisce che una fattura e un cookie di sessione transitino in rete in chiaro.

Devono essere vere due condizioni. In caso contrario, il certificato non viene emesso. Il record DNS A per erp.example.com deve già puntare al VPS. Le porte 80 e 443 devono essere raggiungibili da Internet, perché Let's Encrypt verifica che tu controlli il nome tramite una challenge HTTP-01 sulla porta 80. Controlla anche il firewall di rete del provider e quello del server. Sono controlli distinti e il firewall del pannello è quello che viene dimenticato più spesso.

I certificati vengono salvati nel volume cert-data in /letsencrypt/acme.json. Se il browser mostra un certificato predefinito invece del tuo, individua il nome del servizio proxy in docker compose --project-name erpnext ps e consulta i relativi log per trovare l'errore ACME (automatic certificate management environment). Se esegui altre applicazioni web sullo stesso server, una sola istanza di Traefik davanti a più applicazioni Docker Compose mostra come condividere il proxy invece di creare un conflitto sulla porta 443.

Posta elettronica in uscita, altrimenti le fatture non lasciano il server

Questo è il passaggio che la maggior parte delle guide su ERPNext omette, ma determina se il sistema è realmente utilizzabile. Senza una posta in uscita funzionante, nessuna fattura raggiunge il cliente, nessun messaggio per la reimpostazione della password viene recapitato e nessun report pianificato viene consegnato. Lo stack non include un server di posta.

Non provare a inviare messaggi direttamente dal VPS sulla porta 25. La maggior parte dei provider blocca la porta 25 in uscita per i nuovi account. Inoltre, i messaggi che riescono a uscire vengono rifiutati o finiscono nella cartella spam, perché l'indirizzo di un VPS appena creato non ha una reputazione di invio. Usa un relay autenticato sulla porta 587.

Il percorso supportato è la schermata Email Account nell'interfaccia di ERPNext, che memorizza la password in forma crittografata. Puoi anche inserire le chiavi nella configurazione del sito:

docker compose --project-name erpnext exec backend \
  bench --site erp.example.com set-config mail_server smtp.example.com

docker compose --project-name erpnext exec backend \
  bench --site erp.example.com set-config mail_port 587 --parse

docker compose --project-name erpnext exec backend \
  bench --site erp.example.com set-config use_tls 1 --parse

docker compose --project-name erpnext exec backend \
  bench --site erp.example.com set-config mail_login 'erp@example.com'

docker compose --project-name erpnext exec backend \
  bench --site erp.example.com set-config auto_email_id 'erp@example.com'

--parse memorizza 587 come numero invece che come stringa "587". Rileggi il file e verifica che questi due valori non siano racchiusi tra virgolette:

docker compose --project-name erpnext exec backend \
  cat sites/erp.example.com/site_config.json

Imposta mail_password dalla schermata Email Account anziché dalla riga di comando, in modo che venga memorizzato in forma crittografata e non finisca mai nella cronologia della shell.

Invia quindi un messaggio reale. Crea una Sales Invoice, inviala a un indirizzo che controlli e monitora la coda durante l'operazione:

docker compose --project-name erpnext logs -f queue-short

La posta in uscita è un job in background. Di conseguenza, un messaggio che non arriva compare di solito come job non riuscito in quel log, non come errore nel browser. Pubblica anche i record SPF (sender policy framework) e DKIM (domainkeys identified mail) per il dominio di invio, quindi aggiungi una policy DMARC. Senza questi record, anche una fattura tecnicamente corretta finisce comunque nella cartella spam del cliente. Se preferisci gestire internamente l'intero percorso, un server di posta Mailcow self-hosted ti fornisce un relay sotto il tuo controllo, su un server separato da quello di ERPNext.

Backup che vengono effettivamente ripristinati

Un dump del database, da solo, non costituisce un backup di ERPNext. Gli allegati e i file privati si trovano nella directory sites, non in MariaDB. Se ripristini soltanto il database, ogni ordine di acquisto caricato torna a essere un collegamento non valido.

docker compose --project-name erpnext exec backend \
  bench --site erp.example.com backup --with-files

Questo comando scrive quattro file in sites/erp.example.com/private/backups all'interno del volume sites:

  • un dump -database.sql.gz
  • un archivio -files.tar dei file pubblici
  • un archivio -private-files.tar dei file privati
  • una copia -site_config_backup.json della configurazione del sito

Il quarto file viene spesso eliminato, ma è proprio quello più importante. Contiene encryption_key, la chiave che Frappe usa per cifrare le password memorizzate: credenziali degli account email, chiavi dei gateway di pagamento e tutti i secret delle integrazioni. Se ripristini un database senza la chiave corrispondente, il sito si carica normalmente, ma l'invio delle email non funziona:

frappe.exceptions.ValidationError: Encryption key is invalid! Please check site_config.json

Conserva sempre insieme tutti e quattro i file.

Poi trasferiscili fuori dal server. Un backup conservato nel volume non sopravvive alla perdita del server e bench lo elimina comunque: per impostazione predefinita, elimina da quella directory i backup più vecchi di 24 ore.

docker compose --project-name erpnext cp \
  backend:/home/frappe/frappe-bench/sites/erp.example.com/private/backups \
  ~/erpnext-backups

Esegui questo comando tramite cron, quindi trasferisci la directory in una posizione che non amministri. backup restic cifrati su storage esterno è lo strumento adatto, perché cifra i dati prima dell'upload e restic check verifica che il repository sia ancora leggibile. Un backup di ERP è una copia dell'intero registro contabile, quindi deve essere cifrato a riposo e conservato su hardware diverso da questo.

Verifica il ripristino prima di averne bisogno

Un backup non verificato è solo un'ipotesi. Esegui il test in un secondo sito sullo stesso server, mai su quello in produzione.

docker compose --project-name erpnext exec backend \
  bench new-site --mariadb-user-host-login-scope=% \
  --db-root-password '<your DB_PASSWORD>' \
  --admin-password '<a strong admin password>' \
  restore-test.example.com

docker compose --project-name erpnext exec backend \
  bench --site restore-test.example.com --force restore \
  sites/erp.example.com/private/backups/<stamp>-erp.example.com-database.sql.gz \
  --with-public-files sites/erp.example.com/private/backups/<stamp>-erp.example.com-files.tar \
  --with-private-files sites/erp.example.com/private/backups/<stamp>-erp.example.com-private-files.tar \
  --db-root-password '<your DB_PASSWORD>'

Copia la chiave di crittografia dalla configurazione di cui hai eseguito il backup nel sito ripristinato, altrimenti le relative integrazioni non funzioneranno:

docker compose --project-name erpnext exec backend \
  bench --site restore-test.example.com set-config encryption_key '<value from site_config_backup.json>'

Ora verifica il ripristino come farebbe un contabile. Apri il report Accounts Receivable e confronta il saldo di chiusura con quello del sito in produzione. Apri una fattura di acquisto recente e scarica il relativo allegato. Un sito che visualizza la pagina di login non dimostra nulla.

Rimuovi il sito di test al termine:

docker compose --project-name erpnext exec backend \
  bench drop-site restore-test.example.com

Perché il version pinning è più importante per ERPNext

Su un sito statico, un tag dell'immagine non fissato comporta un riavvio imprevisto. In ERPNext comporta una migrazione dello schema. bench migrate riscrive le tabelle del database e può riscrivere i dati dei documenti; non è possibile annullare l'operazione. Il rollback consiste nel ripristinare un backup, non in un docker compose down.

Fissa quindi il tag. ERPNEXT_VERSION=v16.32.1 era la release fissata nel pwd.yml del repository ad agosto 2026. Non usare quel numero per le versioni future senza prima verificarlo. Le release attuali sono elencate nella pagina delle release di frappe/erpnext, mentre i tag delle immagini disponibili sono indicati su Docker Hub. Leggi le note della versione a cui vuoi passare prima di eseguire l'aggiornamento.

L'aggiornamento inizia con un backup e l'attivazione della modalità di manutenzione.

docker compose --project-name erpnext exec backend \
  bench --site erp.example.com backup --with-files

docker compose --project-name erpnext exec backend \
  bench --site erp.example.com set-maintenance-mode on

Modifica ERPNEXT_VERSION in ~/gitops/erpnext.env, quindi esegui il rendering, scarica l'immagine ed esegui la migrazione.

docker compose --project-name erpnext \
  --env-file ~/gitops/erpnext.env \
  -f compose.yaml \
  -f overrides/compose.mariadb.yaml \
  -f overrides/compose.redis.yaml \
  -f overrides/compose.https.yaml \
  config > ~/gitops/erpnext.yaml

docker compose --project-name erpnext -f ~/gitops/erpnext.yaml pull
docker compose --project-name erpnext -f ~/gitops/erpnext.yaml up -d

docker compose --project-name erpnext exec backend \
  bench --site erp.example.com migrate

docker compose --project-name erpnext exec backend \
  bench --site erp.example.com set-maintenance-mode off

La modalità di manutenzione è importante perché migrate modifica lo schema durante l'esecuzione. Se un utente invia un documento mentre una tabella è stata migrata solo parzialmente, potresti dover riparare i record manualmente.

Passa a una major version alla volta ed esegui un backup tra un passaggio e l'altro. Il codice di migrazione di una release è scritto per eseguire l'upgrade dalla release precedente. Saltare una major version significa eseguire migrazioni in una combinazione che nessuno ha testato.

Il repository include anche overrides/compose.migrator.yaml, che aggiunge un container che esegue bench --site all migrate a ogni avvio. È pratico. Significa anche che un docker compose up con un tag modificato può migrare il database di produzione senza che nessuno controlli l'operazione. Su un sistema aziendale, esegui migrate solo dopo aver preso questa decisione quella mattina.

Hardening di un server che contiene dati dei clienti

Cambia la password Administrator al primo accesso. Il file Compose di valutazione distribuisce admin come password, e questa abitudine può passare agli ambienti di produzione.

Cambia DB_PASSWORD rispetto al valore 123 in example.env. Questo valore finisce in chiaro nel ~/gitops/erpnext.yaml generato, quindi chmod 600 il file e non inserirlo in alcun repository Git. Per una soluzione più sicura, overrides/compose.mariadb-secrets.yaml legge la password da un file Docker secret invece che da una variabile d'ambiente. La guida gestione dei file env e dei secret in Docker Compose illustra i compromessi.

Pubblica solo ciò che serve. Con l'override HTTPS, le uniche porte esposte sono 80 e 443. Non aggiungere un mapping ports al servizio db per facilitare la connessione di un client al database: in questo modo MariaDB viene esposto su Internet. Usa invece docker compose --project-name erpnext exec backend bench mariadb. Sul server consenti 22, 80 e 443, nega tutte le altre porte e controlla anche il firewall di rete separato del provider.

Attiva l'autenticazione a due fattori nelle System Settings per ogni account con il ruolo System Manager. Questo ruolo può leggere ogni documento ed esportare ogni tabella, quindi trattalo come un account amministrativo, non come una semplice comodità. Se esegui più applicazioni self-hosted, Authentik come provider self-hosted di single sign-on è preferibile all'aggiunta di un'altra password per ogni applicazione.

Applica le patch al server e riavvia il sistema per installare gli aggiornamenti del kernel. Prima di fare affidamento sul riavvio automatico dello stack, controlla nel file generato la presenza di una policy restart per ogni servizio, perché uno stack privo di questa policy rimane arrestato dopo il riavvio. La guida riavvio di uno stack Docker Compose dopo un riavvio del sistema illustra la configurazione lato systemd.

Quando ERPNext non è più gestibile su un singolo VPS

Un VPS può supportare una piccola azienda per molto tempo. Questi segnali indicano che non è più sufficiente:

  • I job in background si accumulano, quindi le email e le importazioni arrivano con minuti o ore di ritardo.
  • docker inspect segnala container con "OOMKilled": true o codice di uscita 137.
  • I report che prima richiedevano due secondi ora ne richiedono trenta e MariaDB è il processo che utilizza la CPU.
  • I backup durano abbastanza da sovrapporsi all'esecuzione pianificata successiva.

Inizia assegnando a MariaDB risorse che non deve condividere, perché il database e i worker Python competono per la stessa memoria e il buffer pool è il componente che ne richiede di più. Un server applicativo più grande offre meno vantaggi di quanto ci si aspetti. eseguire il database in Docker o sull'host illustra questa scelta, mentre impostare i limiti di memoria in Docker Compose impedisce a un container di esaurire le risorse degli altri durante l'intervento.

Dopo questo intervento, aggiungi worker per le code invece di aumentare la capacità web. Le attività lente di ERPNext vengono eseguite in background: generazione dei report e importazioni massive. Più container worker costano meno di un server più grande e risolvono il problema di cui gli utenti si lamentano effettivamente.

FAQ

Quanta RAM richiede ERPNext su un VPS?

La documentazione pubblicata indica come punto di partenza 4 GB con 2 vCPU, ma questa configurazione è destinata soltanto alla valutazione. Per un'azienda che lo usa ogni giorno, prevedi 8 GB, 4 vCPU e 100 GB di SSD. Con risorse inferiori, l'out-of-memory killer del kernel arresta i container sotto carico. docker inspect lo segnala come "OOMKilled": true con codice di uscita 137. Questi valori sono punti di partenza, non misurazioni effettive. Monitora quindi l'utilizzo della memoria del tuo sistema durante il primo mese.

Posso eseguire pwd.yml in produzione?

No. Il README del progetto lo indica come adatto soltanto a valutazioni di breve durata e specifica che non è possibile installarvi app personalizzate. Usa compose.yaml con gli override per MariaDB, Redis e HTTPS, esegui il rendering in un unico file con docker compose config e avvia quel file.

Perché il mio sito ERPNext non è raggiungibile subito dopo la creazione?

Per impostazione predefinita, il frontend sceglie quale sito servire in base all'header HTTP Host. Il nome del sito deve quindi corrispondere al dominio usato nel browser. Un sito creato come erpnext non viene servito su erp.example.com. Crea il sito usando il dominio come nome oppure imposta FRAPPE_SITE_NAME_HEADER nel file env con il nome del sito, esegui nuovamente il rendering del file compose e riavvia lo stack.

Cosa deve contenere un backup di ERPNext?

Quattro file, conservati insieme: il dump -database.sql.gz, gli archivi -files.tar e -private-files.tar e la copia di configurazione -site_config_backup.json. L'esecuzione di bench --site erp.example.com backup --with-files produce tutti e quattro i file. La copia di configurazione contiene encryption_key. Senza questo file, un ripristino lascia impossibile decrittografare le password di integrazione memorizzate, con un errore Encryption key is invalid! Please check site_config.json.

Come posso aggiornare ERPNext senza danneggiare i dati?

Esegui il backup con --with-files, attiva la modalità di manutenzione, modifica ERPNEXT_VERSION nel file env, esegui nuovamente il rendering del file compose, esegui il pull, avvia lo stack, quindi esegui bench --site erp.example.com migrate e disattiva la modalità di manutenzione. Procedi una versione principale alla volta e leggi prima le note di rilascio, perché migrate riscrive lo schema e i dati dei documenti senza possibilità di annullamento. Per eseguire il rollback, ripristina il backup creato all'inizio.