ERPNext is an open-source ERP/CRM built on the Frappe framework — accounting, sales, purchasing, inventory, projects, HR, and customer management in one platform. This guide deploys it via Docker Compose with MariaDB, Redis, background workers, a websocket server, and Traefik-managed TLS, then walks through initial setup and creating a customer/lead.
Prerequisites: a Linux server (2 vCPU / 4GB RAM minimum), Docker + Docker Compose, a domain A record (e.g.
erpnext.example.com).
Set Up the Project
$ mkdir -p ~/erpnext/mariadb-init
$ cd ~/erpnext
mariadb-init/ holds a SQL script MariaDB runs on first boot to create ERPNext's database user.
Environment file:
$ nano .env
DOMAIN=erpnext.example.com
LETSENCRYPT_EMAIL=admin@example.com
ERPNEXT_VERSION=v16.17.0
MYSQL_ROOT_PASSWORD=STRONG_ROOT_PASSWORD
ADMIN_PASSWORD=STRONG_ADMIN_PASSWORD
ERPNEXT_DB_NAME=erpnext
ERPNEXT_DB_USER=erpnext
ERPNEXT_DB_PASSWORD=STRONG_DB_PASSWORD
Replace the domain, email, and password placeholders.
MariaDB init script — must match ERPNEXT_DB_PASSWORD above:
$ nano mariadb-init/01-erpnext-user.sql
CREATE USER IF NOT EXISTS 'erpnext'@'%' IDENTIFIED BY 'STRONG_DB_PASSWORD';
GRANT ALL PRIVILEGES ON *.* TO 'erpnext'@'%';
FLUSH PRIVILEGES;
Deploy with Docker Compose
Eleven containers: Traefik, MariaDB, two Redis instances, a one-time configurator, a one-time site-creation job, and the Frappe backend/workers/websocket/frontend.
$ nano docker-compose.yml
services:
traefik:
image: traefik:v3.7.0
container_name: traefik
command:
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
- "--entrypoints.web.http.redirections.entrypoint.to=websecure"
- "--entrypoints.web.http.redirections.entrypoint.scheme=https"
- "--certificatesresolvers.letsencrypt.acme.httpchallenge=true"
- "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web"
- "--certificatesresolvers.letsencrypt.acme.email=${LETSENCRYPT_EMAIL}"
- "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
ports:
- "80:80"
- "443:443"
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro"
- "./letsencrypt:/letsencrypt"
restart: unless-stopped
db:
image: mariadb:10.6
container_name: erpnext-db
command:
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_unicode_ci
- --skip-name-resolve
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
interval: 5s
timeout: 5s
retries: 20
volumes:
- ./db-data:/var/lib/mysql
- ./mariadb-init:/docker-entrypoint-initdb.d:ro
restart: unless-stopped
redis-cache:
image: redis:7-alpine
container_name: erpnext-redis-cache
restart: unless-stopped
redis-queue:
image: redis:7-alpine
container_name: erpnext-redis-queue
volumes:
- ./redis-queue-data:/data
restart: unless-stopped
configurator:
image: frappe/erpnext:${ERPNEXT_VERSION}
container_name: erpnext-configurator
entrypoint: >
bash -c '
bench set-config -g db_host db &&
bench set-config -g db_port 3306 &&
bench set-config -g redis_cache redis://redis-cache:6379 &&
bench set-config -g redis_queue redis://redis-queue:6379 &&
bench set-config -g redis_socketio redis://redis-queue:6379 &&
bench set-config -g socketio_port 9000
'
depends_on:
db:
condition: service_healthy
volumes:
- ./sites:/home/frappe/frappe-bench/sites
restart: "no"
create-site:
image: frappe/erpnext:${ERPNEXT_VERSION}
container_name: erpnext-create-site
entrypoint: >
bash -c '
if [ ! -f sites/${DOMAIN}/site_config.json ]; then
bench new-site ${DOMAIN} \
--db-host db \
--db-port 3306 \
--db-name ${ERPNEXT_DB_NAME} \
--db-user ${ERPNEXT_DB_USER} \
--db-password ${ERPNEXT_DB_PASSWORD} \
--db-root-username root \
--db-root-password ${MYSQL_ROOT_PASSWORD} \
--mariadb-user-host-login-scope "%" \
--admin-password ${ADMIN_PASSWORD} \
--install-app erpnext;
fi
'
environment:
DOMAIN: ${DOMAIN}
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
ADMIN_PASSWORD: ${ADMIN_PASSWORD}
ERPNEXT_DB_NAME: ${ERPNEXT_DB_NAME}
ERPNEXT_DB_USER: ${ERPNEXT_DB_USER}
ERPNEXT_DB_PASSWORD: ${ERPNEXT_DB_PASSWORD}
depends_on:
configurator:
condition: service_completed_successfully
volumes:
- ./sites:/home/frappe/frappe-bench/sites
restart: "no"
backend:
image: frappe/erpnext:${ERPNEXT_VERSION}
container_name: erpnext-backend
depends_on:
create-site:
condition: service_completed_successfully
volumes:
- ./sites:/home/frappe/frappe-bench/sites
restart: unless-stopped
queue-short:
image: frappe/erpnext:${ERPNEXT_VERSION}
container_name: erpnext-queue-short
command: ["bench", "worker", "--queue", "short,default"]
depends_on:
create-site:
condition: service_completed_successfully
volumes:
- ./sites:/home/frappe/frappe-bench/sites
restart: unless-stopped
queue-long:
image: frappe/erpnext:${ERPNEXT_VERSION}
container_name: erpnext-queue-long
command: ["bench", "worker", "--queue", "long,default,short"]
depends_on:
create-site:
condition: service_completed_successfully
volumes:
- ./sites:/home/frappe/frappe-bench/sites
restart: unless-stopped
scheduler:
image: frappe/erpnext:${ERPNEXT_VERSION}
container_name: erpnext-scheduler
command: ["bench", "schedule"]
depends_on:
create-site:
condition: service_completed_successfully
volumes:
- ./sites:/home/frappe/frappe-bench/sites
restart: unless-stopped
websocket:
image: frappe/erpnext:${ERPNEXT_VERSION}
container_name: erpnext-websocket
command: ["node", "/home/frappe/frappe-bench/apps/frappe/socketio.js"]
depends_on:
create-site:
condition: service_completed_successfully
volumes:
- ./sites:/home/frappe/frappe-bench/sites
restart: unless-stopped
frontend:
image: frappe/erpnext:${ERPNEXT_VERSION}
container_name: erpnext-frontend
command: ["nginx-entrypoint.sh"]
environment:
BACKEND: backend:8000
SOCKETIO: websocket:9000
FRAPPE_SITE_NAME_HEADER: ${DOMAIN}
CLIENT_MAX_BODY_SIZE: 50m
PROXY_READ_TIMEOUT: "120"
UPSTREAM_REAL_IP_ADDRESS: 127.0.0.1
UPSTREAM_REAL_IP_HEADER: X-Forwarded-For
UPSTREAM_REAL_IP_RECURSIVE: "off"
depends_on:
backend:
condition: service_started
websocket:
condition: service_started
labels:
- "traefik.enable=true"
- "traefik.http.routers.erpnext.rule=Host(`${DOMAIN}`)"
- "traefik.http.routers.erpnext.entrypoints=websecure"
- "traefik.http.routers.erpnext.tls.certresolver=letsencrypt"
- "traefik.http.services.erpnext.loadbalancer.server.port=8080"
restart: unless-stopped
Key points: db's healthcheck gates everything downstream; configurator writes shared settings once, then create-site provisions the actual ERPNext site — both are one-shot (restart: "no") and other services wait on create-site completing successfully. frontend is what Traefik routes to.
Bootstrap the sites bind mount — Docker only auto-populates a named volume from image contents, not a bind mount, so copy the files in manually:
$ mkdir -p sites
$ docker run --rm -v $(pwd)/sites:/bootstrap-sites frappe/erpnext:v16.17.0 bash -c "cp -a /home/frappe/frappe-bench/sites/. /bootstrap-sites/"
Fix ownership — the frappe user inside the containers is UID 1000:
$ sudo chown -R 1000:1000 sites
Start everything:
$ docker compose up -d
Site creation takes several minutes — ERPNext installs schemas and modules.
$ docker compose logs -f create-site
Watch for Installing frappe... / Updating DocTypes for frappe, then Installing erpnext... / Updating DocTypes for erpnext, each hitting 100%. Ctrl+C once done.
$ docker compose ps -a
Ten containers Up, plus erpnext-configurator and erpnext-create-site showing Exited (0) (expected — they're one-shot jobs).
$ docker compose exec backend ls sites
Should list apps.json, apps.txt, assets, common_site_config.json, and a directory matching your domain.
First-Run Setup
Visit https://erpnext.example.com:
- Log in as Administrator with your
ADMIN_PASSWORD. - Complete the setup wizard — language, country, timezone, currency.
- Create your company profile — name, abbreviation, default currency.
Verify It Works
- Search bar →
Customer→ New Customer → fill name/type → Save. - Search bar →
Lead→ New Lead → fill name/source → Save. - Confirm both show up under Customer List / Lead List.
Next Steps
ERPNext is running with background workers, real-time updates, and TLS. From here:
- Enable additional Frappe apps/modules (HR, Projects, Manufacturing) as your needs grow
- Set up scheduled MariaDB backups given this holds core business data
- Configure email accounts for automated notifications and customer communication
For the full guide, visit the original article on Vultr Docs.
Top comments (0)