# Building Custom ERPNext Images for Production **Production-Grade Workflow for Third-Party and Custom Apps** This guide covers the **Pattern 2 (Gold Standard)** approach: building immutable Docker images with custom apps and pre-compiled assets. This is the recommended method for production deployments. --- ## Table of Contents 1. [Overview](#overview) 2. [Prerequisites](#prerequisites) 3. [Quick Start](#quick-start) 4. [Step-by-Step Guide](#step-by-step-guide) 5. [Real-World Example: India Compliance](#real-world-example-india-compliance) 6. [Adding Custom Apps](#adding-custom-apps) 7. [Updating Apps](#updating-apps) 8. [Uninstall Apps](#uninstall-apps) 9. [Deployment Workflow](#deployment-workflow) 10. [Troubleshooting](#troubleshooting) 11. [Best Practices](#best-practices) --- ## Overview ### What This Achieves - ✅ **True Immutability**: Apps frozen at specific versions (tags/commits) - ✅ **Zero Runtime Builds**: No `bench build` needed in production - ✅ **No Asset Sync**: All containers have identical `/apps/` trees - ✅ **Fast Deployments**: Pull image → deploy → activate on sites - ✅ **Reliable Rollbacks**: Switch image tags instantly - ✅ **Audit Trail**: Image tag = exact code deployed ### When to Use This Method - ✅ Production environments - ✅ Need reproducible deployments - ✅ Regulatory compliance required - ✅ Apps change weekly/monthly (not daily) - ✅ Want reliable rollbacks ### Key Difference from Runtime Install (Pattern 3) | Aspect | Pattern 3 (Runtime) | Pattern 2 (This Guide) | |--------|---------------------|------------------------| | Apps installed | At runtime with `bench get-app` | Baked into image at build time | | Assets compiled | `bench build` in production | Pre-compiled during image build | | Asset sync | Manual `tar` pipeline required | Not needed - assets in image | | Immutability | Partial (apps can drift) | Complete (frozen versions) | | Rollback | Complex | Change image tag | --- ## Prerequisites ### Required Tools ```bash # Verify Docker is installed docker --version # Need 20.10+ # Verify git is available git --version # Verify you have base64 base64 --version ``` ### GitHub Container Registry Access 1. Create Personal Access Token: - Go to: https://github.com/settings/tokens/new - Select scope: `write:packages` - Generate token and save it securely 2. Login to GitHub Container Registry: ```bash export GITHUB_TOKEN=your_token_here echo $GITHUB_TOKEN | docker login ghcr.io -u YOUR_USERNAME --password-stdin ``` 3. Verify login: ```bash docker pull ghcr.io/YOUR_USERNAME/test || echo "Ready to push" ``` --- ## Quick Start **5-minute walkthrough** for experienced users: ```bash # 1. Define apps with pinned versions (custom apps only) cat > production/apps.json < production/apps.json <> production/production.env # 6. Deploy ./scripts/deploy.sh --regenerate ./scripts/deploy.sh # 7. Activate on site docker compose -f production/production.yaml exec backend \ bench --site erp.localhost install-app india_compliance docker compose -f production/production.yaml exec backend \ bench --site erp.localhost migrate # 8. Verify docker compose -f production/production.yaml exec backend \ bench --site erp.localhost list-apps ``` ### What You Get - ✅ India Compliance v15.23.2 installed - ✅ All GST, TDS, and compliance features available - ✅ Assets pre-compiled (no 404 errors) - ✅ Can rollback to previous image anytime - ✅ Image SHA = exact deployed code --- ## Adding Custom Apps ### Scenario: Add Your Own Frappe App **1. Prepare your custom app**: ```bash cd /path/to/your/custom_integrations # Tag a release git tag v1.0.0 git push origin v1.0.0 # Or note the commit hash git log --oneline -1 # abc1234 Add webhook integration ``` **2. Add to apps.json**: ```json [ { "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/YOUR_ORG/custom_integrations", "branch": "v1.0.0" } ] ``` **3. Rebuild image** (same process as above): ```bash # New image tag reflects new date BUILD_DATE=$(date +%Y%m%d) # 20251119 GIT_SHA=$(git rev-parse --short HEAD) export APPS_JSON_BASE64=$(base64 -w0 production/apps.json) docker build \ --build-arg=APPS_JSON_BASE64=$APPS_JSON_BASE64 \ --tag=ghcr.io/duthink/erpnext-custom:${BUILD_DATE}-${GIT_SHA} \ --tag=ghcr.io/duthink/erpnext-custom:production-latest \ --file=images/layered/Containerfile . docker push ghcr.io/duthink/erpnext-custom:${BUILD_DATE}-${GIT_SHA} docker push ghcr.io/duthink/erpnext-custom:production-latest ``` **4. Deploy new image**: ```bash # Update production config nano production/production.env # CUSTOM_TAG=20251119-def5678 ./scripts/deploy.sh --regenerate ./scripts/deploy.sh ``` **5. Install on sites**: ```bash docker compose -f production/production.yaml exec backend \ bench --site erp.localhost install-app custom_integrations docker compose -f production/production.yaml exec backend \ bench --site erp.localhost migrate ``` ### Private Repositories **For private GitHub repos**: ```json [ { "url": "https://YOUR_TOKEN@github.com/YOUR_ORG/private_app", "branch": "v1.0.0" } ] ``` **Security Note**: Never commit tokens to git! Use environment variables: ```bash # In CI/CD or local build export GITHUB_TOKEN=your_token export APPS_JSON_BASE64=$(cat production/apps.json | sed "s/YOUR_TOKEN/$GITHUB_TOKEN/g" | base64 -w0) ``` --- ## Updating Apps ### Scenario: India Compliance Releases v15.24.0 **1. Check for new version**: ```bash curl -s https://api.github.com/repos/resilient-tech/india-compliance/tags | grep '"name"' | head -3 # New output: "name": "v15.24.0" ``` **2. Update apps.json**: ```bash nano production/apps.json # Change: # "branch": "v15.23.2" → "branch": "v15.24.0" ``` **3. Rebuild with new tag**: ```bash export APPS_JSON_BASE64=$(base64 -w0 production/apps.json) BUILD_DATE=$(date +%Y%m%d) GIT_SHA=$(git rev-parse --short HEAD) docker build \ --build-arg=APPS_JSON_BASE64=$APPS_JSON_BASE64 \ --tag=ghcr.io/duthink/erpnext-custom:${BUILD_DATE}-${GIT_SHA} \ --tag=ghcr.io/duthink/erpnext-custom:production-latest \ --file=images/layered/Containerfile . docker push ghcr.io/duthink/erpnext-custom:${BUILD_DATE}-${GIT_SHA} docker push ghcr.io/duthink/erpnext-custom:production-latest ``` **4. Test in staging first** (recommended): ```bash # Use production-latest for staging nano staging/staging.env # CUSTOM_TAG=production-latest ./scripts/deploy-staging.sh # Test thoroughly... ``` **5. Deploy to production**: ```bash nano production/production.env # CUSTOM_TAG=20251125-xyz9999 # New specific tag ./scripts/deploy.sh --regenerate ./scripts/deploy.sh ``` **6. Migrate sites**: ```bash docker compose -f production/production.yaml exec backend \ bench --site erp.localhost migrate ``` **7. Rollback if issues**: ```bash # Just change to old tag nano production/production.env # CUSTOM_TAG=20251118-4c860c6 # Previous working version ./scripts/deploy.sh # Old image still exists in registry! ``` --- ## Deployment Workflow ### Standard Deployment Process ``` ┌─────────────────────────────────────────────────────────────┐ │ 1. Update apps.json with pinned versions │ │ └─ Commit to git │ │ │ │ 2. Build image │ │ └─ Tag with BUILD_DATE-GIT_SHA │ │ └─ Also tag as production-latest │ │ │ │ 3. Push to registry │ │ └─ Both tags pushed │ │ │ │ 4. Test (optional but recommended) │ │ └─ Pull production-latest in staging │ │ └─ Run smoke tests │ │ │ │ 5. Deploy to production │ │ └─ Update CUSTOM_TAG with specific tag │ │ └─ Regenerate production.yaml │ │ └─ Deploy (pulls new image) │ │ │ │ 6. Migrate sites │ │ └─ bench migrate on each site │ │ │ │ 7. Monitor │ │ └─ Check logs │ │ └─ Verify assets load │ │ └─ Test critical features │ └─────────────────────────────────────────────────────────────┘ ``` ### CI/CD Integration (GitHub Actions Example) ```yaml # .github/workflows/build-image.yml name: Build ERPNext Custom Image on: push: paths: - 'production/apps.json' - 'images/**' jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Login to GitHub Container Registry run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin - name: Build image run: | export APPS_JSON_BASE64=$(base64 -w0 production/apps.json) BUILD_DATE=$(date +%Y%m%d) GIT_SHA=$(git rev-parse --short HEAD) docker build \ --build-arg=APPS_JSON_BASE64=$APPS_JSON_BASE64 \ --tag=ghcr.io/${{ github.repository_owner }}/erpnext-custom:${BUILD_DATE}-${GIT_SHA} \ --tag=ghcr.io/${{ github.repository_owner }}/erpnext-custom:production-latest \ --file=images/layered/Containerfile . - name: Push image run: | BUILD_DATE=$(date +%Y%m%d) GIT_SHA=$(git rev-parse --short HEAD) docker push ghcr.io/${{ github.repository_owner }}/erpnext-custom:${BUILD_DATE}-${GIT_SHA} docker push ghcr.io/${{ github.repository_owner }}/erpnext-custom:production-latest ``` --- ## Uninstall Apps When you need to remove an app from a site: ```bash # 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 ``` **Important Notes:** - The app remains in the image's `/apps/` directory but is deactivated on the site - No `bench build` or asset sync needed—assets are pre-compiled in the image - Uninstall permanently deletes all app DocTypes and database records - Always backup before uninstalling - To completely remove an app from future deployments: rebuild image without it in `apps.json` **Complete Removal Workflow:** If you want to stop deploying an app entirely: ```bash # 1. Uninstall from all sites first docker compose -f production/production.yaml exec backend \ bench --site site1.example.com uninstall-app india_compliance docker compose -f production/production.yaml exec backend \ bench --site site2.example.com uninstall-app india_compliance # 2. Remove from apps.json nano production/apps.json # Delete the india_compliance entry # 3. Rebuild image without the app 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 # 4. Update production.env and deploy nano production/production.env # CUSTOM_TAG=20251119-newsha ./scripts/deploy.sh --regenerate ./scripts/deploy.sh ``` **Verify Clean State:** ```bash # Check app is not in image docker compose -f production/production.yaml exec backend bench list-apps # Check app is not active on sites docker compose -f production/production.yaml exec backend \ bench --site erp.example.com list-apps ``` --- ## Troubleshooting ### Issue: Build Fails with "App not found" **Symptom**: ``` ERROR: Could not find app: india_compliance ``` **Causes**: - Typo in repository URL - Branch/tag doesn't exist - Private repo without authentication **Solution**: ```bash # Verify URL and branch exist curl -I https://github.com/resilient-tech/india-compliance curl -I https://github.com/resilient-tech/india-compliance/tree/v15.23.2 # Check apps.json syntax cat production/apps.json | python3 -m json.tool ``` ### Issue: Build Fails with "Node modules not found" **Symptom**: ``` ERROR: Cannot find module 'xyz' ``` **Cause**: Upstream dependency issue in one of the apps **Solution**: ```bash # Try building with specific Node version docker build \ --build-arg=NODE_VERSION=18.18.2 \ ... # Or check app's package.json for required Node version ``` ### Issue: Assets Return 404 After Deployment **Symptom**: CSS/JS files show 404 in browser **This should NOT happen with Pattern 2**, but if it does: **Diagnosis**: ```bash # Verify all containers use same image docker compose -f production/production.yaml images # Check if assets exist in image docker compose -f production/production.yaml exec backend \ ls /home/frappe/frappe-bench/apps/india_compliance/india_compliance/public/dist # Check frontend can access them docker compose -f production/production.yaml exec frontend \ ls /home/frappe/frappe-bench/apps/india_compliance/india_compliance/public/dist ``` **Solution**: Rebuild image, ensure `bench build` completed during build. ### Issue: Image Size Too Large **Symptom**: Image is 3+ GB **Cause**: Includes development dependencies or build cache **Solution**: ```dockerfile # Multi-stage builds help (already in images/layered/Containerfile) # Ensure .dockerignore is present: cat > .dockerignore <