Frappe Containerfile Generator
Generate Containerfiles for Frappe applications using the official docker.io/frappe/erpnext base images.
IMPORTANT: Frappe apps are NOT normal Python apps. They run inside the Frappe bench directory structure (/home/frappe/frappe-bench) and require specific directory layout, user permissions, and the Gunicorn entry point frappe.app:application.
When to Use This Skill
Use this skill when:
- Creating a Containerfile for any Frappe/ERPNext custom app
- Containerising a Frappe app for production deployment
- A DevOps agent needs to produce a Containerfile for a Frappe-based tech stack
Recommended Containerfile Template
This is the standard pattern for all Frappe apps. Replace <app-name> with the actual app name (snake_case, matching the app's Python package name).
FROM docker.io/frappe/erpnext:version-15
USER root
WORKDIR /home/frappe/frappe-bench
# Create asset directory and fix permissions
RUN mkdir -p sites/assets/<app-name> && \
chown -R frappe:frappe sites/assets
USER frappe
WORKDIR /home/frappe/frappe-bench
# Copy application files
COPY --chown=frappe:frappe . ./apps/<app-name>/
# Install app, register in apps.txt, verify asset directory
RUN env/bin/python3 -m pip install -e ./apps/<app-name> && \
export BENCH_DEVELOPER=1 && \
ls -1 ./apps > sites/apps.txt && \
ls -l /home/frappe/frappe-bench/sites/assets/<app-name> && \
cat sites/apps.txt
CMD [ \
"/home/frappe/frappe-bench/env/bin/gunicorn", \
"--chdir=/home/frappe/frappe-bench/sites", \
"--bind=0.0.0.0:8000", \
"--threads=4", \
"--workers=2", \
"--worker-class=gthread", \
"--worker-tmp-dir=/dev/shm", \
"--timeout=120", \
"--preload", \
"frappe.app:application" \
]
Concrete Example — app named invoicing
FROM docker.io/frappe/erpnext:version-15
USER root
WORKDIR /home/frappe/frappe-bench
RUN mkdir -p sites/assets/invoicing && \
chown -R frappe:frappe sites/assets
USER frappe
WORKDIR /home/frappe/frappe-bench
COPY --chown=frappe:frappe . ./apps/invoicing/
RUN env/bin/python3 -m pip install -e ./apps/invoicing && \
export BENCH_DEVELOPER=1 && \
ls -1 ./apps > sites/apps.txt && \
ls -l /home/frappe/frappe-bench/sites/assets/invoicing && \
cat sites/apps.txt
CMD [ \
"/home/frappe/frappe-bench/env/bin/gunicorn", \
"--chdir=/home/frappe/frappe-bench/sites", \
"--bind=0.0.0.0:8000", \
"--threads=4", \
"--workers=2", \
"--worker-class=gthread", \
"--worker-tmp-dir=/dev/shm", \
"--timeout=120", \
"--preload", \
"frappe.app:application" \
]
App Name Substitution Rules (Mandatory)
- Replace
<app-name>everywhere with the real app slug (snake_case) - Keep the copy target and pip path in sync:
COPY --chown=frappe:frappe . ./apps/<app-name>/pip install -e ./apps/<app-name>
- Ensure asset path matches the same app slug:
sites/assets/<app-name>
Example for qrcode:
RUN mkdir -p sites/assets/qrcode && \
chown -R frappe:frappe sites/assets
COPY --chown=frappe:frappe . ./apps/qrcode/
RUN env/bin/python3 -m pip install -e ./apps/qrcode && \
export BENCH_DEVELOPER=1 && \
ls -1 ./apps > sites/apps.txt && \
ls -l /home/frappe/frappe-bench/sites/assets/qrcode && \
cat sites/apps.txt
Version Selection
Use the version that matches the target Frappe/ERPNext deployment:
| Version | Base image tag | Notes |
|---------|---------------|-------|
| 15 | version-15 | Default for new apps — stable, widely deployed |
| 16 | version-16 | Latest, use when targeting v16 sites |
| 14 | version-14 | Legacy, only for existing v14 deployments |
FROM docker.io/frappe/erpnext:version-15
FROM docker.io/frappe/erpnext:version-16
FROM docker.io/frappe/erpnext:version-14
Multi-App Containerfile
When packaging multiple custom apps into one image:
FROM docker.io/frappe/erpnext:version-15
USER root
WORKDIR /home/frappe/frappe-bench
RUN mkdir -p sites/assets/app_one sites/assets/app_two && \
chown -R frappe:frappe sites/assets
USER frappe
WORKDIR /home/frappe/frappe-bench
COPY --chown=frappe:frappe ./app_one ./apps/app_one/
COPY --chown=frappe:frappe ./app_two ./apps/app_two/
RUN env/bin/python3 -m pip install -e ./apps/app_one && \
env/bin/python3 -m pip install -e ./apps/app_two && \
export BENCH_DEVELOPER=1 && \
ls -1 ./apps > sites/apps.txt && \
cat sites/apps.txt
CMD [ \
"/home/frappe/frappe-bench/env/bin/gunicorn", \
"--chdir=/home/frappe/frappe-bench/sites", \
"--bind=0.0.0.0:8000", \
"--threads=4", \
"--workers=2", \
"--worker-class=gthread", \
"--worker-tmp-dir=/dev/shm", \
"--timeout=120", \
"--preload", \
"frappe.app:application" \
]
Critical Rules
- Base image: Always use
docker.io/frappe/erpnext:version-{VER}— NEVER use generic UBI, Python, or Debian images - Directory layout: Apps live under
/home/frappe/frappe-bench/apps/<app-name>/ - Asset directory: Must create
sites/assets/<app-name>and chown tofrappe:frappe - User context: Start as
rootfor mkdir/chown, switch tofrappebefore COPY and RUN - apps.txt: Must regenerate
sites/apps.txtafter installing the app (ls -1 ./apps > sites/apps.txt) - pip install: Use
env/bin/python3 -m pip install -e(the bench virtualenv) - Entry point: Gunicorn with
frappe.app:application— NOTflask run, NOTbench start - Port: Bind to
0.0.0.0:8000(Frappe's standard port) - COPY ownership: Always use
--chown=frappe:frappe - No EXPOSE needed: Port 8000 is implicit from the Gunicorn bind
Build and Run
podman build -t my-frappe-app .
podman run -p 8000:8000 my-frappe-app
Best Practices
- Volume mounts for dev: don't bake app source into the image during development — use compose.yml with volume mounts instead
- Layer optimization: combine RUN commands to reduce image layers
- Permissions: always set
--chown=frappe:frappewhen copying files - Multi-arch: build for both amd64 and arm64 if distributing images
- Version pinning: pin specific version tags, never use
latest
Integration with frappe-new-app
When generating a new app, automatically include:
Containerfileusing the template above- Place
Containerfilein the app root directory - Default to
version-15unless user specifies a version