SSD Nodes Learn Hosting plans →
How to do am Matt ConnorBy Matt Connor · Updated 2026-08-29

How to Host ERPNext for VPS with Docker

Set up ERPNext for your own VPS with Docker: eleven containers, sizing, TLS, outbound email, pinned versions, and a restore test wey really work.

Wetin you dey agree to run

To self-host ERPNext for VPS na operations work, e no be one-command installation. The official Docker Compose stack get eleven containers, and e dey store your general ledger and customer records. This one raise the standard for everything wey follow: backup no be backup until you don restore am, and image tag wey you no pin na schema migration wey dey wait to happen.

Some names go show throughout this guide. ERPNext na the business application. Frappe na the Python framework wey dey underneath am. Bench na the command line tool wey dey manage sites; dem don already install am inside the containers. A site na one tenant: one MariaDB database plus one directory for uploaded files. Almost every command for here dey run bench inside the backend container against one named site.

This guide dey use the frappe_docker repository, wey na the deployment wey the project dey maintain. We check every command below against that repository for August 2026. If Docker Compose still new to you, run Docker Compose for VPS go cover the basics wey this guide assume.

How much VPS ERPNext need?

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
  }
]

Published guidance start from 2 vCPU and 4 GB RAM before even one user log in. Na the evaluation tier be this. These figures na starting points, no be measurements from this guide, and the amount of document wey you get go decide the real number. The last row no be published minimum at all. Na roughly the point where memory stop being the main thing wey you dey worry about.

Make you dey honest with yourself about the small plans. 1 GB or 2 GB VPS go start the stack, then e go crash for the first import or first long report, because nine long-running containers plus MariaDB buffer pool plus Python worker wey dey build report no fit enter that memory. The failure no dey graceful. Kernel out-of-memory killer go stop one container, and docker inspect on top of am go show "OOMKilled": true with exit code 137. If worker die halfway through job, submitted document fit remain with the background work only half complete.

For company wey dey use ERPNext every day, 8 GB RAM and 4 vCPU with 100 GB SSD na the honest minimum. RAM go finish first. Disk dey grow faster than people expect, because every attachment and every local backup dey enter the same volume wey database dey use.

Di container eleven dem, and wetin each one dey do

Run docker compose ps after the stack don come up and nine containers dey run. Two more, configurator and create-site, go do their work once and exit. Na from there the total of eleven come.

  • backend dey run the Frappe application under gunicorn. Na here bench dey.
  • frontend na nginx. E dey serve static assets and pass everything else go the backend.
  • queue-short and queue-long na RQ (Redis Queue) workers. Dem dey run background jobs like outgoing email, imports and report builds.
  • scheduler dey run the time-based jobs, including scheduled reports and auto repeat documents.
  • websocket na the socket.io process wey dey behind live updates for browser.
  • db na MariaDB.
  • redis-cache and redis-queue na two separate Redis instances: one for cache and one for the job queue.

This split worth learning because e go tell you which log to read. If email hang, na queue worker problem, so docker compose logs -f queue-short na the correct command. If page load but notification badge no ever update, na websocket problem. To read backend logs for either case na to waste afternoon.

Install with production compose files, no be use demo

The repository release pwd.yml, and README talk am plainly: "This setup na only for short-term evaluation. You no go fit install custom apps for this setup." Use am to inspect ERPNext for one afternoon. No run company business on top am.

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

Open ~/gitops/erpnext.env and change four values. ERPNEXT_VERSION lock the image tag. DB_PASSWORD release as 123 for the example file. SITES_RULE na the Traefik routing rule, and LETSENCRYPT_EMAIL dey receive certificate warnings.

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

Now render one compose file, then start am.

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 no start anything. E merge the base file with the override files and print the result after e substitute every variable. Then run that rendered file. This extra step dey useful: the running stack become one file wey you fit read and commit, so e no fit change by itself when person edit the env file or when you pull the repository. how several Docker Compose files merge explain the override rules in detail.

Wait for db to start and for configurator to exit. E go take a few seconds. Then create the site.

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

Check am:

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

list-apps suppose print frappe and erpnext with their versions. A healthy ps dey show nine services for running state and none for restarting.

Two things dey fail often here. --mariadb-user-host-login-scope=% no be optional under Docker. The app container dey reach MariaDB through the Docker network, so e dey connect as remote host. Database user wey scope to localhost no fit log in from there. Site creation go then fail with MariaDB access denied error wey mention the root user. The % scope give the new site's user access from any host for that private network.

The second issue na the site name. By default, the frontend choose which site to serve from the HTTP Host header. So site wey you create as erpnext no go reachable at erp.example.com, even though both sites dey exist. Name the site after the domain, like above, or set FRAPPE_SITE_NAME_HEADER inside the env file to the site name, then render the compose file again.

HTTPS, and wetin suppose dey true before e work

The compose.https.yaml override dey run Traefik for port 443, redirect port 80 go am, and request certificates from Let's Encrypt. TLS (transport layer security) na wetin stop invoice and session cookie from pass through network as plain text.

Two things suppose dey true, otherwise no certificate go ever issue. DNS A record for erp.example.com must already point to the VPS. Ports 80 and 443 must dey reachable from internet, because Let's Encrypt dey prove say you control the name with HTTP-01 challenge for port 80. Check your provider network firewall, plus the one for the server. Dem na separate controls, and na the panel firewall people dey often forget.

Certificates dey enter cert-data volume for /letsencrypt/acme.json. If browser show default certificate instead of your own, find proxy service name for docker compose --project-name erpnext ps and read its logs for the ACME (automatic certificate management environment) error. You dey run other web apps for the same server? one Traefik instance in front of several Docker Compose apps show how to share the proxy instead of competing for port 443. The second app for server like this often dey face customers, and a self-hosted Chatwoot support desk dey behind that same proxy, so people wey dey handle invoices fit answer customer email and chat for one place.

Email wey dey go out, or the invoices no go ever leave the server

Na this step most ERPNext guides dey skip, and na e dey decide whether the system useful. If outbound mail no dey work, no invoice go reach customer, no password reset go arrive, and no scheduled report go deliver. This stack no get mail server.

No try send mail direct from the VPS through port 25. Most providers dey block outbound port 25 for new accounts. Any mail wey manage pass fit still get rejected or enter spam, because fresh VPS address no get sending reputation. Use authenticated relay for port 587.

The supported method na the Email Account screen for ERPNext interface. E dey store the password encrypted. You fit also write the keys inside site config:

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 dey store 587 as number instead of the string "587". Read the file again and confirm say those two values no get quotes around dem:

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

Set mail_password through the Email Account screen instead of command line. This one go store am encrypted, and e no go enter your shell history.

Then send one real message. Create a Sales Invoice, email am go an address wey you control, and monitor the queue as you dey do am:

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

Outgoing mail na background job, so if message no arrive, e usually go show as failed job for that log instead of error for browser. Publish SPF (sender policy framework) and DKIM (domainkeys identified mail) records for the sending domain too, then add DMARC policy. Without dem, invoice wey technically correct fit still enter customer's spam folder. If you prefer to control the whole path, self-hosted Mailcow mail server go give you relay wey you control, for separate box from the ERP.

Backups wey fit really restore

Database dump by itself no be backup for ERPNext. Attachments and private files dey inside sites directory, no be MariaDB. If you restore only the database, every purchase order wey dem upload go come back as broken link.

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

This one go write four files inside sites/erp.example.com/private/backups for sites volume:

  • -database.sql.gz dump
  • -files.tar archive of public files
  • -private-files.tar archive of private files
  • -site_config_backup.json copy of site config

Na the fourth file people dey throw away, but na am dey cause serious problem. E hold encryption_key, the key wey Frappe dey use to encrypt stored passwords: email account credentials, payment gateway keys, and every integration secret. If you restore database without the matching key, the site go load normally, but sending mail go fail with:

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

Keep all four files together every time.

After that, move dem comot from the server. Backup wey dey inside the volume no go survive if the server spoil, and bench go prune am anyway: by default, e dey delete backups wey pass 24 hours from that directory.

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

Run this from cron, then push the directory go somewhere wey you no dey administer. encrypted restic backups to off-site storage na the correct tool, because e dey encrypt files before upload, and restic check proves say the repository still dey readable. ERP backup na copy of your complete ledger, so e suppose dey encrypted at rest for hardware wey no be this one.

Test the restore before you need am

Backup wey you never test na guesswork. Test am for second site wey dey the same box. Never test am for live site.

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>'

Copy the encryption key from the backed up config enter the restored site. If you no do am, the integrations no go work:

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

Now check the restore as accountant for do am. Open the Accounts Receivable report and compare the closing balance with the live site. Open recent purchase invoice and download the attachment. If site fit show login page, that one no prove anything.

Remove the test site when you don finish:

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

Version pinning matter more for ERPNext

For static site, unpinned image tag mean surprise restart. For ERPNext, e mean schema migration. bench migrate dey rewrite database tables and fit rewrite document data, and e no get undo. To roll back, na restore from backup, no be docker compose down.

So pin the tag. ERPNEXT_VERSION=v16.32.1 na the release wey repository own pwd.yml pin for August 2026. No carry that number forward without checking am. Current releases dey listed for the frappe/erpnext releases page, and image tags wey dey available dey for Docker Hub. Read the notes for the version wey you wan move to before you move.

The upgrade itself start with backup and maintenance mode.

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

Edit ERPNEXT_VERSION for ~/gitops/erpnext.env, then render, pull and migrate.

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

Maintenance mode matter because migrate dey alter the schema while e dey run. If user submit document against table wey migration never finish, na so you go end up repairing records by hand.

Move one major version at a time, with backup between each step. The migration code for one release dey written to upgrade from the release before am. So, if you skip major versions, migrations go run for combination wey nobody test.

The repository also releases overrides/compose.migrator.yaml, wey add container wey dey run bench --site all migrate every time e start. E convenient. But e also mean say docker compose up with changed tag fit migrate your production database while nobody dey monitor am. For business system, run migrate as decision wey you make that morning.

Hardening a box wey dey hold customer records

Change the Administrator password for the first login. The evaluation compose file releases admin as that password, and people fit carry this habit enter production.

Change DB_PASSWORD comot from the 123 wey dey inside example.env. That value go end up as plain text for the rendered ~/gitops/erpnext.yaml, so chmod 600 the file and no keep am for any git repository. If you want something stronger, overrides/compose.mariadb-secrets.yaml fit read the password from a Docker secret file instead of an environment variable. how to handle env files and secrets for Docker Compose explain the trade-offs.

Publish only the things you need. With the HTTPS override, na ports 80 and 443 only dey exposed. No add ports mapping to the db service just to make database client connection easier: that one go put MariaDB for public internet. Use docker compose --project-name erpnext exec backend bench mariadb instead. For the host, allow 22, 80 and 443, deny every other one, and check the provider separate network firewall too.

Turn on two factor authentication for System Settings for every account wey get the System Manager role. That role fit read every document and export every table, so treat am as administrator account, no be convenience account. If you dey run many self-hosted apps, Authentik as self-hosted single sign-on provider better pass adding another password for every app.

Patch the host and reboot am when kernel updates dey involved. Before you depend on the stack to come back, check the rendered file for a restart policy on every service, because stack wey no get one go remain down after that reboot. how to make Docker Compose stack start again after reboot cover the systemd side.

Wen ERPNext stop dey comfortable for one VPS

One VPS fit carry small company for long time. You go know say e don reach limit when:

  • Background jobs dey pile up, so emails and imports dey arrive minutes or hours late.
  • docker inspect dey report containers with "OOMKilled": true or exit code 137.
  • Reports wey dey take two seconds before dey take thirty, and MariaDB na the process wey dey use CPU.
  • Backups dey run long enough make one overlap the next scheduled run.

Start by giving MariaDB resources wey e no go share, because database and Python workers dey compete for the same memory. Buffer pool na the part wey need more memory. Bigger application server no dey help as much as people expect. running database for Docker or for host explain that decision, while setting memory limits for Docker Compose prevent one container from starving the others as you dey make the change.

After that, add queue workers instead of web capacity. ERPNext slow work na background work: report generation and bulk imports. More worker containers cost less than bigger server, and dem fix the problem wey users dey actually complain about.

FAQ

How much RAM ERPNext need for VPS?

Published guidance start from 4 GB with 2 vCPU, and na evaluation only for that tier. For company wey dey use am every day, plan for 8 GB and 4 vCPU with 100 GB SSD. If e fall below that, kernel out of memory killer go stop containers when load high. docker inspect dey report this as "OOMKilled": true with exit code 137. Treat these figures as starting points, no be exact measurements. Monitor your own memory use during the first month.

I fit run pwd.yml for production?

No. The project README describe am as something for short-lived evaluation only, and e note say you no fit install custom apps inside am. Use compose.yaml with the MariaDB, Redis and HTTPS overrides. Render dem into one file with docker compose config, then run that file.

Why my ERPNext site no dey reachable immediately after I create am?

By default, frontend choose which site to serve from the HTTP Host header. So the site name must match the domain wey dey inside browser. Site wey you create as erpnext no dey serve for erp.example.com. Either create the site with the domain as the name, or set FRAPPE_SITE_NAME_HEADER for the env file to the site name. Render the compose file again, then restart the stack.

Wetin must dey inside ERPNext backup?

Four files, and you must keep dem together: the -database.sql.gz dump, the -files.tar and -private-files.tar archives, and the -site_config_backup.json config copy. Running bench --site erp.example.com backup --with-files go produce all four. The config copy contain encryption_key. So if you restore without am, stored integration passwords no fit decrypt, and the error go show as Encryption key is invalid! Please check site_config.json.

How I fit upgrade ERPNext without spoil my data?

Back up with --with-files. Turn on maintenance mode. Change ERPNEXT_VERSION for your env file. Render the compose file again, pull, bring the stack up, then run bench --site erp.example.com migrate and turn maintenance mode off. Move one major version at a time and read the release notes first, because migrate dey rewrite schema and document data without any undo. To roll back, restore the backup wey you take at the beginning.