Context
When packaging custom Frappe apps in a Docker image, bench get-app is awkward to use because it requires network access at build time and isn't idempotent for purely local code. The natural alternative — and what every "custom app in Docker" tutorial converges on — is:
COPY apps/oysi_compliance /home/frappe/frappe-bench/apps/oysi_compliance
RUN /home/frappe/frappe-bench/env/bin/pip install -e /home/frappe/frappe-bench/apps/oysi_compliance
This installs the Python package correctly. The app is importable, hooks fire, DocTypes get created on bench migrate, and bench list-apps shows it.
But the sites/assets/<app> symlink is silently missing, and the asset pipeline relies on it for serving static files (<app>/<app>/public/). bench get-app creates this symlink as part of its install flow; pip install -e does not.
Symptom
Custom JS/CSS bundles in <app>/<app>/public/css/... and <app>/<app>/public/js/... return 404 from the frontend. From the user's perspective the app is "installed" — only the assets are silently broken.
Reproduction
- Base image:
frappe/erpnext:v16
- Add a custom app via
COPY + pip install -e (as above), with a <app>/<app>/public/css/<app>.bundle.css referenced via app_include_css hook.
- Build and start the bench, run
bench migrate, request the desk in a browser.
- Network tab shows 404 for
assets/<app>/css/<app>.bundle.css.
- Inspect:
ls -la /home/frappe/frappe-bench/sites/assets/ — missing <app> symlink.
- Manual fix:
ln -s ../../apps/<app>/<app>/public sites/assets/<app>. Assets load immediately.
We hit this with three local custom apps in a row (oysi_compliance, ecommerce_integrations, oysi_branding); each one cost a debug round before the pattern clicked.
Suggested fix
In order of increasing scope:
- Doc note in the "Custom Apps in Docker" section: explicitly call out that
pip install -e skips the asset symlink, and provide the one-line ln -s fix or recommend a wrapper.
bench symlink-app-assets <app> subcommand — idempotent, creates the symlink if missing. Easy to drop into a Dockerfile (RUN bench symlink-app-assets oysi_compliance).
- Refactor
bench get-app so the symlink creation lives in its own internal function that can be invoked independently from the network-fetch path.
(2) feels like the sweet spot — small surface, fixes the docker-packaging case cleanly without touching get-app semantics.
Environment
frappe/erpnext:v16 base image (Frappe 16.13, ERPNext 16.12)
- Three local custom apps installed via
COPY + pip install -e
- Symptom and fix verified on production deployment
Context
When packaging custom Frappe apps in a Docker image,
bench get-appis awkward to use because it requires network access at build time and isn't idempotent for purely local code. The natural alternative — and what every "custom app in Docker" tutorial converges on — is:This installs the Python package correctly. The app is importable, hooks fire, DocTypes get created on
bench migrate, andbench list-appsshows it.But the
sites/assets/<app>symlink is silently missing, and the asset pipeline relies on it for serving static files (<app>/<app>/public/).bench get-appcreates this symlink as part of its install flow;pip install -edoes not.Symptom
Custom JS/CSS bundles in
<app>/<app>/public/css/...and<app>/<app>/public/js/...return 404 from the frontend. From the user's perspective the app is "installed" — only the assets are silently broken.Reproduction
frappe/erpnext:v16COPY+pip install -e(as above), with a<app>/<app>/public/css/<app>.bundle.cssreferenced viaapp_include_csshook.bench migrate, request the desk in a browser.assets/<app>/css/<app>.bundle.css.ls -la /home/frappe/frappe-bench/sites/assets/— missing<app>symlink.ln -s ../../apps/<app>/<app>/public sites/assets/<app>. Assets load immediately.We hit this with three local custom apps in a row (
oysi_compliance,ecommerce_integrations,oysi_branding); each one cost a debug round before the pattern clicked.Suggested fix
In order of increasing scope:
pip install -eskips the asset symlink, and provide the one-lineln -sfix or recommend a wrapper.bench symlink-app-assets <app>subcommand — idempotent, creates the symlink if missing. Easy to drop into a Dockerfile (RUN bench symlink-app-assets oysi_compliance).bench get-appso the symlink creation lives in its own internal function that can be invoked independently from the network-fetch path.(2) feels like the sweet spot — small surface, fixes the docker-packaging case cleanly without touching
get-appsemantics.Environment
frappe/erpnext:v16base image (Frappe 16.13, ERPNext 16.12)COPY+pip install -e