frappe_docker/production/docs/custom-image-workflow.md

27 KiB

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
  2. Prerequisites
  3. Quick Start
  4. Step-by-Step Guide
  5. Real-World Example: India Compliance
  6. Adding Custom Apps
  7. Updating Apps
  8. Uninstall Apps
  9. Deployment Workflow
  10. Troubleshooting
  11. 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

# 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:

  2. Login to GitHub Container Registry:

    export GITHUB_TOKEN=your_token_here
    echo $GITHUB_TOKEN | docker login ghcr.io -u YOUR_USERNAME --password-stdin
    
  3. Verify login:

    docker pull ghcr.io/YOUR_USERNAME/test || echo "Ready to push"
    

Quick Start

5-minute walkthrough for experienced users:

# 1. Define apps with pinned versions (custom apps only)
cat > production/apps.json <<EOF
[
  {
    "url": "https://github.com/frappe/erpnext",
    "branch": "v15.88.1"
  },
  {
    "url": "https://github.com/resilient-tech/india-compliance",
    "branch": "v15.23.2"
  }
]
EOF

# 2. Build immutable image
export APPS_JSON_BASE64=$(base64 -w0 production/apps.json)
BUILD_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=$BUILD_TAG \
  --tag=ghcr.io/YOUR_USERNAME/erpnext-custom:production-latest \
  --file=images/layered/Containerfile .

# 3. Push to registry
docker push $BUILD_TAG
docker push ghcr.io/YOUR_USERNAME/erpnext-custom:production-latest

# 4. Update production config
nano production/production.env
# Set: CUSTOM_TAG=20251118-4c860c6

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

# 6. Activate apps on sites
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

Step-by-Step Guide

Step 1: Find App Versions

Goal: Pin exact versions for immutability

For Third-Party Apps (GitHub)

# Find latest stable tags
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

# Or browse tags on GitHub:
# https://github.com/frappe/frappe/tags
# https://github.com/frappe/erpnext/tags
# https://github.com/resilient-tech/india-compliance/tags

Example output:

"name": "v15.88.1",   Use this specific tag
"name": "v15.88.0",
"name": "v15.87.2",

For Custom Apps (Your Repository)

# Tag your custom app first
cd /path/to/your/custom-app
git tag v1.0.0
git push origin v1.0.0

# Or use specific commit
git log --oneline -5
# abc1234 Fix invoice bug  ← Use this commit hash

Step 2: Create apps.json with Pinned Versions

Location: production/apps.json

Bad Example (not immutable):

[
  {
    "url": "https://github.com/frappe/erpnext",
    "branch": "version-15"   WRONG: Moving target!
  }
]

Good Example (immutable):

[
  {
    "url": "https://github.com/frappe/erpnext",
    "branch": "v15.88.1"   CORRECT: Frozen version
  },
  {
    "url": "https://github.com/resilient-tech/india-compliance",
    "branch": "v15.23.2"   Specific tag
  },
  {
    "url": "https://github.com/YOUR_ORG/custom-hrms-integration",
    "branch": "v2.1.0"   Your custom app
  }
]

Using Commit Hashes (even more precise):

[
  {
    "url": "https://github.com/frappe/erpnext",
    "branch": "version-15",
    "commit": "a1b2c3d4e5f6"   Exact commit
  }
]

Edit the file:

nano production/apps.json

Important: \n- Do NOT include Frappe Framework in apps.json - it's controlled via FRAPPE_BRANCH build arg\n- The upstream Containerfile expects Frappe via build args, not in apps.json\n- Only include custom/third-party apps (ERPNext, india_compliance, HRMS, etc.)\n- Use specific tags for immutability\n\n### Step 3: Build the Immutable Image

Generate unique image tag:

# Components of the tag
BUILD_DATE=$(date +%Y%m%d)           # 20251118
GIT_SHA=$(git rev-parse --short HEAD) # 4c860c6
USERNAME="duthink"  # Your GitHub username

# Full image tags
IMAGE_TAG="ghcr.io/${USERNAME}/erpnext-custom:${BUILD_DATE}-${GIT_SHA}"
IMAGE_LATEST="ghcr.io/${USERNAME}/erpnext-custom:production-latest"

echo "Will create tags:"
echo "  Specific: $IMAGE_TAG"
echo "  Latest:   $IMAGE_LATEST"

Encode apps.json:

export APPS_JSON_BASE64=$(base64 -w0 production/apps.json)

# Verify it worked
echo "Base64 encoded (first 50 chars): ${APPS_JSON_BASE64:0:50}..."

Build the image:

docker build \
  --build-arg=FRAPPE_PATH=https://github.com/frappe/frappe \
  --build-arg=FRAPPE_BRANCH=v15.88.1 \
  --build-arg=PYTHON_VERSION=3.11.6 \
  --build-arg=NODE_VERSION=18.18.2 \
  --build-arg=APPS_JSON_BASE64=$APPS_JSON_BASE64 \
  --tag=$IMAGE_TAG \
  --tag=$IMAGE_LATEST \
  --file=images/layered/Containerfile \
  .

What happens during build (10-15 minutes):

  1. Installs Frappe Framework v15.88.1 (from FRAPPE_BRANCH arg)
  2. Installs all custom apps from apps.json
  3. Installs Python dependencies
  4. Installs Node.js dependencies
  5. Runs bench build (compiles all assets!)
  6. Creates final image with everything baked in

Verify build success:

# Check images exist
docker images | grep erpnext-custom

# Expected output:
# ghcr.io/duthink/erpnext-custom  20251118-4c860c6    e3768a4d428a  1.5GB
# ghcr.io/duthink/erpnext-custom  production-latest   e3768a4d428a  1.5GB

Step 4: Push to Registry

Push both tags:

# Push specific version (immutable)
docker push ghcr.io/${USERNAME}/erpnext-custom:${BUILD_DATE}-${GIT_SHA}

# Push latest (convenience pointer)
docker push ghcr.io/${USERNAME}/erpnext-custom:production-latest

Note: The second push is instant (just updates the tag pointer, no re-upload).

Verify on GitHub:

  1. Go to: https://github.com/YOUR_USERNAME?tab=packages
  2. Find erpnext-custom
  3. Check both tags exist

Step 5: Update Production Configuration

Edit production environment:

nano production/production.env

Update these lines:

# Custom Image Configuration
CUSTOM_IMAGE=ghcr.io/duthink/erpnext-custom
CUSTOM_TAG=20251118-4c860c6  # Use your BUILD_DATE-GIT_SHA
PULL_POLICY=always

Why not use production-latest?

  • Production needs specific, immutable tags
  • production-latest moves when you push new images
  • Specific tags enable reliable rollbacks

Step 6: Deploy the New Image

Regenerate production.yaml:

./scripts/deploy.sh --regenerate

What this does:

  • Merges base compose.yaml with overlays
  • Injects your CUSTOM_IMAGE and CUSTOM_TAG
  • Generates production/production.yaml

Deploy to production:

./scripts/deploy.sh

What this does:

  1. Validates configuration
  2. Deploys Traefik (if not running)
  3. Deploys MariaDB (if not running)
  4. Pulls new image from registry
  5. Recreates containers with new image
  6. Starts all services

Verify deployment:

# Check all containers use new image
docker compose -f production/production.yaml images

# Expected output:
# CONTAINER        IMAGE                                             
# backend          ghcr.io/duthink/erpnext-custom:20251118-4c860c6
# frontend         ghcr.io/duthink/erpnext-custom:20251118-4c860c6
# queue-short      ghcr.io/duthink/erpnext-custom:20251118-4c860c6
# ...

Step 7: Activate Apps on Sites

Important: Apps are in the image, but not yet active on your sites.

Install app on existing site:

docker compose -f production/production.yaml exec backend \
  bench --site erp.localhost install-app india_compliance

Run migrations:

docker compose -f production/production.yaml exec backend \
  bench --site erp.localhost migrate

That's it! No bench build or asset sync needed. Assets are already compiled and present in all containers.

Verify it works:

# Check app is installed
docker compose -f production/production.yaml exec backend \
  bench --site erp.localhost list-apps

# Expected output:
# frappe 15.88.2
# erpnext 15.88.1
# india_compliance 15.23.2

# Test the site
curl -k -I https://erp.localhost/app/home
# Should return: HTTP/2 200

Real-World Example: India Compliance

Complete Workflow from Scratch

# 1. Check current stable version
curl -s https://api.github.com/repos/resilient-tech/india-compliance/tags | grep '"name"' | head -3
# Output: "name": "v15.23.2"

# 2. Create apps.json (custom apps only - NOT Frappe)
cat > production/apps.json <<EOF
[
  {
    "url": "https://github.com/frappe/erpnext",
    "branch": "v15.88.1"
  },
  {
    "url": "https://github.com/resilient-tech/india-compliance",
    "branch": "v15.23.2"
  }
]
EOF

# 3. Build image
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=FRAPPE_PATH=https://github.com/frappe/frappe \
  --build-arg=FRAPPE_BRANCH=v15.88.1 \
  --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 .

# Takes 10-15 minutes...

# 4. Push to registry
docker push ghcr.io/duthink/erpnext-custom:${BUILD_DATE}-${GIT_SHA}
docker push ghcr.io/duthink/erpnext-custom:production-latest

# 5. Update production config
echo "CUSTOM_TAG=${BUILD_DATE}-${GIT_SHA}" >> 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:

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:

[
  {
    "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):

# 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:

# Update production config
nano production/production.env
# CUSTOM_TAG=20251119-def5678

./scripts/deploy.sh --regenerate
./scripts/deploy.sh

5. Install on sites:

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:

[
  {
    "url": "https://YOUR_TOKEN@github.com/YOUR_ORG/private_app",
    "branch": "v1.0.0"
  }
]

Security Note: Never commit tokens to git! Use environment variables:

# 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:

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:

nano production/apps.json

# Change:
# "branch": "v15.23.2"  → "branch": "v15.24.0"

3. Rebuild with new tag:

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):

# Use production-latest for staging
nano staging/staging.env
# CUSTOM_TAG=production-latest

./scripts/deploy-staging.sh
# Test thoroughly...

5. Deploy to production:

nano production/production.env
# CUSTOM_TAG=20251125-xyz9999  # New specific tag

./scripts/deploy.sh --regenerate
./scripts/deploy.sh

6. Migrate sites:

docker compose -f production/production.yaml exec backend \
  bench --site erp.localhost migrate

7. Rollback if issues:

# 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)

# .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:

# 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:

# 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:

# 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:

# 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:

# 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:

# 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:

# Multi-stage builds help (already in images/layered/Containerfile)
# Ensure .dockerignore is present:
cat > .dockerignore <<EOF
.git
.github
node_modules
*.log
development/
docs/
tests/
EOF

Issue: Cannot Push to Registry

Symptom:

denied: permission_denied

Solutions:

# 1. Verify login
docker login ghcr.io -u YOUR_USERNAME

# 2. Check token has write:packages scope
# Go to: https://github.com/settings/tokens

# 3. Ensure repository exists or image name matches your username
# Image must be: ghcr.io/YOUR_USERNAME/image-name

Best Practices

1. Version Pinning

Always use specific tags or commits:

// ✅ GOOD
{"url": "...", "branch": "v15.23.2"}
{"url": "...", "branch": "main", "commit": "abc1234"}

// ❌ BAD
{"url": "...", "branch": "version-15"}
{"url": "...", "branch": "main"}

2. Image Tagging Strategy

Use semantic, traceable tags:

# Format: YYYYMMDD-GITSHA
20251118-4c860c6  # Date + git commit

# Why?
✅ Chronological ordering
✅ Git traceability
✅ Unique identifier
✅ Easy to find in registry

3. Keep Old Images

Don't delete old images immediately:

# Keep last 5-10 production images
# Allows rollback window of several months

Cleanup script:

# Keep only last 10 tags (manual cleanup)
# Use GitHub Packages retention policies

4. Document Image Contents

Tag your git commits when building images:

git tag release-20251118-india-compliance-v15.23.2
git push origin release-20251118-india-compliance-v15.23.2

Maintain a changelog:

## 20251118-4c860c6
- Added india_compliance v15.23.2
- Updated erpnext to v15.88.1

## 20251117-abc1234
- Initial production image
- erpnext v15.88.1

5. Test Before Production

Always test new images:

# Pull latest in staging
docker pull ghcr.io/duthink/erpnext-custom:production-latest

# Run tests
# - Create test site
# - Install apps
# - Run migrations
# - Test critical workflows

# Only promote to production after validation

6. Automate Builds

Use CI/CD for consistency:

  • Automatic builds on apps.json changes
  • Automatic tagging with git SHA
  • Automatic push to registry
  • Manual approval for production deployment

7. Monitor Image Registry

Set up alerts:

  • Failed builds
  • Image size growing unexpectedly
  • Old images not being cleaned up

8. Security Scanning

Scan images before production:

# Using Trivy (example)
trivy image ghcr.io/duthink/erpnext-custom:20251118-4c860c6

# Fix critical vulnerabilities before deploying

Summary

Key Takeaways

  1. Pin versions in apps.json for true immutability
  2. Build once, deploy many - assets pre-compiled
  3. Tag with date+sha for traceability
  4. Push to registry for reliable distribution
  5. Use specific tags in production (not latest)
  6. Keep old images for rollback capability
  7. Test in staging before production
  8. Automate via CI/CD for consistency

Workflow Checklist

  • Update apps.json with pinned versions
  • Build image with unique tag
  • Push both specific and latest tags
  • Test in staging environment
  • Update CUSTOM_TAG in production.env
  • Regenerate production.yaml
  • Deploy to production
  • Migrate sites
  • Monitor and verify
  • Document in changelog

Next Steps

  • Set up CI/CD pipeline for automated builds
  • Implement staging environment for testing
  • Configure monitoring and alerts
  • Document rollback procedures
  • Train team on workflow

Additional Resources


Last Updated: November 2025
Maintainer: This repository
Pattern: Pattern 2 (Gold Standard - Immutable Images)