Labels
security, bug, docker, docs
Description of the issue
When building custom images with private apps, apps.json contains GitHub PATs embedded in repository URLs. The current Containerfiles (images/custom/Containerfile and images/layered/Containerfile) pass this as a Docker build argument (--build-arg=APPS_JSON_BASE64). Docker build arguments are permanently recorded in image layer metadata, meaning anyone with access to the image can trivially extract private repo tokens.
This is a known Docker anti-pattern documented by Docker themselves: SecretsUsedInArgOrEnv.
Impact: Any image built with private apps using the documented workflow has credentials permanently embedded. Images pushed to registries (even private ones) expose PATs to all users with pull access.
Affected files:
images/custom/Containerfile
images/layered/Containerfile
docs/02-setup/02-build-setup.md
docs/02-setup/08-single-server-nginxproxy-example.md
Context information (for bug reports)
- frappe_docker: main branch (latest as of 2026-04-05)
- Docker Engine: any version with BuildKit support
- Host OS: any (issue is in Containerfile / docs, not host-specific)
Steps to reproduce the issue
- Create
apps.json with a private repo URL containing a PAT:
[
{
"url": "https://USER:ghp_XXXX@github.com/org/private-app.git",
"branch": "main"
}
]
- Build the custom image following the current docs:
docker build \
--build-arg=APPS_JSON_BASE64=$(base64 -w 0 apps.json) \
--build-arg=FRAPPE_PATH=https://github.com/frappe/frappe \
--build-arg=FRAPPE_BRANCH=version-16 \
--tag=custom:latest \
--file=images/custom/Containerfile .
- Inspect the image:
docker image history --no-trunc custom:latest
Observed result
The full base64-encoded apps.json (including PATs) is visible in the ARG layer metadata. Decoding it reveals the plain-text GitHub PAT:
echo "<base64_from_history>" | base64 -d
# {"url": "https://USER:ghp_XXXX@github.com/org/private-app.git", ...}
Expected result
Private repository tokens should never be persisted in image layers or metadata. The image should be safe to push to registries or share with team members without leaking credentials.
Suggested fix
Replace ARG / --build-arg with a Docker BuildKit secret mount. I have a PR ready for this.
Containerfile (remove ARG APPS_JSON_BASE64 and the decode RUN, replace with):
RUN --mount=type=secret,id=apps_json,target=/opt/frappe/apps.json,uid=1000,gid=1000 \
export APP_INSTALL_ARGS="" && \
if [ -f /opt/frappe/apps.json ] && [ -s /opt/frappe/apps.json ]; then \
export APP_INSTALL_ARGS="--apps_path=/opt/frappe/apps.json"; \
fi && \
bench init ${APP_INSTALL_ARGS} ...
Build command:
DOCKER_BUILDKIT=1 docker build \
--secret=id=apps_json,src=apps.json \
--build-arg=FRAPPE_BRANCH=version-16 \
--tag=custom:latest \
--file=images/custom/Containerfile .
Key points:
--mount=type=secret makes the file available only during that RUN step -- it is never committed to any layer
uid=1000,gid=1000 ensures the frappe user can read the secret
- The
-f and -s checks handle the case where no apps.json is provided (backward compatible)
- Requires
DOCKER_BUILDKIT=1 or docker buildx build
References
Labels
security,bug,docker,docsDescription of the issue
When building custom images with private apps,
apps.jsoncontains GitHub PATs embedded in repository URLs. The current Containerfiles (images/custom/Containerfileandimages/layered/Containerfile) pass this as a Docker build argument (--build-arg=APPS_JSON_BASE64). Docker build arguments are permanently recorded in image layer metadata, meaning anyone with access to the image can trivially extract private repo tokens.This is a known Docker anti-pattern documented by Docker themselves: SecretsUsedInArgOrEnv.
Impact: Any image built with private apps using the documented workflow has credentials permanently embedded. Images pushed to registries (even private ones) expose PATs to all users with pull access.
Affected files:
images/custom/Containerfileimages/layered/Containerfiledocs/02-setup/02-build-setup.mddocs/02-setup/08-single-server-nginxproxy-example.mdContext information (for bug reports)
Steps to reproduce the issue
apps.jsonwith a private repo URL containing a PAT:[ { "url": "https://USER:ghp_XXXX@github.com/org/private-app.git", "branch": "main" } ]docker image history --no-trunc custom:latestObserved result
The full base64-encoded
apps.json(including PATs) is visible in theARGlayer metadata. Decoding it reveals the plain-text GitHub PAT:Expected result
Private repository tokens should never be persisted in image layers or metadata. The image should be safe to push to registries or share with team members without leaking credentials.
Suggested fix
Replace
ARG/--build-argwith a Docker BuildKit secret mount. I have a PR ready for this.Containerfile (remove
ARG APPS_JSON_BASE64and the decodeRUN, replace with):Build command:
DOCKER_BUILDKIT=1 docker build \ --secret=id=apps_json,src=apps.json \ --build-arg=FRAPPE_BRANCH=version-16 \ --tag=custom:latest \ --file=images/custom/Containerfile .Key points:
--mount=type=secretmakes the file available only during that RUN step -- it is never committed to any layeruid=1000,gid=1000ensures the frappe user can read the secret-fand-schecks handle the case where noapps.jsonis provided (backward compatible)DOCKER_BUILDKIT=1ordocker buildx buildReferences