What you will set up
ERPNext is an open-source ERP: accounting, invoices, customers, suppliers, stock, purchasing, projects and HR, in your browser. It is built on the Frappe framework, and all of it stays on your server.
Here it runs as a pod of eight containers under a user of its own called erpnext, behind Caddy from the Podman guide: MariaDB, two Redis servers, and ERPNext's web server, background worker, scheduler, realtime server and web front.
Every step below was run on a Melonslab VC-P Alloy (2 vCPU, 8 GB) with Debian 13:
- ERPNext 16.35.0 was installed, and for a company in Sweden the setup wizard chose SEK and the Swedish BAS 2024 chart of accounts by itself, with VAT templates for 25, 12, 6 and 0 percent.
- A customer, an item and an invoice were created, and the invoice downloaded as a PDF.
- ERPNext logged each login with the visitor's real address, and its realtime server answered through Caddy.
- ERPNext was updated to 16.36.0 with the steps in step 10, with the data intact, and everything came back by itself after a reboot.
ERPNext used about 710 MB of memory with one company and few users. The ERPNext project recommends 2 vCPU and 4 GB for a server running it.
Before you start
You need:
- a server set up as in the Podman guide, with Caddy running, and at least 4 GB of memory;
- an A record and an AAAA record for
erp.example.compointing at your server.
The examples use erp.example.com for ERPNext and 203.0.113.10 for your server's IPv4 address, which ip -brief address show eth0 shows. Replace them throughout.
1. Create the user
As root:
useradd -m -s /bin/bash erpnext
loginctl enable-linger erpnext
machinectl shell erpnext@
Everything from now on runs as erpnext, except step 7.
2. Create the database password
openssl rand -hex 24 | tr -d '\n' | podman secret create erpnext-db-root-password -
This is the password for MariaDB's root user. ERPNext uses it once, in step 6, to create its own database and database user.
3. Describe the pod
mkdir -p ~/.config/containers/systemd
cd ~/.config/containers/systemd
Create erpnext.pod:
[Pod]
PodName=erpnext
# Only Caddy, on this server, can reach ERPNext: the port is not open to the internet.
PublishPort=127.0.0.1:8093:8080
# ERPNext fetches its own pages when it makes PDFs: reach them through the host.
AddHost=erp.example.com:host-gateway
[Install]
WantedBy=default.target
Create erpnext-db.container:
[Container]
ContainerName=erpnext-db
Image=docker.io/library/mariadb:11.8
Pod=erpnext.pod
Volume=erpnext-db:/var/lib/mysql
Environment=MARIADB_AUTO_UPGRADE=1
Secret=erpnext-db-root-password,type=env,target=MARIADB_ROOT_PASSWORD
Exec=--character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --skip-character-set-client-handshake
AutoUpdate=registry
[Service]
Restart=always
Create erpnext-redis-cache.container:
[Container]
ContainerName=erpnext-redis-cache
Image=docker.io/library/redis:8.6-alpine
Pod=erpnext.pod
AutoUpdate=registry
[Service]
Restart=always
Create erpnext-redis-queue.container:
[Container]
ContainerName=erpnext-redis-queue
Image=docker.io/library/redis:8.6-alpine
Pod=erpnext.pod
Volume=erpnext-redis-queue:/data
# The cache already uses the default port in the pod.
Exec=redis-server --port 6380
AutoUpdate=registry
[Service]
Restart=always
The five ERPNext containers all run the same image, with the same sites volume, where ERPNext keeps its settings and uploaded files. Create erpnext-backend.container, the web server:
[Unit]
After=erpnext-db.service
[Container]
ContainerName=erpnext-backend
Image=docker.io/frappe/erpnext:v16.36.0
Pod=erpnext.pod
Volume=erpnext-sites:/home/frappe/frappe-bench/sites
[Service]
Restart=always
Create erpnext-worker.container, which runs background jobs:
[Unit]
After=erpnext-db.service
[Container]
ContainerName=erpnext-worker
Image=docker.io/frappe/erpnext:v16.36.0
Pod=erpnext.pod
Volume=erpnext-sites:/home/frappe/frappe-bench/sites
Exec=bench worker --queue long,default,short
[Service]
Restart=always
Create erpnext-scheduler.container, which starts scheduled jobs:
[Unit]
After=erpnext-db.service
[Container]
ContainerName=erpnext-scheduler
Image=docker.io/frappe/erpnext:v16.36.0
Pod=erpnext.pod
Volume=erpnext-sites:/home/frappe/frappe-bench/sites
Exec=bench schedule
[Service]
Restart=always
Create erpnext-websocket.container, which sends realtime updates to browsers:
[Unit]
After=erpnext-db.service
[Container]
ContainerName=erpnext-websocket
Image=docker.io/frappe/erpnext:v16.36.0
Pod=erpnext.pod
Volume=erpnext-sites:/home/frappe/frappe-bench/sites
Exec=node /home/frappe/frappe-bench/apps/frappe/socketio.js
[Service]
Restart=always
And erpnext-frontend.container, the web front that Caddy talks to:
[Unit]
After=erpnext-db.service
[Container]
ContainerName=erpnext-frontend
Image=docker.io/frappe/erpnext:v16.36.0
Pod=erpnext.pod
Volume=erpnext-sites:/home/frappe/frappe-bench/sites
Exec=nginx-entrypoint.sh
Environment=BACKEND=127.0.0.1:8000 SOCKETIO=127.0.0.1:9000
# Caddy's connections reach the pod from the server's own IPv4 address.
Environment=UPSTREAM_REAL_IP_ADDRESS=203.0.113.10
[Service]
Restart=always
In the pod, the containers share one network, so they reach each other on 127.0.0.1, and the second Redis moves to port 6380. The ERPNext image is pinned to an exact version, because each update also needs a database step, as step 10 shows.
4. Start it
systemctl --user daemon-reload
systemctl --user start erpnext-pod
podman ps
The first start downloads the images, which take about 3.4 GB of disk. When podman ps lists nine containers, including erpnext-infra, the pod's own, go on. Until the next step is done, the worker, scheduler and websocket containers keep restarting.
5. Tell ERPNext where everything is
podman exec erpnext-backend bash -c "bench set-config -g db_host 127.0.0.1 && bench set-config -gp db_port 3306 && bench set-config -g redis_cache redis://127.0.0.1:6379 && bench set-config -g redis_queue redis://127.0.0.1:6380 && bench set-config -g redis_socketio redis://127.0.0.1:6380 && bench set-config -gp socketio_port 9000 && bench set-config -g chromium_path /usr/bin/chromium-headless-shell"
systemctl --user restart erpnext-pod
That writes the addresses of the database and Redis into common_site_config.json in the sites volume, which every ERPNext container reads.
6. Create the site
ERPNext calls each installation a site, named after its address. Print the database password first, since the next command asks for it:
podman secret inspect --showsecret --format '{{.SecretData}}' erpnext-db-root-password; echo
podman exec -it erpnext-backend bench new-site erp.example.com --mariadb-user-host-login-scope='%' --install-app erpnext --set-default
It asks three things:
Enter mysql super user [root]:press Enter;MySQL root password:paste the database password;Set Administrator password:choose a password for ERPNext'sAdministratoruser.
Installing took about 12 minutes on 2 vCPU. Then turn on the scheduler, which is off for a new site, and give ERPNext its public address, which it uses in links and emails:
podman exec erpnext-backend bench --site erp.example.com enable-scheduler
podman exec erpnext-backend bench --site erp.example.com set-config host_name https://erp.example.com
7. Put Caddy in front
Go back to root with exit, switch to machinectl shell caddy@, and add this block at the end of ~/Caddyfile:
erp.example.com {
reverse_proxy 127.0.0.1:8093
}
Restart Caddy with systemctl --user restart caddy.
8. Run the setup wizard
Open https://erp.example.com, and log in as Administrator with the password from step 6. The setup wizard has four steps:
- Welcome: check Your Language, Your Country, Time Zone and Currency, which ERPNext guesses from where you are.
- Your own account: Full Name, Email Address and Password. From now on, you log in with this email address.
- Four questions about your business, and the modules you plan to use.
- Setup your organization: Company Name, Company Abbreviation, Chart of Accounts and Financial Year Begins On. For a company in Sweden, ERPNext picks BAS 2024 med Nummer, the Swedish standard chart of accounts.
Choose Complete Setup. After about a minute, ERPNext opens its desk, with Accounting, Selling, Buying, Stock and the other modules. Keep the Administrator password for maintenance, and work as your own user.
9. What to do next
Some good first steps, each found by typing its name in the search bar at the top of ERPNext:
- User List: add your colleagues, each with the roles they need;
- Email Account: set up outgoing email, so that invoices can be sent from ERPNext;
- Sales Taxes and Charges Template: check the VAT templates that came with the chart of accounts, such as
Försäljning Moms 25%, with your accountant.
10. Keep it up to date
Turn on Podman's daily updates for MariaDB and Redis:
systemctl --user enable --now podman-auto-update.timer
ERPNext itself is updated by hand, because the database has to be migrated to each new version. New versions of ERPNext 16 come out every week or two, on the GitHub page for ERPNext's releases. Back up first, as in step 11, then, with the new version in place of v16.37.0:
podman pull docker.io/frappe/erpnext:v16.37.0
sed -i 's|frappe/erpnext:v16.36.0|frappe/erpnext:v16.37.0|' ~/.config/containers/systemd/erpnext-*.container
systemctl --user daemon-reload
systemctl --user restart erpnext-pod
podman exec erpnext-backend bench --site all migrate
podman image prune -a -f
Downloading first keeps the pause short, and the last command removes the old image, which takes 3 GB. bench version in the backend container shows the version you run. A new major version, such as 17, has its own upgrade notes: read them before you change the tag.
11. Back up
As erpnext:
mkdir -p ~/backup
podman exec erpnext-backend bench --site all backup --with-files
podman cp erpnext-backend:/home/frappe/frappe-bench/sites/erp.example.com/private/backups/. ~/backup/
Each backup has four files: the database, the public and the private files, and the site's settings with its encryption key, which is needed to restore saved passwords. Copy ~/backup to another machine, and keep it private: it holds all your accounts.
Troubleshooting
Downloading a PDF fails, and the error says wkhtmltopdf reported an error with ConnectionRefusedError. The AddHost line in erpnext.pod is missing, or names a different host. ERPNext fetches its own address when it makes a PDF.
The worker, scheduler or websocket container keeps restarting. Step 5 has not been done, or the pod was not restarted after it.
Scheduled jobs, such as recurring invoices or email, never run. Run the enable-scheduler command from step 6.