DEV Community

Cover image for Deploying ERPNext – Open-Source ERP and CRM Platform
Sanskriti Harmukh for Vultr

Posted on with Aashish Chaurasiya • Originally published at docs.vultr.com

Deploying ERPNext – Open-Source ERP and CRM Platform

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
Enter fullscreen mode Exit fullscreen mode

mariadb-init/ holds a SQL script MariaDB runs on first boot to create ERPNext's database user.

Environment file:

$ nano .env
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode

Replace the domain, email, and password placeholders.

MariaDB init script — must match ERPNEXT_DB_PASSWORD above:

$ nano mariadb-init/01-erpnext-user.sql
Enter fullscreen mode Exit fullscreen mode
CREATE USER IF NOT EXISTS 'erpnext'@'%' IDENTIFIED BY 'STRONG_DB_PASSWORD';
GRANT ALL PRIVILEGES ON *.* TO 'erpnext'@'%';
FLUSH PRIVILEGES;
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode

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/"
Enter fullscreen mode Exit fullscreen mode

Fix ownership — the frappe user inside the containers is UID 1000:

$ sudo chown -R 1000:1000 sites
Enter fullscreen mode Exit fullscreen mode

Start everything:

$ docker compose up -d
Enter fullscreen mode Exit fullscreen mode

Site creation takes several minutes — ERPNext installs schemas and modules.

$ docker compose logs -f create-site
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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:

  1. Log in as Administrator with your ADMIN_PASSWORD.
  2. Complete the setup wizard — language, country, timezone, currency.
  3. Create your company profile — name, abbreviation, default currency.

Verify It Works

  1. Search bar → CustomerNew Customer → fill name/type → Save.
  2. Search bar → LeadNew Lead → fill name/source → Save.
  3. 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)