How to Deploy ERPNext - Open-Source ERP and CRM Platform

ERPNext is an open-source enterprise resource planning (ERP) and customer relationship management (CRM) platform built on the Frappe framework. It includes modules for accounting, sales, purchasing, inventory, projects, human resources, and customer management, making it suitable for small to medium-sized businesses.
This article explains how to deploy ERPNext on a Linux server using Docker Compose with MariaDB for the database, Redis for caching and background job processing, and Traefik for automatic HTTPS. It covers directory setup, environment configuration, service deployment, and demonstrates the application by creating a company and customer records.
Prerequisites
Before you begin, you need to:
- Have access to a Linux-based server (with at least 2 CPU cores and 4 GB of RAM) as a non-root user with sudo privileges.
- Install Docker and Docker Compose.
- Create a DNS A record pointing to your server's IP address (for example,
erpnext.example.com).
Set Up the Directory Structure, Configuration, and Environment Variables
ERPNext requires a project directory containing configuration files, environment variables, and database initialization scripts. The Docker Compose deployment uses these files to configure MariaDB, Redis, and the Frappe/ERPNext services.
Create the project directory with all required subdirectories.
console$ mkdir -p ~/erpnext/mariadb-init
mariadb-init/: Contains SQL initialization scripts that MariaDB executes on first startup to create the ERPNext database user.
Navigate to the project directory.
console$ cd ~/erpnext
Create the environment file.
console$ nano .env
Add the following configuration:
ini# Domain and HTTPS configuration DOMAIN=erpnext.example.com LETSENCRYPT_EMAIL=admin@example.com # ERPNext version ERPNEXT_VERSION=v16.17.0 # MariaDB credentials MYSQL_ROOT_PASSWORD=STRONG_ROOT_PASSWORD # ERPNext site credentials ADMIN_PASSWORD=STRONG_ADMIN_PASSWORD ERPNEXT_DB_NAME=erpnext ERPNEXT_DB_USER=erpnext ERPNEXT_DB_PASSWORD=STRONG_DB_PASSWORD
Replace:
erpnext.example.comwith your domain name.admin@example.comwith your email address for Let's Encrypt notifications.STRONG_ROOT_PASSWORD,STRONG_ADMIN_PASSWORD, andSTRONG_DB_PASSWORDwith secure passwords.
Save and close the file.
Create the MariaDB initialization script. This script creates the database user required by ERPNext.
console$ nano mariadb-init/01-erpnext-user.sql
Add the following content, replacing
STRONG_DB_PASSWORDwith the same value used forERPNEXT_DB_PASSWORDin the.envfile:sqlCREATE USER IF NOT EXISTS 'erpnext'@'%' IDENTIFIED BY 'STRONG_DB_PASSWORD'; GRANT ALL PRIVILEGES ON *.* TO 'erpnext'@'%'; FLUSH PRIVILEGES;
Save and close the file.
Deploy with Docker Compose
Docker Compose orchestrates the ERPNext stack including MariaDB, Redis, Frappe backend services, background workers, websocket server, and Traefik reverse proxy. A one-time configurator container initializes shared settings, and a site creation container sets up the ERPNext application.
Create the Docker Compose file.
console$ nano docker-compose.yml
Add the following configuration.
yamlservices: 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
Save and close the file.
In the above manifest:
- services: Launches eleven containers managed by Docker Compose:
- traefik: Serves as the reverse proxy and TLS termination point with automatic Let's Encrypt certificate provisioning.
- db: MariaDB 10.6 database that stores ERPNext application data.
- redis-cache: Redis instance for caching frequently accessed data.
- redis-queue: Redis instance for background job queues with persistent storage.
- configurator: One-time container that writes shared settings to
common_site_config.json. - create-site: One-time container that initializes the ERPNext site and installs the application.
- backend: Frappe application server that handles API requests.
- queue-short and queue-long: Background workers that process asynchronous jobs.
- scheduler: Runs scheduled tasks and cron jobs.
- websocket: Node.js server for real-time updates and notifications.
- frontend: Nginx server that serves static assets and proxies requests to the backend.
- healthcheck (db): Ensures dependent services wait until MariaDB is fully initialized.
- depends_on: Controls startup order so configurator runs before site creation, and site creation completes before application services start.
- labels (frontend): Registers the frontend container with Traefik for HTTPS routing.
- volumes: Provides persistent storage for TLS certificates, database files, Redis data, and ERPNext site files.
- restart: unless-stopped: Enables automatic recovery after failures or server reboots.
- services: Launches eleven containers managed by Docker Compose:
Create the
sitesdirectory and seed it with the default configuration files baked into the ERPNext image. Docker only auto-populates a named volume from an image's existing directory contents; a bind mount like./sitesstays empty unless you copy those files in yourself.console$ 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/"
The
frappeuser inside the ERPNext containers runs as UID 1000. Set matching ownership on thesitesdirectory so the containers can write to it.console$ sudo chown -R 1000:1000 sites
Build and start all services in detached mode.
console$ docker compose up -d
The site creation process takes several minutes as ERPNext installs database schemas and application modules.
Monitor the site creation progress.
console$ docker compose logs -f create-site
Wait until the installation completes. The output shows a progress bar for each stage, starting with
Installing frappe...andUpdating DocTypes for frappe, thenInstalling erpnext...andUpdating DocTypes for erpnext, each reaching 100%.Press
Ctrl+Cto exit the log viewer after installation completes.Verify all services are running.
console$ docker compose ps -a
The output displays ten running containers and two completed initialization containers. All containers should show
Upexcepterpnext-configuratoranderpnext-create-site, which showExited (0)after completing their initialization tasks.Verify the site directory was created.
console$ docker compose exec backend ls sites
The output lists
apps.json,apps.txt,assets,common_site_config.json, and a directory matching your domain, confirming the ERPNext site was initialized with its configuration files and site directory.
Access and Configure ERPNext
After deployment, access the ERPNext web interface to complete the initial setup wizard and configure the application.
Open a web browser and navigate to
https://erpnext.example.com. Replaceerpnext.example.comwith your configured domain.
Log in with the administrator credentials.
- Username:
Administrator - Password: The value of
ADMIN_PASSWORDfrom the.envfile.
- Username:
Complete the setup wizard by configuring language, country, timezone, and currency settings.
Create your company profile by entering the company name, abbreviation, and default currency.
After completing the wizard, the ERPNext dashboard displays with access to all ERP modules.
Verify the ERPNext Application
Creating basic records confirms that ERPNext modules are functioning correctly and the database is properly connected.
Create a customer record. Click the search bar, type
Customer, and select New Customer. Enter the customer name and type, then click Save.Create a lead record. Click the search bar, type
Lead, and select New Lead. Enter the lead name and source, then click Save.Verify the records appear in the respective lists by searching for
Customer ListandLead List.
Conclusion
You have deployed ERPNext on a Linux server using Docker Compose with MariaDB for the database, Redis for caching and job queues, and Traefik for automatic HTTPS. The deployment includes background workers for asynchronous processing and a websocket server for real-time updates. For advanced configuration, module customization, and production hardening, refer to the official ERPNext documentation.