Skip to Content

Production Guide: Deploy ERPNext with Docker Compose + Caddy + MariaDB + Redis on Ubuntu

A production-oriented ERPNext deployment pattern with TLS, durable data, Redis queues, backup routines, and operational checks.

ERPNext is a practical open-source ERP for teams that want accounting, CRM, stock, projects, purchasing, HR, and helpdesk workflows in one operational system instead of a chain of disconnected spreadsheets. The moment ERPNext becomes the place where invoices, inventory, and customer records live, the deployment needs to look less like a demo and more like a service: TLS at the edge, stable database storage, reliable Redis queues, predictable backups, and a clean upgrade path. This guide walks through a production-oriented ERPNext deployment on Ubuntu using Docker Compose, Caddy, MariaDB, Redis, and the standard Frappe service layout.

The pattern below is designed for a small business or internal operations team that wants a manageable single-host deployment without giving up the basics: least-privilege secrets, durable volumes, health checks, log visibility, and a recovery routine you can test before the first finance close. It is not a replacement for a clustered ERPNext architecture, but it gives you a disciplined baseline that can be documented, monitored, and handed to another administrator without tribal knowledge.

Architecture and flow overview

The public request path is intentionally simple. Caddy terminates HTTPS for erp.example.com, obtains certificates automatically, and reverse proxies traffic to the ERPNext frontend container on the host loopback interface. The frontend routes browser traffic to the Frappe backend and websocket services over the internal Compose network. MariaDB stores transactional data, while Redis is split across cache, queue, and socket channels so background jobs and realtime updates do not compete with the database.

Operationally, think of the stack in five layers. The edge layer is Caddy and DNS. The application layer is ERPNext/Frappe frontend, backend, websocket, scheduler, and workers. The data layer is MariaDB plus persistent site files. The queue layer is Redis for asynchronous jobs and realtime events. The maintenance layer is backup, restore testing, upgrades, and monitoring. Keeping those responsibilities separate makes troubleshooting much faster: a TLS failure is not a MariaDB issue, and a stuck email queue is usually not a Caddy issue.

Prerequisites

  • Ubuntu 22.04 or 24.04 LTS with a non-root sudo user.
  • A DNS record such as erp.example.com pointed at the server.
  • Docker Engine and the Docker Compose plugin installed.
  • Ports 80 and 443 open to the internet; keep MariaDB and Redis private.
  • At least 4 vCPU, 8 GB RAM, and fast SSD storage for a real team. Start larger if accounting, stock, and attachments will grow quickly.
  • An SMTP provider for outbound email and a tested backup destination outside the server.

Step-by-step deployment

Create a dedicated directory and keep all deployment artifacts in one place. This makes audits, backups, and handover cleaner. The examples use /opt/erpnext, but the same pattern works in another application directory if your team standardizes on one.

sudo install -d -m 0750 -o "$USER" -g "$USER" /opt/erpnext
cd /opt/erpnext
mkdir -p caddy sites logs backups
umask 077
touch .env

Manual copy fallback: select the command block above and copy it if the browser does not show clipboard feedback.

Store secrets in .env and never commit this file. Use long random values for the administrator password, MariaDB root password, and encryption key. If you run a password manager or secrets platform, generate values there and paste them only on the server.

cat > .env <<'EOF'
ERPNEXT_VERSION=v15
SITE_NAME=erp.example.com
ADMIN_PASSWORD=replace-with-a-long-random-admin-password
MYSQL_ROOT_PASSWORD=replace-with-a-long-random-mysql-root-password
MYSQL_DATABASE=erpnext
MYSQL_USER=erpnext
MYSQL_PASSWORD=replace-with-a-long-random-mysql-password
FRAPPE_SITE_NAME_HEADER=erp.example.com
SMTP_SERVER=smtp.example.com
SMTP_PORT=587
[email protected]
SMTP_PASSWORD=replace-with-smtp-password
EOF
chmod 0600 .env

Manual copy fallback: select the command block above and copy it if the browser does not show clipboard feedback.

Use a Compose file that keeps database and Redis services internal, publishes only the frontend to loopback, and assigns persistent volumes for MariaDB and Frappe sites. The service names mirror the Frappe container roles so logs are understandable during an incident.

cat > docker-compose.yml <&lt'EOF'
services:
  mariadb:
    image: mariadb:10.6
    command:
      - --character-set-server=utf8mb4
      - --collation-server=utf8mb4_unicode_ci
      - --skip-character-set-client-handshake
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      MYSQL_DATABASE: ${MYSQL_DATABASE}
      MYSQL_USER: ${MYSQL_USER}
      MYSQL_PASSWORD: ${MYSQL_PASSWORD}
    volumes:
      - mariadb-data:/var/lib/mysql
    restart: unless-stopped

  redis-cache:
    image: redis:7-alpine
    restart: unless-stopped
  redis-queue:
    image: redis:7-alpine
    restart: unless-stopped
  redis-socketio:
    image: redis:7-alpine
    restart: unless-stopped

  backend:
    image: frappe/erpnext:${ERPNEXT_VERSION}
    env_file: .env
    volumes:
      - sites:/home/frappe/frappe-bench/sites
      - ./logs:/home/frappe/frappe-bench/logs
    depends_on: [mariadb, redis-cache, redis-queue, redis-socketio]
    restart: unless-stopped

  frontend:
    image: frappe/erpnext:${ERPNEXT_VERSION}
    env_file: .env
    command: nginx-entrypoint.sh
    ports:
      - "127.0.0.1:8088:8080"
    volumes:
      - sites:/home/frappe/frappe-bench/sites
    depends_on: [backend, websocket]
    restart: unless-stopped

  websocket:
    image: frappe/erpnext:${ERPNEXT_VERSION}
    env_file: .env
    command: node /home/frappe/frappe-bench/apps/frappe/socketio.js
    volumes:
      - sites:/home/frappe/frappe-bench/sites
    restart: unless-stopped

  scheduler:
    image: frappe/erpnext:${ERPNEXT_VERSION}
    env_file: .env
    command: bench schedule
    volumes:
      - sites:/home/frappe/frappe-bench/sites
    restart: unless-stopped

  queue-default:
    image: frappe/erpnext:${ERPNEXT_VERSION}
    env_file: .env
    command: bench worker --queue default
    volumes:
      - sites:/home/frappe/frappe-bench/sites
    restart: unless-stopped

  queue-long:
    image: frappe/erpnext:${ERPNEXT_VERSION}
    env_file: .env
    command: bench worker --queue long
    volumes:
      - sites:/home/frappe/frappe-bench/sites
    restart: unless-stopped

  queue-short:
    image: frappe/erpnext:${ERPNEXT_VERSION}
    env_file: .env
    command: bench worker --queue short
    volumes:
      - sites:/home/frappe/frappe-bench/sites
    restart: unless-stopped

volumes:
  mariadb-data:
  sites:
EOF

Manual copy fallback: select the command block above and copy it if the browser does not show clipboard feedback.

Create the first site with the same image version used by the runtime services. This command initializes the Frappe site, points it to MariaDB and Redis, installs ERPNext, and sets the administrator password from your environment file. Run it once, then bring the full stack up.

set -a; . ./.env; set +a

docker compose run --rm backend new-site "$SITE_NAME" \
  --mariadb-root-password "$MYSQL_ROOT_PASSWORD" \
  --admin-password "$ADMIN_PASSWORD" \
  --db-host mariadb \
  --db-name "$MYSQL_DATABASE" \
  --db-user "$MYSQL_USER" \
  --db-password "$MYSQL_PASSWORD" \
  --install-app erpnext

docker compose up -d

Manual copy fallback: select the command block above and copy it if the browser does not show clipboard feedback.

Put Caddy in front of the loopback-only frontend. Caddy should be installed on the host, not inside the application network, so certificate renewal and HTTP logs remain visible even if the ERPNext stack is being upgraded.

sudo tee /etc/caddy/Caddyfile >/dev/null <&lt'EOF'
erp.example.com {
  encode zstd gzip
  reverse_proxy 127.0.0.1:8088
  header {
    X-Content-Type-Options nosniff
    X-Frame-Options SAMEORIGIN
    Referrer-Policy strict-origin-when-cross-origin
  }
}
EOF
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy

Manual copy fallback: select the command block above and copy it if the browser does not show clipboard feedback.

Configuration and secrets handling best practices

Treat ERPNext like a financial system, not a generic web app. Limit shell access to administrators, keep .env mode 0600, and avoid pasting secrets into tickets or chat logs. Rotate the initial administrator password after onboarding a named admin account, enforce strong passwords, and configure role-based permissions before inviting regular users. If SMTP is enabled, verify SPF, DKIM, and DMARC for the sender domain so invoices and password resets do not land in spam.

Backups need both database dumps and site files because attachments, private files, and generated assets live outside MariaDB. Store at least one encrypted copy away from the server and test restoration into a temporary environment after every major ERPNext upgrade. A backup you have never restored is only a guess.

cat > backup-erpnext.sh <&lt'EOF'
#!/usr/bin/env bash
set -euo pipefail
cd /opt/erpnext
stamp="$(date -u +%Y%m%dT%H%M%SZ)"
mkdir -p backups/"$stamp"
docker compose exec -T backend bench --site erp.example.com backup --with-files
container_path="/home/frappe/frappe-bench/sites/erp.example.com/private/backups"
docker compose exec -T backend sh -lc "ls -1t $container_path | head -n 5"
docker run --rm -v erpnext_sites:/sites -v "$PWD/backups/$stamp:/out" alpine \
  sh -lc 'tar czf /out/sites-files.tgz -C /sites .'
EOF
chmod 0750 backup-erpnext.sh
./backup-erpnext.sh

Manual copy fallback: select the command block above and copy it if the browser does not show clipboard feedback.

Verification checklist

After the stack is online, verify each layer separately. First, confirm Caddy can reach the loopback frontend and that TLS is valid. Then log in as Administrator, create a test company, send a test email, and trigger a background job such as a report export. Finally, inspect worker and scheduler logs to make sure queues are moving.

docker compose ps
curl -I https://erp.example.com
docker compose logs --tail=80 backend
docker compose logs --tail=80 scheduler
docker compose logs --tail=80 queue-default

Manual copy fallback: select the command block above and copy it if the browser does not show clipboard feedback.

A production handoff should include the URL, responsible owner, DNS record, backup location, restore procedure, SMTP provider, upgrade window, and emergency rollback steps. ERPNext touches sensitive business processes, so document who can approve app updates and who can restore data if a migration behaves badly.

Common issues and fixes

Login page loads but assets are broken: confirm the site name matches the host header and that FRAPPE_SITE_NAME_HEADER uses the public domain. Recreate the site only if it was initialized with the wrong name and has no production data.

Background jobs do not run: inspect queue-default, queue-long, queue-short, and scheduler. A healthy web UI does not prove workers are processing email, reports, or integrations.

Database errors during site creation: wait for MariaDB to finish first initialization, verify root credentials, and confirm the service name is mariadb. Avoid exposing port 3306 publicly just to debug the issue.

Emails fail silently: check SMTP credentials, sender domain authentication, and ERPNext email account settings. Use a transactional email provider for production rather than a personal mailbox.

Upgrades feel risky: test Compose image upgrades on a restored backup in a temporary server. Take a fresh backup, record the old image tag, upgrade during a maintenance window, and keep rollback instructions nearby.

FAQ

Can I run ERPNext on a smaller server?

You can test on a smaller server, but production ERP workloads benefit from memory and fast storage. Use at least 8 GB RAM for a team deployment and monitor queue latency, database load, and swap usage after onboarding users.

Should MariaDB and Redis be exposed to the internet?

No. Keep both services on the internal Compose network. Caddy is the only public entry point, and the frontend is bound to 127.0.0.1 on the host.

How often should I back up ERPNext?

Back up at least daily for low-volume teams and more frequently if invoices, stock movements, or support records change constantly. Always include files and test restores, not just database dumps.

Can I use Nginx instead of Caddy?

Yes, but Caddy reduces certificate-management work for smaller teams. If your organization standardizes on Nginx, keep the same loopback reverse proxy pattern and document certificate renewal.

How do I handle ERPNext upgrades?

Pin image versions, read release notes, restore the latest backup to a staging server, run the upgrade there, and only then repeat the process in production during a maintenance window.

What should be monitored first?

Monitor HTTPS availability, container restarts, disk usage, MariaDB storage growth, backup freshness, scheduler logs, queue worker health, and SMTP failures. These signals catch most operational problems early.

Internal links

Talk to us

If you want this implemented with hardened defaults, observability, and tested recovery playbooks, our team can help.

Contact Us

Production Guide: Deploy Matomo with Docker Compose + Caddy + MariaDB on Ubuntu
A production-oriented Matomo analytics stack with HTTPS, MariaDB, backups, verification, and privacy-focused operations.