| .. | ||
| custom-image-workflow.md | ||
| README.md | ||
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.88.1 - ERP application
- Frappe Framework v15.88.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 24.04 LTS | All scripts are validated on this release; Debian 11/12 behave the same as long as Docker 24+ is installed. |
| ERPNext | v15.88.1 | Set through custom image built with apps.json. For immutable images, version is frozen at build time. |
| Frappe Framework | v15.88.1 | Controlled via FRAPPE_BRANCH build arg when building custom images. 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 22.04/24.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.88.1 # ERPNext version
FRAPPE_VERSION=v15.88.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
Production Standard: This guide covers deploying ERPNext with custom or third-party apps using immutable Docker images with pre-compiled assets. This approach provides reproducible deployments, instant rollbacks, and eliminates runtime build complexity.
📖 Detailed Guide: For comprehensive implementation details, troubleshooting, and CI/CD integration, see
production/docs/custom-image-workflow.md
Why Custom Images?
- True Immutability: Apps frozen at specific versions (tags/commits)
- Zero Runtime Builds: Assets pre-compiled during image build
- Reliable Rollbacks: Switch image tags to revert instantly
- Upstream-Safe: Custom logic isolated from infrastructure updates
- Audit Trail: Image tag maps to exact deployed code
1. Create apps.json with Pinned Versions
Create production/apps.json listing custom/third-party apps only with pinned versions:
[
{
"url": "https://github.com/frappe/erpnext",
"branch": "v15.88.1"
},
{
"url": "https://github.com/resilient-tech/india-compliance",
"branch": "v15.23.2"
},
{
"url": "https://github.com/frappe/hrms",
"branch": "v15.12.0"
}
]
Important:
- Frappe Framework is controlled via
FRAPPE_BRANCHbuild arg (not in apps.json) - Use specific tags (e.g.,
v15.88.1) for custom apps, NOT moving branches (e.g.,version-15) - This ensures reproducible builds—same apps.json = identical image
Find available versions:
# Check tags on GitHub
curl -s https://api.github.com/repos/frappe/erpnext/tags | grep '"name"' | head -5
curl -s https://api.github.com/repos/resilient-tech/india-compliance/tags | grep '"name"' | head -5
# For Frappe Framework (use as FRAPPE_BRANCH build arg)
curl -s https://api.github.com/repos/frappe/frappe/tags | grep '"name"' | head -5
2. Build Immutable Image
Build a custom image with your apps and pre-compiled assets:
# Encode apps.json
export APPS_JSON_BASE64=$(base64 -w0 production/apps.json)
# Generate traceable image tag (date + git commit)
BUILD_DATE=$(date +%Y%m%d)
GIT_SHA=$(git rev-parse --short HEAD)
IMAGE_TAG="ghcr.io/YOUR_USERNAME/erpnext-custom:${BUILD_DATE}-${GIT_SHA}"
# Build image (includes bench build - assets compiled into image)
docker build \
--build-arg=FRAPPE_PATH=https://github.com/frappe/frappe \
--build-arg=FRAPPE_BRANCH=v15.88.1 \
--build-arg=APPS_JSON_BASE64=$APPS_JSON_BASE64 \
--build-arg=PYTHON_VERSION=3.11.6 \
--build-arg=NODE_VERSION=18.18.2 \
--tag=$IMAGE_TAG \
--tag=ghcr.io/YOUR_USERNAME/erpnext-custom:production-latest \
--file=images/layered/Containerfile \
.
What happens during build:
- Installs all apps from
apps.json - Installs Python and Node.js dependencies
- Runs
bench build- compiles all JS/CSS assets - Creates immutable image with everything baked in
Push to registry:
# Push specific version (for production)
docker push ghcr.io/YOUR_USERNAME/erpnext-custom:${BUILD_DATE}-${GIT_SHA}
# Push latest tag (convenience pointer)
docker push ghcr.io/YOUR_USERNAME/erpnext-custom:production-latest
Image tagging strategy:
20251118-4c860c6- Immutable tag for production (date + git commit)production-latest- Mutable pointer to newest build (for staging/testing)
3. Update Production Configuration
Edit production/production.env to use your custom image:
CUSTOM_IMAGE=ghcr.io/YOUR_USERNAME/erpnext-custom
CUSTOM_TAG=20251118-4c860c6 # Use your BUILD_DATE-GIT_SHA
PULL_POLICY=always
Important: Use the specific date-commit tag in production, not production-latest. This ensures you can rollback by simply changing the tag.
Regenerate configuration and deploy:
./scripts/deploy.sh --regenerate # Updates production.yaml with new image
./scripts/deploy.sh # Pulls and deploys new image
Verify all containers use the same image:
docker compose -f production/production.yaml images
4. Install Apps on Sites
Apps are in the image but need to be activated per site.
New site with apps:
./scripts/create-site.sh erp.example.com
docker compose -f production/production.yaml exec backend \
bench --site erp.example.com install-app india_compliance hrms
Existing site - add new app:
# Install the app on the site
docker compose -f production/production.yaml exec backend \
bench --site erp.example.com install-app india_compliance
# Run database migrations
docker compose -f production/production.yaml exec backend \
bench --site erp.example.com migrate
That's it! No bench build or asset sync needed—assets are already compiled and present in all containers from the image.
Verify it works:
# Check installed apps
docker compose -f production/production.yaml exec backend \
bench --site erp.example.com list-apps
# Test site access
curl -k -I https://erp.example.com/app/home
5. Update Apps
When apps release new versions:
# 1. Update apps.json with new versions (custom apps only)
nano production/apps.json
# Example: India Compliance: "branch": "v15.23.2" → "branch": "v15.24.0"
# 2. Update Frappe version if needed (via build arg)
# Check available versions: curl -s https://api.github.com/repos/frappe/frappe/tags | grep '"name"'
# 3. Rebuild image with new tag
export APPS_JSON_BASE64=$(base64 -w0 production/apps.json)
NEW_TAG="ghcr.io/YOUR_USERNAME/erpnext-custom:$(date +%Y%m%d)-$(git rev-parse --short HEAD)"
docker build \
--build-arg=APPS_JSON_BASE64=$APPS_JSON_BASE64 \
--tag=$NEW_TAG \
--tag=ghcr.io/YOUR_USERNAME/erpnext-custom:production-latest \
--file=images/layered/Containerfile \
.
# 4. Push new image
docker push $NEW_TAG
docker push ghcr.io/YOUR_USERNAME/erpnext-custom:production-latest
# 5. Update production.env
nano production/production.env
# CUSTOM_TAG=20251119-xyz5678 # New date-commit tag
# 6. Deploy
./scripts/deploy.sh --regenerate
./scripts/deploy.sh
# 7. Migrate all sites
docker compose -f production/production.yaml exec backend \
bench --site erp.example.com migrate
# 8. Clear cache and restart
docker compose -f production/production.yaml exec backend \
bench --site erp.example.com clear-cache
docker compose -f production/production.yaml restart frontend
Rollback if needed:
# Just revert to previous tag
nano production/production.env
# CUSTOM_TAG=20251118-4c860c6 # Previous working version
./scripts/deploy.sh
# Old image still exists in registry!
6. Uninstall Apps
Remove an app from a site:
# 1. Backup first (uninstall deletes DocTypes and data!)
./scripts/backup-site.sh erp.example.com --with-files --auto-copy
# 2. Uninstall from site
docker compose -f production/production.yaml exec backend \
bench --site erp.example.com uninstall-app india_compliance
# 3. Clear cache and restart
docker compose -f production/production.yaml exec backend \
bench --site erp.example.com clear-cache
docker compose -f production/production.yaml restart frontend
Notes:
- The app remains in the image's
/apps/but is deactivated on the site - To completely remove: rebuild image without it in
apps.json - Check dependencies before uninstalling
- Always backup first—uninstall deletes all app data
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
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 apps from the image
docker compose -f production/production.yaml exec backend \
bench --site erp2.example.com install-app india_compliance hrms
# Run migrations
docker compose -f production/production.yaml exec backend \
bench --site erp2.example.com migrate
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/Frappe Base Images (Official Releases)
⚠️ Important: This section is for updating the base Frappe/ERPNext images (official releases without custom apps). If you're using custom images with apps.json, skip to Update Custom Apps & Integrations instead.
What this updates: ERPNext features, bug fixes, Frappe framework (base images only)
# 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.88.1 → ERPNEXT_VERSION=v15.90.0
# FRAPPE_VERSION=v15.88.1 → FRAPPE_VERSION=v15.90.0
# Note: Only use this if NOT using custom images with apps.json
# 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
docker compose -f production/production.yaml exec backend \
bench --site erp.example.com clear-cache
# Note: No bench build needed with immutable images - assets pre-compiled in image!
# 10. Verify new version
docker compose -f production/production.yaml exec backend bench version
Apply Updates to Sites
With immutable images, the workflow is identical for both new and existing sites:
- Deploy the new image (apps are already in the image)
- Install apps on sites using
bench install-app - Run
bench migrateto apply database changes
New sites:
./scripts/create-site.sh new.example.com
docker compose -f production/production.yaml exec backend \
bench --site new.example.com install-app india_compliance
docker compose -f production/production.yaml exec backend \
bench --site new.example.com migrate
Existing sites
- Stop users (maintenance window) and take a backup:
./scripts/backup-site.sh erp.example.com --with-files. - After redeploying containers, run migrations:
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 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 apps with new features or bug fixes.
# 1. Update apps.json with new versions (custom apps only)
nano production/apps.json
# Example: Update custom apps (ERPNext, India Compliance, etc.)
# ERPNext: "branch": "v15.88.1" → "branch": "v15.89.0"
# India Compliance: "branch": "v15.23.2" → "branch": "v15.24.0"
# 2. Update Frappe version via build arg (if needed)
# Change FRAPPE_BRANCH in the docker build command below
# Example: v15.88.1 → v15.89.0
# 3. Rebuild the image with new tag
export APPS_JSON_BASE64=$(base64 -w0 production/apps.json)
NEW_TAG="ghcr.io/YOUR_USERNAME/erpnext-custom:$(date +%Y%m%d)-$(git rev-parse --short HEAD)"
docker build \
--build-arg=FRAPPE_PATH=https://github.com/frappe/frappe \
--build-arg=FRAPPE_BRANCH=v15.88.1 \
--build-arg=APPS_JSON_BASE64=$APPS_JSON_BASE64 \
--tag=$NEW_TAG \
--tag=ghcr.io/YOUR_USERNAME/erpnext-custom:production-latest \
--file=images/layered/Containerfile .
docker push $NEW_TAG
docker push ghcr.io/YOUR_USERNAME/erpnext-custom:production-latest
# 4. Update production.env with new tag
nano production/production.env
# CUSTOM_TAG=20251119-xyz5678
# 5. Deploy
./scripts/deploy.sh --regenerate
./scripts/deploy.sh
# 6. Migrate all sites
docker compose -f production/production.yaml exec backend \
bench --site erp.example.com migrate
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. Clear cache
docker compose -f production/production.yaml exec backend \
bench --site erp.example.com clear-cache
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
production/apps.jsonwith that tag/commit. This manifest is the source of truth forAPPS_JSON_BASE64. -
Rebuild/push the custom image:
export APPS_JSON_BASE64=$(base64 -w0 production/apps.json) NEW_TAG="ghcr.io/YOUR_USERNAME/erpnext-custom:$(date +%Y%m%d)-$(git rev-parse --short HEAD)" docker build \ --build-arg=FRAPPE_PATH=https://github.com/frappe/frappe \ --build-arg=FRAPPE_BRANCH=v15.88.1 \ --build-arg=APPS_JSON_BASE64=$APPS_JSON_BASE64 \ --tag=$NEW_TAG \ --file=images/layered/Containerfile . docker push $NEW_TAG -
Update
production/production.envwith the newCUSTOM_TAG, regenerate, then migrate 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. Copy backup files from host to container (if backups are on host)
docker cp production/backups/erp.example.com-2024-09-15-1200.sql.gz \
$(docker compose -f production/production.yaml ps -q backend):/tmp/
# 3. Restore data (if schema already changed)
docker compose -f production/production.yaml exec backend \
bench --site erp.example.com restore /tmp/erp.example.com-2024-09-15-1200.sql.gz
# Note: If prompted for MySQL root password, use the DB_PASSWORD from production/production.env
# 4. Reapply files if needed
docker cp production/backups/erp.example.com-2024-09-15-1200-files.tar.gz \
$(docker compose -f production/production.yaml ps -q backend):/tmp/
docker compose -f production/production.yaml exec backend \
tar -xzf /tmp/erp.example.com-2024-09-15-1200-files.tar.gz -C /home/frappe/frappe-bench/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 india_compliance
docker compose -f production/production.yaml exec backend \
bench --site erp.example.com migrate
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.88.1
frappe/frappe:v15.88.1
library/mariadb:11.8
library/redis:alpine
traefik:v2.11
You maintain:
production/directory (deployment scripts, configs, docs)production/apps.json- manifest for custom/third-party apps.gitignore(excludes *.env files)
You track upstream:
compose.yaml(base infrastructure)overrides/compose.*.yaml(feature overlays)images/layered/Containerfile(for building custom images)
Why This Approach?
Benefits:
- ✅ Get official, tested ERPNext images (or build custom ones)
- ✅ Receive infrastructure updates from frappe_docker
- ✅ Keep custom apps isolated from infrastructure changes
- ✅ Easy to merge upstream improvements
- ✅ Pre-compiled assets eliminate runtime build complexity
When to build custom images:
- Adding custom or third-party Frappe apps
- Modifying Python/Node dependencies
- Adding system packages
- Need reproducible production deployments
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.88.1, Frappe v15.88.1, Docker 24.0+, Ubuntu 24.04 LTS
Deployment Method: Immutable images with pre-compiled assets
Last Updated: November 2025