ERPNext su VPS con Docker: guida al self-hosting
Guida a ERPNext su VPS con Docker: dimensionamento, stack da undici container, TLS, email in uscita, versioni bloccate e ripristino dei backup verificato.
Cosa comporta questa configurazione
Gestire ERPNext in modalità self-hosted su un VPS è un'attività operativa, non un'installazione eseguibile con un solo comando. Lo stack Docker Compose ufficiale contiene undici container e gestisce il libro mastro e i dati dei clienti. Questo innalza il livello di attenzione richiesto per tutto ciò che segue: un backup non è un backup finché non ne hai verificato il ripristino, e un tag di immagine non fissato a una versione specifica può causare una migrazione dello schema.
Nel testo ricorrono alcuni nomi. ERPNext è l'applicazione gestionale. Frappe è il framework Python sottostante. Bench è lo strumento da riga di comando che gestisce i siti ed è già installato nei container. Un site è 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, operando su un site specifico.
Questa guida usa il repository frappe_docker, che contiene la distribuzione gestita dal progetto. Tutti i comandi riportati di seguito sono stati verificati rispetto a quel repository nell'agosto 2026. Se Docker Compose è una novità, eseguire Docker Compose su un VPS tratta i prerequisiti dati per acquisiti da questa guida.
Di quante risorse VPS ha bisogno ERPNext?
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
}
]La documentazione pubblicata parte 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 effettuate in questa guida, e il numero effettivo dipende dalla quantità di documenti. L'ultima riga non rappresenta affatto un requisito minimo pubblicato. Indica più o meno il punto in cui la memoria smette di essere il primo fattore da considerare.
Valutate con realismo i piani più piccoli. Un VPS con 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 rientrare in quella memoria. L'arresto non avviene in modo controllato. Il kernel out of memory killer termina 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 già inviato può rimanere con l'elaborazione in background completata 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 si riempie più rapidamente del previsto, perché ogni allegato e ogni backup locale viene salvato nello 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 poi terminano. Da qui deriva il totale di undici container.
backendesegue l'applicazione Frappe tramite gunicorn. È qui che risiedebench.frontendè nginx. Serve gli asset statici e inoltra tutto il resto al backend.queue-shortequeue-longsono worker RQ (Redis Queue). Eseguono job in background, come l'invio delle email, le importazioni e la generazione dei report.scheduleresegue i job basati sull'orario, inclusi i report pianificati e i documenti a ripetizione automatica.websocketè il processo socket.io che gestisce gli aggiornamenti in tempo reale nel browser.dbè MariaDB.redis-cacheeredis-queuesono due istanze Redis separate: una per la cache e una per la coda dei job.
È importante comprendere questa separazione, perché indica quale log consultare. Un'email bloccata è un problema del worker della coda, quindi docker compose logs -f queue-short è il comando corretto. Se una pagina viene caricata ma il badge delle notifiche non si aggiorna, il problema riguarda i websocket. Consultare i log di backend per uno dei due problemi fa perdere tempo inutilmente.
Installare con i file Compose per la produzione, non con quelli demo
Il repository include pwd.yml e il README lo specifica chiaramente: "Questa configurazione è destinata esclusivamente a valutazioni di breve durata. Non sarà possibile installare app personalizzate in questa configurazione." Usalo per provare ERPNext per un pomeriggio. Non usarlo per gestire l'infrastruttura IT 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.envApri ~/gitops/erpnext.env e modifica quattro valori. ERPNEXT_VERSION fissa il tag dell'immagine. DB_PASSWORD nel file di esempio è impostato su 123. 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.comOra genera un unico file Compose, quindi avvialo.
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 -dconfig non avvia nulla. Unisce il file di base agli override e stampa il risultato con tutte le variabili già sostituite. Esegui quindi il file generato. Questo passaggio aggiuntivo è utile: lo stack in esecuzione è descritto da un unico file che puoi leggere e salvare nel repository, quindi non può cambiare senza che tu lo rilevi se qualcuno modifica il file env o se aggiorni il repository. come vengono uniti più file Docker Compose spiega in dettaglio le regole degli override.
Attendi che db si avvii e che configurator termini, operazione che richiede alcuni secondi, quindi crea 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.comVerifica il risultato:
docker compose --project-name erpnext ps
docker compose --project-name erpnext exec backend bench --site erp.example.com list-appslist-apps dovrebbe stampare frappe e erpnext con le rispettive versioni. Un ps in stato operativo 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 viene considerato 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 su quella rete privata.
Il secondo problema riguarda il nome del sito. Per impostazione predefinita, il frontend sceglie quale sito pubblicare in base all'header HTTP Host, quindi un sito creato come erpnext non è raggiungibile su erp.example.com, anche se entrambi esistono. Assegna al sito il nome del dominio, come nell'esempio precedente, oppure imposta FRAPPE_SITE_NAME_HEADER nel file env con il nome del sito e genera nuovamente il file Compose.
HTTPS e condizioni necessarie
L'override compose.https.yaml esegue Traefik sulla porta 443, reindirizza la porta 80 verso di essa e richiede i certificati a Let's Encrypt. TLS (sicurezza del livello di trasporto) impedisce che una fattura e un cookie di sessione transitino in chiaro sulla rete.
Devono essere vere due condizioni, altrimenti il certificato non viene mai emesso. Il record DNS A per erp.example.com deve puntare già al VPS. Le porte 80 e 443 devono essere raggiungibili da Internet, perché Let's Encrypt verifica il controllo del 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 è spesso quello che viene dimenticato.
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 (ambiente di gestione automatica dei certificati). Esegui altre applicazioni web sullo stesso server? una singola istanza Traefik davanti a più applicazioni Docker Compose mostra come condividere il proxy invece di contendersi la porta 443. La seconda applicazione su un server di questo tipo è spesso destinata ai clienti e un help desk Chatwoot self-hosted viene eseguito dietro lo stesso proxy, così le persone che gestiscono le fatture possono rispondere nello stesso posto alle email e alle chat dei clienti.
Posta in uscita, altrimenti le fatture non lasciano mai il server
Questo è il passaggio che la maggior parte delle guide su ERPNext omette e che determina se il sistema è realmente utilizzabile. Senza un servizio di posta in uscita funzionante, nessuna fattura raggiunge il cliente, nessun messaggio per il recupero della password viene recapitato e nessun report pianificato viene consegnato. Lo stack non include un mail server.
Non provare a inviare la posta direttamente dal VPS sulla porta 25. La maggior parte dei provider blocca la porta 25 in uscita sui nuovi account. Inoltre, i messaggi che riescono a uscire vengono rifiutati o finiscono nello spam, perché un indirizzo di un VPS appena attivato non ha una reputazione di invio. Usa un relay autenticato sulla porta 587.
Il percorso supportato è la schermata Email Account dell'interfaccia ERPNext, che memorizza la password in forma cifrata. Puoi anche scrivere 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.jsonImposta mail_password dalla schermata Email Account, invece che dalla riga di comando, in modo che venga memorizzato cifrato e non finisca mai nella cronologia della shell.
Invia quindi un messaggio reale. Crea una Sales Invoice, inviala tramite posta elettronica a un indirizzo che controlli e monitora la coda durante l'operazione:
docker compose --project-name erpnext logs -f queue-shortLa posta in uscita è un job in background. Di conseguenza, un messaggio che non arriva viene normalmente registrato 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 mail server Mailcow self-hosted ti fornisce un relay sotto il tuo controllo, su un server separato da quello ERP.
Backup che possono essere realmente ripristinati
Un dump del database, da solo, non è 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-filesQuesto comando scrive quattro file in sites/erp.example.com/private/backups all'interno del volume sites:
- un dump
-database.sql.gz - un archivio
-files.tardei file pubblici - un archivio
-private-files.tardei file privati - una copia
-site_config_backup.jsondella configurazione del sito
Il quarto file è quello che spesso viene eliminato, ma è anche quello più importante. Contiene encryption_key, la chiave che Frappe usa per crittografare 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 e mostra:
frappe.exceptions.ValidationError: Encryption key is invalid! Please check site_config.jsonConserva sempre tutti e quattro i file insieme.
Poi trasferiscili fuori dal server. Un backup conservato nel volume non sopravvive alla perdita del server e bench lo elimina comunque: per impostazione predefinita, cancella 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-backupsEsegui questo comando tramite cron, quindi trasferisci la directory in una posizione che non amministri direttamente. backup restic crittografati su storage esterno è lo strumento appropriato, perché crittografa i dati prima del caricamento e restic check verifica che il repository sia ancora leggibile. Un backup di ERP è una copia dell'intero registro contabile, quindi deve essere crittografato a riposo e conservato su hardware diverso da questo server.
Verifica il ripristino prima di averne bisogno
Un backup non verificato è solo un'ipotesi. Verificalo su un secondo sito nello 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 cifratura dalla configurazione di cui hai eseguito il backup al sito ripristinato; altrimenti le relative integrazioni resteranno non funzionanti:
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 finale con quello del sito in produzione. Apri una fattura di acquisto recente e scarica il relativo allegato. Il fatto che un sito visualizzi la pagina di accesso non dimostra nulla.
Rimuovi il sito di test al termine:
docker compose --project-name erpnext exec backend \
bench drop-site restore-test.example.comPerché il version pinning è più importante per ERPNext
Su un sito statico, un tag dell'immagine non fissato comporta un riavvio imprevisto. Su 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 nell'eseguire un docker compose down.
Fissa quindi il tag. ERPNEXT_VERSION=v16.32.1 era la release fissata nel pwd.yml del repository nell'agosto 2026. Non mantenere quel numero senza averlo verificato. Le release correnti sono elencate nella pagina delle release di frappe/erpnext, mentre i tag delle immagini disponibili sono pubblicati 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 onModifica 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 offLa 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 manualmente i record.
Passa a una major version alla volta, eseguendo un backup tra un passaggio e l'altro. Il codice di migrazione incluso in una release è progettato per aggiornare dalla release precedente; saltare una major version esegue quindi le migrazioni in una combinazione che nessuno ha verificato.
Il repository include anche overrides/compose.migrator.yaml, che aggiunge un container in cui viene eseguito bench --site all migrate a ogni avvio. È comodo. Significa però anche che un docker compose up con un tag modificato può migrare il database di produzione senza che nessuno monitori l'operazione. Su un sistema aziendale, esegui migrate solo come decisione consapevole presa quella mattina.
Hardening di un server che contiene dati dei clienti
Cambia la password dell'Administrator al primo accesso. Il file Compose di valutazione usa admin come password, e questa abitudine può arrivare fino all'ambiente di produzione.
Cambia DB_PASSWORD rispetto a 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 di secret Docker invece che da una variabile d'ambiente. gestione dei file env e dei secret in Docker Compose illustra i compromessi.
Pubblica solo ciò che serve. Con l'override HTTPS, le sole 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 sarebbe 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 in Impostazioni di sistema per ogni account con il ruolo System Manager. Questo ruolo può leggere ogni documento ed esportare ogni tabella, quindi trattalo come un account amministrativo e non come una semplice comodità. Se esegui diverse 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 riavvialo per installare gli aggiornamenti del kernel. Prima di fare affidamento sul riavvio dello stack, controlla nel file generato la presenza di una policy restart per ogni servizio, perché senza questa policy lo stack resta inattivo dopo il riavvio. avviare di nuovo uno stack Docker Compose dopo un riavvio descrive 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 e le email e le importazioni arrivano con minuti o ore di ritardo.
docker inspectsegnala container con"OOMKilled": trueo codice di uscita 137.- I report che richiedevano due secondi ora ne richiedono trenta e MariaDB è il processo che utilizza la CPU.
- I backup richiedono così tanto tempo che un'esecuzione si sovrappone a quella pianificata successivamente.
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 application server più potente aiuta meno 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 sottrarre risorse agli altri durante l'intervento.
Successivamente, aggiungi worker per le code invece di aumentare la capacità web. Le attività lente di ERPNext vengono eseguite in background: generazione di report e importazioni massive. Aggiungere altri container per i worker costa meno di un server più potente e risolve 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 solo alla valutazione. Per un'azienda che lo utilizza ogni giorno, prevedi 8 GB e 4 vCPU con 100 GB di SSD. Al di sotto di questi valori, l'out-of-memory killer del kernel arresta i container sotto carico; docker inspect lo segnala come "OOMKilled": true con codice di uscita 137. Si tratta di valori iniziali, non di misurazioni: monitora quindi l'utilizzo effettivo della memoria durante il primo mese.
Posso eseguire pwd.yml in produzione?
No. Il README del progetto lo descrive come destinato esclusivamente a valutazioni di breve durata e specifica che non è possibile installarvi app personalizzate. Usa compose.yaml con gli override per MariaDB, Redis e HTTPS, generali in un unico file con docker compose config ed esegui quel file.
Perché il mio sito ERPNext non è raggiungibile subito dopo la creazione?
Per impostazione predefinita, il frontend sceglie quale sito pubblicare in base all'header HTTP Host, quindi il nome del sito deve corrispondere al dominio inserito nel browser. Un sito creato come erpnext non viene pubblicato su erp.example.com. Crea il sito usando il dominio come nome oppure imposta FRAPPE_SITE_NAME_HEADER nel file env sul nome del sito, genera nuovamente il 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 della configurazione -site_config_backup.json. L'esecuzione di bench --site erp.example.com backup --with-files produce tutti e quattro i file. La copia della configurazione contiene encryption_key; senza questo file, durante il ripristino le password delle integrazioni memorizzate non possono essere decrittografate e il problema si manifesta come 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, genera nuovamente il 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 devi ripristinare il backup creato all'inizio.