frappe_docker/production/docs
2025-11-19 00:11:27 +05:30
..
custom-image-workflow.md docs: Add comprehensive guide for building custom ERPNext images for production 2025-11-19 00:11:27 +05:30
README.md docs: Add comprehensive guide for building custom ERPNext images for production 2025-11-19 00:11:27 +05:30

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:

  1. Traefik - Reverse proxy, SSL certificates, load balancing
  2. MariaDB - Shared database for all ERPNext sites
  3. 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

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 (Lets 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-copy mirrors to the host; use --flat-host-path (default) to drop files directly in $HOST_BACKUP_ROOT or --nested-host-path/HOST_BACKUP_LAYOUT=nested for $HOST_BACKUP_ROOT/<site>/<timestamp>. Add --host-only to delete container copies once the host copy is verified.
  • Cleanup policies: --cleanup-old accepts N (days), keep:N (retain newest runs), or latest (keep only the current run); defaults to BACKUP_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 latest
    

    This 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 30day retention

    AUTO_COPY=1 CLEANUP_OLD=1 ./scripts/backup-site.sh erp.example.com
    

    Environment flags keep the command short for crontab entries. Set BACKUP_RETENTION_DAYS globally (defaults to 30) or export CLEANUP_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:

  1. backend - Gunicorn application server
  2. frontend - Nginx reverse proxy
  3. websocket - Socket.io for real-time features
  4. queue-short - Short-running background jobs
  5. queue-long - Long-running background jobs
  6. scheduler - Cron-like background scheduler
  7. 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

  1. production/production.env - ERPNext application config
  2. production/mariadb.env - Database config
  3. production/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:

  1. Validates environment configuration (./scripts/validate-env.sh)
  2. Deploys Traefik (reverse proxy + SSL)
  3. Deploys MariaDB (database)
  4. 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
  5. 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

  1. Open browser: https://erp.example.com
  2. Username: Administrator
  3. Password: Check production/production.envADMIN_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

📚 Technical Deep-Dive: For understanding different deployment patterns and asset management approaches, see production/docs/asset-management-frappe.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_BRANCH build 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=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:

  1. Installs all apps from apps.json
  2. Installs Python and Node.js dependencies
  3. Runs bench build - compiles all JS/CSS assets
  4. 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 \
  .

# 3. Push new image
docker push $NEW_TAG
docker push ghcr.io/YOUR_USERNAME/erpnext-custom:production-latest

# 4. Update production.env
nano production/production.env
# CUSTOM_TAG=20251119-xyz5678  # New date-commit tag

# 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

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-files to include uploaded files
  • Auto-copy: --auto-copy copies backups to host $HOST_BACKUP_ROOT/<site>/<timestamp>
  • Host-only: --host-only deletes container copies after verifying the host snapshot
  • Encryption: --encrypt with GPG AES256 (requires BACKUP_PASSPHRASE)
  • Cleanup: --cleanup-old accepts N (days), keep:N (runs), or latest to 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 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:

  1. Infrastructure Layer (frappe_docker)

    • Docker Compose configurations
    • Container build instructions
    • Deployment scripts structure
    • Updates via: git fetch upstream && git merge upstream/main
  2. Application Layer (ERPNext/Frappe)

    • ERPNext features and bug fixes
    • Frappe framework updates
    • Updates via: Changing version tags in production.env
  3. 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

  1. Run ./scripts/create-site.sh new.example.com once the new image is live.
  2. Install any optional apps: bench --site new.example.com install-app custom_integrations hrms.
  3. 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

  1. Stop users (maintenance window) and take a backup: ./scripts/backup-site.sh erp.example.com --with-files.
  2. 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
  1. If the update introduced new apps, install them explicitly and migrate again.
  2. 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
nano production/apps.json
# Example: Update Frappe, ERPNext, or custom apps
# Frappe: "branch": "v15.88.1" → "branch": "v15.89.0"
# India Compliance: "branch": "v15.23.2" → "branch": "v15.24.0"

# 2. 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=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

# 3. Update production.env with new tag
nano production/production.env
# CUSTOM_TAG=20251119-xyz5678

# 4. Deploy
./scripts/deploy.sh --regenerate
./scripts/deploy.sh

# 5. 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).
  • upstreamhttps://github.com/frappe/frappe_docker.git.

Sync loop (weekly)

  1. git checkout dev && git fetch upstream bring in the latest frappe_docker changes.
  2. git merge upstream/main (or git rebase upstream/main) resolve conflicts in overrides/, compose.yaml, and production/ scripts.
  3. Smoke-test locally (./scripts/deploy.sh --regenerate && docker compose -f production/production.yaml up -d).
  4. Promote into staging: git switch staging && git merge dev once tests pass.
  5. Cut a tested release into main only after staging verification and update the stack-version table + .env pins.

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.json with that tag/commit. This manifest is the source of truth for APPS_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_BRANCH=version-15 \
      --build-arg=APPS_JSON_BASE64=$APPS_JSON_BASE64 \
      --tag=$NEW_TAG \
      --file=images/layered/Containerfile .
    
    docker push $NEW_TAG
    
  • Update production/production.env with the new CUSTOM_TAG, regenerate, then migrate each site.

Helpful habits

  • Use git worktree to keep dev, staging, and main checked out simultaneously so you can hotfix production without stashing local experiments.
  • Commit regenerated production/production.yaml only 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 bench commands exactly like python 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 are Up and 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-upgrade or manual apt update && apt upgrade).
  • Review pending PRs from upstream frappe_docker—if a fix matters to you, merge it into dev early.

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.sh against 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

  1. Notify + prep enable maintenance banners in ERPNext, pause schedulers if heavy migrations are expected.
  2. Fresh backups ./scripts/backup-site.sh <site> --with-files --auto-copy --cleanup-old latest.
  3. Deploy ./scripts/stop.sh, docker compose -f production/production.yaml pull, ./scripts/deploy.sh.
  4. Post-deploy bench tasks run bench --site <site> migrate, build, and clear-cache (see Apply Updates to Sites).
  5. Smoke tests log in as an admin, run a report, submit a Sales Order, and ping critical integrations/webhooks.
  6. Resume schedulers bench --site <site> enable-scheduler.
  7. 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-copy is 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_PASSWORD mismatch between production.env and mariadb.env
  • DB_HOST should be mariadb-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_KEY to ensure set-config wrote 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:

  1. Traefik projecttraefik.env

    • Reverse proxy configuration
    • SSL certificate management
    • Dashboard access
  2. MariaDB projectmariadb.env

    • Database root password
    • Shared across all ERPNext instances
  3. ERPNext projectproduction.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, 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


Tested With: ERPNext v15.88.1, Frappe v15.88.1, Docker 24.0+, Ubuntu 22.04 LTS
Deployment Method: Immutable images with pre-compiled assets
Last Updated: November 2025