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
- Overview
- Prerequisites
- Quick Start
- Step-by-Step Guide
- Real-World Example: India Compliance
- Adding Custom Apps
- Updating Apps
- Uninstall Apps
- Deployment Workflow
- Troubleshooting
- Best Practices
Overview
What This Achieves
- ✅ True Immutability: Apps frozen at specific versions (tags/commits)
- ✅ Zero Runtime Builds: No
bench buildneeded 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
-
Create Personal Access Token:
- Go to: https://github.com/settings/tokens/new
- Select scope:
write:packages - Generate token and save it securely
-
Login to GitHub Container Registry:
export GITHUB_TOKEN=your_token_here echo $GITHUB_TOKEN | docker login ghcr.io -u YOUR_USERNAME --password-stdin -
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):
- Installs Frappe Framework v15.88.1 (from FRAPPE_BRANCH arg)
- Installs all custom apps from
apps.json - Installs Python dependencies
- Installs Node.js dependencies
- Runs
bench build(compiles all assets!) - 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:
- Go to: https://github.com/YOUR_USERNAME?tab=packages
- Find
erpnext-custom - 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-latestmoves 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.yamlwith overlays - Injects your
CUSTOM_IMAGEandCUSTOM_TAG - Generates
production/production.yaml
Deploy to production:
./scripts/deploy.sh
What this does:
- Validates configuration
- Deploys Traefik (if not running)
- Deploys MariaDB (if not running)
- Pulls new image from registry
- Recreates containers with new image
- 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 buildor 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.jsonchanges - 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
- Pin versions in
apps.jsonfor true immutability - Build once, deploy many - assets pre-compiled
- Tag with date+sha for traceability
- Push to registry for reliable distribution
- Use specific tags in production (not
latest) - Keep old images for rollback capability
- Test in staging before production
- Automate via CI/CD for consistency
Workflow Checklist
- Update
apps.jsonwith pinned versions - Build image with unique tag
- Push both specific and latest tags
- Test in staging environment
- Update
CUSTOM_TAGin 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)