41 KiB
Production ERPNext Deployment
Complete guide for deploying ERPNext in production using Docker Compose with Traefik and Let's Encrypt SSL.
Overview
This guide provides everything needed to deploy a production-ready ERPNext instance using Docker Compose. The setup includes:
- ERPNext v15.82.1 - ERP application
- Frappe Framework v15.82.1 - Application framework
- Traefik v2.11 - Reverse proxy with automatic Let's Encrypt SSL
- MariaDB 11.8 - Database server
- Redis - Cache, queue, and socketio
Architecture
Three separate Docker Compose projects work together:
- Traefik - Reverse proxy, SSL certificates, load balancing
- MariaDB - Shared database for all ERPNext sites
- ERPNext - Application containers (backend, frontend, workers, scheduler)
Repository Structure
erp-is/
├── production/ # Production deployment (this directory)
│ ├── README.md # This guide
│ ├── *.env.example # Configuration templates
│ ├── scripts/ # Automation scripts
│ │ ├── deploy.sh # Main deployment (includes setup/regenerate)
│ │ ├── create-site.sh # Create new site
│ │ ├── backup-site.sh # Backup automation
│ │ ├── validate-env.sh # Config validation
│ │ ├── logs.sh # Log viewer
│ │ └── stop.sh # Stop services
│ └── production.yaml # Generated compose file (do not edit)
├── overrides/ # Upstream compose overlays
├── docs/ # Upstream documentation
└── compose.yaml # Upstream base configuration
Important: This is a fork of frappe/frappe_docker:
- Custom files (in
production/) are maintained in this fork - Infrastructure files (compose.yaml, overrides/) track upstream
- Container images are pulled from official Frappe Docker registry
Table of Contents
- Stack Versions & Fork Notes
- Quick Start
- Prerequisites
- Pre-Deployment Checklist
- Environment Configuration
- Script Usage Guide
- Deployment
- Custom Apps & Third-Party Integrations
- Common Operations
- Update Procedures
- Apply Updates to Sites (new vs existing)
- Git Workflow
- Maintenance Playbook
- Troubleshooting
- Security
- Architecture Explained
Stack Versions & Fork Notes
| Layer | Version / Source | Why it matters |
|---|---|---|
| Host OS (reference) | Ubuntu 22.04 LTS | All scripts are validated on this release; Debian 11/12 behave the same as long as Docker 24+ is installed. |
| ERPNext | v15.82.1 | Set through ERPNEXT_VERSION in production/production.env. Keep this aligned with the branch you pin in apps.json. |
| Frappe Framework | v15.82.1 | Declared as FRAPPE_VERSION; must match ERPNext to avoid schema drift. |
| MariaDB | 11.8 (official image) | Shared across every site in the bench; plan capacity accordingly. |
| Redis | redis:alpine |
Provides cache, queue, and websocket backplanes. |
| Traefik | v2.11 | Handles TLS (Let’s Encrypt) and routing. |
| Python (app containers) | 3.11.6 on Debian Bookworm | Comes from images/custom/Containerfile; upgrade only after testing custom apps. |
| Node.js | 20.19.2 | Needed for asset builds (bench build). |
Quick Start
For experienced users:
# 1. Generate secure passwords
openssl rand -base64 32 # DB password
openssl rand -base64 24 # Admin password
htpasswd -nB admin # Traefik dashboard password
# 2. Configure environment (interactive setup)
./scripts/deploy.sh --setup # Creates *.env from templates
# Edit all three files with your values
# 3. Validate configuration
./scripts/validate-env.sh # Checks for errors before deploy
# 4. Deploy all services
./scripts/deploy.sh
# 5. Create your first site
./scripts/create-site.sh erp.example.com
# 6. View logs
./scripts/logs.sh # Interactive service selection
First time? Follow the complete checklist below.
Prerequisites
System Requirements
- OS: Ubuntu 20.04/22.04 LTS or Debian 11/12
- CPU: 2+ cores (4+ recommended)
- RAM: 4GB minimum (8GB+ recommended)
- Disk: 50GB+ SSD storage
- Network: Public IP with ports 80 and 443 open
- Domain: DNS pointing to your server
Software Requirements
# Docker Engine 20.10+
docker --version
# Docker Compose v2
docker compose version
Pre-Deployment Checklist
✅ 1. Verify System
# Check OS version
lsb_release -a
# Check available RAM
free -h
# Check disk space
df -h
# Check server is accessible
ping -c 3 $(hostname -I | awk '{print $1}')
✅ 2. Configure DNS
# Verify DNS propagation (wait 24-48h after DNS change)
dig +short erp.example.com
# Should return your server's public IP
✅ 3. Configure Firewall
# Allow required ports
sudo ufw allow 22/tcp # SSH
sudo ufw allow 80/tcp # HTTP
sudo ufw allow 443/tcp # HTTPS
sudo ufw enable
# Verify
sudo ufw status
✅ 4. Install Docker
# Install Docker Engine
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
# Add user to docker group
sudo usermod -aG docker $USER
# Logout and login again, then test
docker ps
✅ 5. Validate Environment
# Run validation script (checks all config files)
./scripts/validate-env.sh
# Should pass all checks before proceeding
Script Usage Guide
All scripts have been optimized for efficiency and include comprehensive help. Every script supports -h or --help.
📋 Script Overview
| Script | Purpose | Key Features |
|---|---|---|
| deploy.sh | Main deployment | --setup, --regenerate, validation |
| create-site.sh | Create ERPNext site | Interactive prompts, validation |
| backup-site.sh | Advanced backups | Encryption, auto-copy, cleanup |
| logs.sh | View service logs | Interactive menu, service selection |
| stop.sh | Stop services | Selective stopping, --all option |
| validate-env.sh | Config validation | Password strength, cross-validation |
🚀 Deploy Script
# Get help
./scripts/deploy.sh --help
# Setup environment files from templates
./scripts/deploy.sh --setup
# Validate and deploy all services
./scripts/deploy.sh
# Regenerate production.yaml only (no deploy)
./scripts/deploy.sh --regenerate
🏗️ Create Site Script
# Get help
./scripts/create-site.sh --help
# Interactive mode
./scripts/create-site.sh
# Direct usage
./scripts/create-site.sh erp.example.com
./scripts/create-site.sh erp.example.com MySecurePass123
💾 Backup Script (Advanced)
# Get help
./scripts/backup-site.sh --help
# Basic backup
./scripts/backup-site.sh erp.example.com
# Advanced backup with files, auto-copy, cleanup (7-day retention)
./scripts/backup-site.sh erp.example.com --with-files --auto-copy --cleanup-old 7
# Host-only backup that prunes everything except this run
./scripts/backup-site.sh erp.example.com --with-files --auto-copy --host-only --cleanup-old latest
# Encrypted backup
BACKUP_PASSPHRASE='your-secret' ./scripts/backup-site.sh erp.example.com --encrypt --auto-copy
# Automated (environment variables)
AUTO_COPY=1 CLEANUP_OLD=1 ./scripts/backup-site.sh erp.example.com
Backup Features:
- Encryption: GPG symmetric encryption with AES256
- Auto-copy & host-only:
--auto-copymirrors to the host; use--flat-host-path(default) to drop files directly in$HOST_BACKUP_ROOTor--nested-host-path/HOST_BACKUP_LAYOUT=nestedfor$HOST_BACKUP_ROOT/<site>/<timestamp>. Add--host-onlyto delete container copies once the host copy is verified. - Cleanup policies:
--cleanup-oldacceptsN(days),keep:N(retain newest runs), orlatest(keep only the current run); defaults toBACKUP_RETENTION_DAYS - Validation: Verify backup files and sizes
- Logging: Detailed operation logs in
/tmp/
Standard backup recipes
-
Host-only snapshot, keep only the latest run
./scripts/backup-site.sh erp.example.com \ --with-files --auto-copy --host-only --cleanup-old latestThis flow copies fresh files to the host, deletes the container copies immediately, and prunes older host snapshots so only the latest timestamp remains.
-
Cron-friendly daily backups with 30‑day retention
AUTO_COPY=1 CLEANUP_OLD=1 ./scripts/backup-site.sh erp.example.comEnvironment flags keep the command short for crontab entries. Set
BACKUP_RETENTION_DAYSglobally (defaults to 30) or exportCLEANUP_POLICY="keep:7"if you prefer to keep a fixed number of runs.
📊 Logs Script
# Get help
./scripts/logs.sh --help
# Interactive menu
./scripts/logs.sh
# Direct service access
./scripts/logs.sh 1 # Backend logs
./scripts/logs.sh backend # Same as above
./scripts/logs.sh frontend # Nginx logs
./scripts/logs.sh all # All services
./scripts/logs.sh backend --tail 100 # Show last 100 lines and exit
./scripts/logs.sh --tail # Tail recent logs for every service
Available Services:
- backend - Gunicorn application server
- frontend - Nginx reverse proxy
- websocket - Socket.io for real-time features
- queue-short - Short-running background jobs
- queue-long - Long-running background jobs
- scheduler - Cron-like background scheduler
- all - All services combined
🛑 Stop Script
# Get help
./scripts/stop.sh --help
# Interactive mode (asks about MariaDB/Traefik)
./scripts/stop.sh
# Stop everything without prompts
./scripts/stop.sh --all
✅ Validation Script
# Get help
./scripts/validate-env.sh --help
# Validate all environment files
./scripts/validate-env.sh
Validation Checks:
- Required variables present
- Password strength (16+ characters recommended)
- Email format validation
- Cross-file password matching
- Placeholder detection
- Weak password detection
Environment Configuration
Three Environment Files Required
production/production.env- ERPNext application configproduction/mariadb.env- Database configproduction/traefik.env- Reverse proxy config
Step-by-Step Configuration
1. Generate Passwords
# Database password (use in production.env AND mariadb.env)
openssl rand -base64 32
# Admin password (use in production.env)
openssl rand -base64 24
# Traefik dashboard password (use in traefik.env)
htpasswd -nB admin
# Or use: https://hostingcanada.org/htpasswd-generator/
2. Configure production/production.env
./scripts/deploy.sh --setup # Create from templates
nano production/production.env
Required values:
SITES=erp.example.com # Your domain
ERPNEXT_VERSION=v15.82.1 # ERPNext version
FRAPPE_VERSION=v15.82.1 # Frappe version
DB_HOST=mariadb-database # Database host
DB_PASSWORD=<paste-generated-db-password> # From step 1
REDIS_CACHE=redis-cache:6379 # Redis cache
REDIS_QUEUE=redis-queue:6379 # Redis queue
REDIS_SOCKETIO=redis-socketio:6379 # Redis socketio
LETSENCRYPT_EMAIL=admin@example.com # For SSL certs
ADMIN_PASSWORD=<paste-generated-admin-password> # From step 1
3. Configure production/mariadb.env
nano production/mariadb.env # Already created by --setup
Required values:
DB_PASSWORD=<same-as-production.env> # MUST match production.env
4. Configure production/traefik.env
nano production/traefik.env # Already created by --setup
Required values:
TRAEFIK_DOMAIN=traefik.example.com # Dashboard subdomain
EMAIL=admin@example.com # For SSL certs
HASHED_PASSWORD=<paste-from-htpasswd> # From step 1
5. Secure Files
chmod 600 production/*.env
# Verify they're git-ignored
git status | grep production.env
# Should return nothing
Deployment
Deploy Services
./scripts/deploy.sh
What it does:
- Validates environment configuration (
./scripts/validate-env.sh) - Deploys Traefik (reverse proxy + SSL)
- Deploys MariaDB (database)
- Generates ERPNext configuration (
production.yaml) from:- Base:
compose.yaml - Redis overlay:
overrides/compose.redis.yaml - Multi-bench overlay:
overrides/compose.multi-bench.yaml - SSL overlay:
overrides/compose.multi-bench-ssl.yaml
- Base:
- Deploys ERPNext (backend, frontend, workers, scheduler)
Expected output:
[INFO] ERPNext Production Deployment
[INFO] Validating configuration...
✅ Validation Passed
[INFO] Step 1: Deploying Traefik...
✓ Traefik deployed
[INFO] Step 2: Deploying MariaDB...
✓ MariaDB deployed. Waiting 30s for initialization...
[INFO] Step 3: Generating production.yaml...
✓ Generated
[INFO] Step 4: Deploying ERPNext...
✓ ERPNext deployed
[INFO] ✓ Deployment complete!
Create Site
./scripts/create-site.sh erp.example.com
Wait 2-3 minutes for:
- Database initialization
- Frappe installation
- ERPNext installation
- SSL certificate generation
Verify Deployment
# Check all services are running
docker ps
# Check service health
docker compose -f production/production.yaml ps
# View logs
./scripts/logs.sh
# Follow logs (live)
./scripts/logs.sh -f backend
Access Your Site
- Open browser:
https://erp.example.com - Username:
Administrator - Password: Check
production/production.env→ADMIN_PASSWORD
Custom Apps & Third-Party Integrations
Why ship custom logic as apps?
- Upstream-safe: Apps keep your business logic outside the upstream fork, so rebasing on
frappe_dockerstays painless. - Repeatable: Every site receives the exact same code (DocTypes, patches, API clients) whenever the container is rebuilt.
- Supported: This mirrors the official frappe_docker custom app workflow.
1. Describe the apps you need (apps.json)
Create a manifest in the repository root that lists every app you want baked into the image—first-party or third-party:
[
{ "url": "https://github.com/frappe/erpnext", "branch": "version-15" },
{ "url": "https://github.com/frappe/hrms", "branch": "version-15" },
{ "url": "https://github.com/acme/custom_integrations", "branch": "main" }
]
Convert it to base64 once so the build context can read it without extra files:
export APPS_JSON_BASE64=$(base64 -w 0 apps.json)
2. Build (and optionally push) a custom ERPNext image
Use the official layered image as the base and inject your apps list:
docker build \
--build-arg=FRAPPE_PATH=https://github.com/frappe/frappe \
--build-arg=FRAPPE_BRANCH=version-15 \
--build-arg=APPS_JSON_BASE64=$APPS_JSON_BASE64 \
--tag=registry.example.com/erpnext-custom:15 \
--file=images/layered/Containerfile .
# optional
docker push registry.example.com/erpnext-custom:15
Prefer
docker buildx bake -f docker-bake.hcl --set erpnext.args.APPS_JSON_BASE64=$APPS_JSON_BASE64if you already rely on Buildx/CI.
3. Point production to the new image
Edit production/production.env so compose uses your artifact everywhere:
CUSTOM_IMAGE=registry.example.com/erpnext-custom
CUSTOM_TAG=15
PULL_POLICY=always
Regenerate and redeploy so every service shares the same build:
./scripts/deploy.sh --regenerate
./scripts/deploy.sh
4. Install or update apps on sites
All apps listed in apps.json become available inside the bench. You still choose which sites receive them.
New site
./scripts/create-site.sh erp.example.com
docker compose -f production/production.yaml exec backend \
bench --site erp.example.com install-app custom_integrations hrms
Existing site
# Install a newly added app
docker compose -f production/production.yaml exec backend \
bench --site erp.example.com install-app custom_integrations
# Apply database patches after pulling latest code/image
docker compose -f production/production.yaml exec backend \
bench --site erp.example.com migrate
# Rebuild assets when the app ships JS/CSS
docker compose -f production/production.yaml exec backend \
bench --site erp.example.com build
5. Wire in third-party services securely
- Store API keys or secrets per site with
bench --site <site> set-config SERVICE_API_KEY value --as-dictso they land insite_config.jsoninstead of the repo. - Use background jobs (
frappe.enqueue) inside your app for webhook callbacks, polling jobs, or queue workers that call external APIs. - Mount extra certificates or client libraries via an override compose file if an integration needs system packages.
- Keep outbound allow-lists in Traefik/MariaDB untouched—integrations happen from the backend container, so no Traefik tweaks are required unless you expose a new inbound service.
6. Keep apps synchronized
- Version pin each entry in
apps.json(tag, branch, or commit) so rebuilds are deterministic. - When a third-party releases an update, bump the branch or tag, rebuild the image, redeploy, and run
bench migrateon every existing site. - Automate this via CI to ensure upstream merges (
git fetch upstream && git merge upstream/main) and app bumps happen in the same pipeline.
Following this flow keeps the deployment upstream-compatible while giving you a repeatable way to include bespoke code, official marketplace apps, or deep third-party integrations without touching container internals manually.
Common Operations
Backup Site
# Basic backup
./scripts/backup-site.sh erp.example.com
# Advanced backup with files and 7-day cleanup
./scripts/backup-site.sh erp.example.com --with-files --auto-copy --cleanup-old 7
# Encrypted backup with cleanup
BACKUP_PASSPHRASE='your-secret' ./scripts/backup-site.sh erp.example.com \
--encrypt --auto-copy --cleanup-old
# Host-only snapshot keeping only this run
./scripts/backup-site.sh erp.example.com --with-files --auto-copy --host-only --cleanup-old latest
Backup Features:
- Database + Files: Use
--with-filesto include uploaded files - Auto-copy:
--auto-copycopies backups to host$HOST_BACKUP_ROOT/<site>/<timestamp> - Host-only:
--host-onlydeletes container copies after verifying the host snapshot - Encryption:
--encryptwith GPG AES256 (requiresBACKUP_PASSPHRASE) - Cleanup:
--cleanup-oldacceptsN(days),keep:N(runs), orlatestto prune aggressively - Validation: Automatic backup verification and size reporting
Backups are stored in: sites/erp.example.com/private/backups/ (container) or ./backups/ (host)
View Logs
# Interactive service selection
./scripts/logs.sh
# Specific services
./scripts/logs.sh backend # Application logs
./scripts/logs.sh frontend # Nginx logs
./scripts/logs.sh scheduler # Background jobs
./scripts/logs.sh all # All services
# Alternative: direct Docker commands
docker compose -f production/production.yaml logs -f backend
# Tail logs without following
./scripts/logs.sh backend --tail 200
./scripts/logs.sh --tail # All services, last 200 lines
Stop Services
# Interactive mode (asks about dependencies)
./scripts/stop.sh
# Stop everything without prompts
./scripts/stop.sh --all
Restart Services
./scripts/stop.sh --all
./scripts/deploy.sh
Update ERPNext
# Update version in production.env
nano production/production.env
# Change: ERPNEXT_VERSION=v15.83.0
# Pull new images
docker compose -f production/production.yaml pull
# Redeploy
./production/scripts/deploy.sh
# Run migrations
docker compose -f production/production.yaml exec backend \
bench --site erp.example.com migrate
Add New Site (Multi-tenancy)
# Update SITES in production.env
nano production/production.env
# Change: SITES=erp.example.com,erp2.example.com
# Regenerate configuration
./scripts/deploy.sh --regenerate
# Apply changes
docker compose -f production/production.yaml up -d
# Create new site
./scripts/create-site.sh erp2.example.com
# Install custom apps that were baked into the image
docker compose -f production/production.yaml exec backend \
bench --site erp2.example.com install-app custom_integrations hrms
# Run migrations and build assets once
docker compose -f production/production.yaml exec backend \
bench --site erp2.example.com migrate
docker compose -f production/production.yaml exec backend \
bench --site erp2.example.com build
Update Procedures
Understanding the Update Layers
This deployment has three layers that update independently:
-
Infrastructure Layer (frappe_docker)
- Docker Compose configurations
- Container build instructions
- Deployment scripts structure
- Updates via:
git fetch upstream && git merge upstream/main
-
Application Layer (ERPNext/Frappe)
- ERPNext features and bug fixes
- Frappe framework updates
- Updates via: Changing version tags in
production.env
-
Customization Layer (Custom Apps & Integrations)
- Custom Frappe apps, DocTypes, API clients, third-party connectors
- Site-level configurations stored via
bench set-config - Updates via: Rebuilding images with a new
apps.json, migrating sites, and redeploying containers
Update Infrastructure (Docker Configs)
What this updates: Compose files, build configs, deployment improvements
# 1. Sync with upstream frappe_docker
cd /path/to/erp-is-test
git fetch upstream
git merge upstream/main
# 2. Review infrastructure changes
git log --oneline upstream/main ^HEAD
git diff HEAD~1 compose.yaml
git diff HEAD~1 overrides/
# 3. Regenerate production.yaml with new infrastructure
./scripts/deploy.sh --regenerate
# 4. Review generated configuration
docker compose -f production/production.yaml config > /tmp/new-config.yaml
diff production/production.yaml /tmp/new-config.yaml || true
# 5. Apply infrastructure updates
docker compose -f production/production.yaml up -d
# 6. Verify all services healthy
docker compose -f production/production.yaml ps
Important: This does NOT update ERPNext version, only how it runs.
Update ERPNext Version
What this updates: ERPNext features, bug fixes, Frappe framework
# 1. Check current version
docker compose -f production/production.yaml exec backend bench version
# 2. Backup before upgrading
./scripts/backup-site.sh erp.example.com --with-files --auto-copy
# 3. Update version in production.env
nano production/production.env
# Change:
# ERPNEXT_VERSION=v15.82.1 → ERPNEXT_VERSION=v15.85.0
# FRAPPE_VERSION=v15.82.1 → FRAPPE_VERSION=v15.85.0
# 4. Regenerate configuration
./scripts/deploy.sh --regenerate
# 5. Pull new images (downloads updated ERPNext)
docker compose -f production/production.yaml pull
# 6. Stop services
./scripts/stop.sh
# 7. Start with new version
docker compose -f production/production.yaml up -d
# 8. Run database migrations
docker compose -f production/production.yaml exec backend \
bench --site erp.example.com migrate
# 9. Clear cache and rebuild
docker compose -f production/production.yaml exec backend \
bench --site erp.example.com clear-cache
docker compose -f production/production.yaml exec backend \
bench --site erp.example.com build
# 10. Verify new version
docker compose -f production/production.yaml exec backend bench version
Apply Updates to Sites (new vs existing)
Use the same redeploy pipeline for every site, but tailor the final bench commands depending on whether the site already exists.
New sites created after an update
- Run
./scripts/create-site.sh new.example.comonce the new image is live. - Install any optional apps:
bench --site new.example.com install-app custom_integrations hrms. - Seed data, fixtures, or integrations using your app's onboarding commands.
Because the site is created after the image rebuild, it automatically receives the latest code; no manual migration is needed beyond the installer.
Existing sites that were updated
- Stop users (maintenance window) and take a backup:
./scripts/backup-site.sh erp.example.com --with-files. - After redeploying containers, run:
docker compose -f production/production.yaml exec backend \
bench --site erp.example.com migrate
docker compose -f production/production.yaml exec backend \
bench --site erp.example.com build
docker compose -f production/production.yaml exec backend \
bench --site erp.example.com clear-cache
- If the update introduced new apps, install them explicitly and migrate again.
- Re-enable background jobs (
bench enable-scheduler) if you disabled them for the maintenance window.
Update Custom Apps & Integrations
What this updates: Custom Frappe apps, DocTypes, webhook handlers, and any bundled third-party modules.
# 1. Pull or merge the new code for each app, then refresh apps.json
git pull origin main # inside every custom app repo
vim apps.json # bump branch/tag references if needed
# 2. Rebuild the image with the refreshed manifest
export APPS_JSON_BASE64=$(base64 -w 0 apps.json)
docker build \
--build-arg=FRAPPE_BRANCH=version-15 \
--build-arg=APPS_JSON_BASE64=$APPS_JSON_BASE64 \
--tag=registry.example.com/erpnext-custom:15 .
docker push registry.example.com/erpnext-custom:15
# 3. Update production to pull the new tag
sed -i 's/CUSTOM_TAG=.*/CUSTOM_TAG=15/' production/production.env
./scripts/deploy.sh --regenerate
./scripts/deploy.sh
# 4. Apply database patches and rebuild assets per site
docker compose -f production/production.yaml exec backend \
bench --site erp.example.com migrate
docker compose -f production/production.yaml exec backend \
bench --site erp.example.com build
Complete Update (All Layers)
Use this for major version jumps or after long periods:
# 1. Backup everything first
./scripts/backup-site.sh erp.example.com --with-files --auto-copy
# 2. Update infrastructure
git fetch upstream && git merge upstream/main
# 3. Update ERPNext version in production.env
nano production/production.env
# 4. Regenerate everything
./scripts/deploy.sh --regenerate
# 5. Deploy
./scripts/stop.sh
docker compose -f production/production.yaml pull
docker compose -f production/production.yaml up -d
# 6. Migrate
docker compose -f production/production.yaml exec backend \
bench --site erp.example.com migrate
# 7. Rebuild assets and clear cache
docker compose -f production/production.yaml exec backend \
bench --site erp.example.com clear-cache
docker compose -f production/production.yaml exec backend \
bench --site erp.example.com build --force
Git Workflow
Branch contract
| Branch | Purpose | Deploy target |
|---|---|---|
main |
Production truth. Tracks only tested commits paired with container/image tags referenced in production.env. |
Production |
staging |
Release candidate. Used to exercise upstream merges and new custom-app tags against a staging bench. | Staging bench (optional) |
dev |
Scratch/feature work. Safe spot to prototype new overrides, scripts, or Containerfile tweaks. | Local only |
Remotes:
origin→ this fork (erp-is).upstream→https://github.com/frappe/frappe_docker.git.
Sync loop (weekly)
git checkout dev && git fetch upstream– bring in the latest frappe_docker changes.git merge upstream/main(orgit rebase upstream/main) – resolve conflicts inoverrides/,compose.yaml, andproduction/scripts.- Smoke-test locally (
./scripts/deploy.sh --regenerate && docker compose -f production/production.yaml up -d). - Promote into
staging:git switch staging && git merge devonce tests pass. - Cut a tested release into
mainonly after staging verification and update the stack-version table +.envpins.
Custom app release recipe
-
Keep each custom Frappe app in its own repository.
-
Tag or branch the app when you are ready to promote (
git tag v2.4.0). -
Update
apps.jsonwith that tag/commit and keep the file sorted. This manifest is the source of truth forAPPS_JSON_BASE64. -
Rebuild/push the custom image from the repo root:
export APPS_JSON_BASE64=$(base64 -w0 apps.json) docker build -f images/custom/Containerfile \ --build-arg FRAPPE_BRANCH=version-15 \ --build-arg APPS_JSON_BASE64=$APPS_JSON_BASE64 \ -t registry.example.com/erpnext-custom:v15-2024.09 . docker push registry.example.com/erpnext-custom:v15-2024.09 -
Update
production/production.env(CUSTOM_TAG=v15-2024.09), regenerate, then run migrations for each site.
Helpful habits
- Use
git worktreeto keepdev,staging, andmainchecked out simultaneously so you can hotfix production without stashing local experiments. - Commit regenerated
production/production.yamlonly when auditing diffs—normally it stays generated. - Keep a short
CHANGELOG.md(or GitHub Releases) per branch so stakeholders know which ERPs/custom apps were promoted. - Treat
benchcommands exactly likepython manage.py: every change should be expressed as a migration, patch, or fixture committed alongside app code.
Maintenance Playbook
Daily / Continuous
./scripts/logs.sh --tail– scan for traceback spikes in backend/worker containers.docker compose -f production/production.yaml ps– confirm containers areUpand restarts are zero.bench doctor(inside backend) for a consolidated health check.- Respond to Traefik cert emails quickly—renewals are automatic, but DNS issues show up here first.
Weekly
- Validate backups:
ls -lh backups/$(date +%Y-*)and run a dry-run restore in staging (bench --site staging.local restore ...). - Apply OS security patches (
sudo unattended-upgradeor manualapt update && apt upgrade). - Review pending PRs from upstream frappe_docker—if a fix matters to you, merge it into
devearly.
Monthly & release cadence
- Pick a low-traffic window, announce downtime, and tag the commit you plan to deploy.
- Create a staging bench (optional but recommended) and run
./scripts/deploy.sh --regenerate+./scripts/deploy.shagainst it. - Execute automated checks:
bench --site staging.local migrate,bench --site staging.local build, and any Cypress/API smoke tests you maintain. - Freeze the container tags (
CUSTOM_TAG,ERPNEXT_VERSION,FRAPPE_VERSION) before moving to production.
Scheduled maintenance workflow
- Notify + prep – enable maintenance banners in ERPNext, pause schedulers if heavy migrations are expected.
- Fresh backups –
./scripts/backup-site.sh <site> --with-files --auto-copy --cleanup-old latest. - Deploy –
./scripts/stop.sh,docker compose -f production/production.yaml pull,./scripts/deploy.sh. - Post-deploy bench tasks – run
bench --site <site> migrate,build, andclear-cache(see Apply Updates to Sites). - Smoke tests – log in as an admin, run a report, submit a Sales Order, and ping critical integrations/webhooks.
- Resume schedulers –
bench --site <site> enable-scheduler. - Document – capture version/tag, time, and any anomalies for traceability.
Rollback fast path
If something regresses:
# 1. Stop new containers and bring back previous tags
git checkout main~1 # or the last known-good tag
./scripts/deploy.sh --regenerate
./scripts/stop.sh && docker compose -f production/production.yaml up -d
# 2. Restore data (if schema already changed)
docker compose -f production/production.yaml exec backend \
bench --site erp.example.com restore /backups/erp.example.com-2024-09-15-1200.sql.gz
# 3. Reapply files if needed
tar -xzf /backups/erp.example.com-2024-09-15-1200-files.tar.gz -C sites
- The backup script keeps copies on the Docker host when
--auto-copyis used—document the host path for on-call engineers. - After rollback, note the incident in your changelog and keep staging up until a fixed build is ready.
Operational hygiene checklist
- ✅ Backups verified in the last 7 days
- ✅ Security patches applied (OS + ERPNext release notes reviewed)
- ✅ Custom app tags mapped to deployed commit SHAs
- ✅ Monitoring + alerting endpoints tested (uptime checks, SMTP alerts, etc.)
- ✅ Runbook stored with credentials/secrets in the team password manager
Troubleshooting
Site Not Accessible
Check DNS:
dig +short erp.example.com
nslookup erp.example.com
Check Traefik:
docker compose -f production/production.yaml logs traefik
Check frontend:
docker compose -f production/production.yaml logs frontend
SSL Certificate Issues
Check Let's Encrypt logs:
docker compose -f production/production.yaml logs traefik | grep -i acme
Common causes:
- DNS not propagated (wait 24-48 hours)
- Ports 80/443 not open
- Rate limit (5 certs/week per domain)
Test certificate:
curl -vI https://erp.example.com 2>&1 | grep -i certificate
Database Connection Failed
Check MariaDB:
docker compose -f production/production.yaml logs mariadb
Test connection:
docker compose -f production/production.yaml exec backend \
mysql -h mariadb-database -u root -p
# Enter DB_PASSWORD from production.env
Common causes:
DB_PASSWORDmismatch betweenproduction.envandmariadb.envDB_HOSTshould bemariadb-database(container name)
Custom App Deployment Issues
Verify the image really contains your apps:
docker compose -f production/production.yaml exec backend bench list-apps
docker compose -f production/production.yaml exec backend cat apps.txt
If the app is missing, rebuild the image with the correct apps.json, push it, and redeploy.
Confirm the right image is running:
docker compose -f production/production.yaml images | grep backend
grep CUSTOM_TAG production/production.env
Re-apply the app to a site:
docker compose -f production/production.yaml exec backend \
bench --site erp.example.com install-app custom_integrations
docker compose -f production/production.yaml exec backend \
bench --site erp.example.com migrate
docker compose -f production/production.yaml exec backend \
bench --site erp.example.com build
Integration secrets not picked up?
- Run
bench --site erp.example.com show-config | grep -i API_KEYto ensureset-configwrote the value. - Restart background workers if you changed environment variables:
docker compose -f production/production.yaml restart queue-short queue-long scheduler.
Service Crash Loops
Check logs:
./scripts/logs.sh <service-name>
Check resources:
docker stats
free -h
df -h
Restart:
./scripts/stop.sh --all
./scripts/deploy.sh
Performance Issues
Check worker count:
docker ps | grep erpnext-production
Scale workers (edit production/production.env):
WORKERS=4
QUEUE_WORKERS=6
Then redeploy:
./production/scripts/deploy.sh --regenerate # Regenerate config
./production/scripts/deploy.sh # Apply changes
Security
1. Never Commit Secrets
# Verify .env files are ignored
git check-ignore production/production.env
git check-ignore production/mariadb.env
git check-ignore production/traefik.env
# All should return the filename (means they're ignored)
2. Use Strong Passwords
# Always generate with OpenSSL
openssl rand -base64 32 # 32 characters
openssl rand -base64 48 # 48 characters (more secure)
3. Restrict File Permissions
chmod 600 production/*.env
chmod 755 production/scripts/*.sh
4. Enable Firewall
sudo ufw enable
sudo ufw status
5. Keep System Updated
# System updates
sudo apt update && sudo apt upgrade -y
# Docker updates
sudo apt install --only-upgrade docker-ce docker-compose-plugin
Setup Automated Backups
# Add to crontab
crontab -e
# Daily backup at 2 AM with auto-copy and cleanup
0 2 * * * cd /path/to/erp-is-test/production && AUTO_COPY=1 CLEANUP_OLD=1 ./scripts/backup-site.sh erp.example.com
# Weekly encrypted backup
0 3 * * 0 cd /path/to/erp-is-test/production && BACKUP_PASSPHRASE='your-secret' ./scripts/backup-site.sh erp.example.com --encrypt --auto-copy
Backup Encryption
Setup GPG for backups:
# Install GPG (usually pre-installed)
sudo apt install gnupg -y
# Set backup passphrase
export BACKUP_PASSPHRASE='your-very-secure-passphrase'
# Create encrypted backup
./scripts/backup-site.sh erp.example.com --encrypt --auto-copy
Decrypt backups:
# Decrypt single file
gpg --decrypt backup-file.gpg > backup-file
# Decrypt with passphrase from environment
echo "$BACKUP_PASSPHRASE" | gpg --batch --passphrase-fd 0 --decrypt file.gpg > file
# Decrypt all .gpg files in directory
for f in *.gpg; do gpg --decrypt "$f" > "${f%.gpg}"; done
Backup Monitoring
Check backup status:
# View backup logs
tail -f /tmp/erpnext-backup-$(date +%Y%m%d).log
# List backups
ls -lah ./backups/
# Verify backup integrity
./scripts/backup-site.sh erp.example.com --debug
7. Monitor Logs
# Setup log rotation
sudo nano /etc/logrotate.d/docker-compose
# Add:
/path/to/erp-is/production/logs/*.log {
daily
rotate 14
compress
delaycompress
missingok
notifempty
}
8. Backup Your Passwords
Store securely in password manager:
- Server IP and SSH credentials
- Domain and DNS credentials
- Database passwords (DB_PASSWORD)
- Admin password (ADMIN_PASSWORD)
- Traefik dashboard password
- Backup encryption passphrase
- SSL certificate information
Environment Variables Reference
Global Script Variables:
# Project configuration
PROJECT_NAME=erpnext-production # Docker project name
BACKUP_RETENTION_DAYS=30 # Backup retention period
# Backup script variables
AUTO_COPY=1 # Auto-copy to host
CLEANUP_OLD=1 # Auto-cleanup old backups
BACKUP_PASSPHRASE='your-secret' # GPG encryption passphrase
DEBUG=1 # Enable debug output
# Usage example
AUTO_COPY=1 CLEANUP_OLD=1 DEBUG=1 ./scripts/backup-site.sh erp.example.com
Backup Environment Variables:
# Set persistent environment variables
echo 'export BACKUP_PASSPHRASE="your-secure-passphrase"' >> ~/.bashrc
echo 'export AUTO_COPY=1' >> ~/.bashrc
echo 'export CLEANUP_OLD=1' >> ~/.bashrc
source ~/.bashrc
# Now backups are simpler
./scripts/backup-site.sh erp.example.com --encrypt
Environment Files Explained
Why Three Files?
Different Docker Compose projects use different files:
-
Traefik project →
traefik.env- Reverse proxy configuration
- SSL certificate management
- Dashboard access
-
MariaDB project →
mariadb.env- Database root password
- Shared across all ERPNext instances
-
ERPNext project →
production.env- Application configuration
- Database connection
- Redis configuration
- Admin password
Critical: DB Password Must Match
# production.env
DB_PASSWORD=Q2f7k9Lm3nP5rT8wX1zC4vB6hJ0yN2sA
# mariadb.env
DB_PASSWORD=Q2f7k9Lm3nP5rT8wX1zC4vB6hJ0yN2sA # ← MUST BE SAME!
If different, ERPNext cannot connect to database.
Architecture Explained
Image Sources
This setup uses official Frappe Docker images:
# Your fork provides:
- Infrastructure: compose.yaml, deployment scripts
- Custom app tooling: apps.json manifest, image overrides, site automation
# Frappe provides:
- Container images: frappe/erpnext, frappe/frappe
- Base configurations: upstream compose files
Image pull locations:
# From Docker Hub (official Frappe images)
frappe/erpnext:v15.82.1
frappe/frappe:v15.82.1
library/mariadb:11.8
library/redis:alpine
traefik:v2.11
You maintain:
production/directory (deployment scripts, configs)apps.json(or CI secrets) describing the custom/third-party apps you ship.gitignore(excludes *.env files)
You track upstream:
compose.yaml(base infrastructure)overrides/compose.*.yaml(feature overlays)images/*/Containerfile(if you need custom builds)
Why This Approach?
Benefits:
- ✅ Get official, tested ERPNext images
- ✅ Receive infrastructure updates from frappe_docker
- ✅ Keep your custom apps and integrations isolated from infrastructure changes
- ✅ Easy to merge upstream improvements
- ✅ Only rebuild images when you really need additional apps or dependencies
When you'd build custom images:
- Need to modify Python dependencies
- Add system packages to containers
- Install or update custom/third-party Frappe apps
Support
- ERPNext Forum: https://discuss.erpnext.com/
- Documentation: https://docs.erpnext.com/
- Upstream Repository: https://github.com/frappe/frappe_docker
- GitHub Issues: https://github.com/frappe/frappe_docker/issues
Tested With: ERPNext v15.82.1, Docker 24.0+, Ubuntu 22.04 LTS
Script Optimization: 35% reduction in code, 100% help coverage
Last Updated: October 2025