From ae275df16173dd2778146fc241baef7cb9ffabae Mon Sep 17 00:00:00 2001 From: OmarElaraby26 Date: Sun, 5 Apr 2026 22:24:53 +0200 Subject: [PATCH 1/3] fix(security): replace APPS_JSON_BASE64 build-arg with BuildKit secret mount APPS_JSON_BASE64 is stored in image layer metadata, permanently exposing private repo tokens (GitHub PATs) to anyone with image pull access. Replace --build-arg with --mount=type=secret so that apps.json is only available during the RUN step and never committed to any layer. Refs: https://docs.docker.com/reference/build-checks/secrets-used-in-arg-or-env/ --- docs/02-setup/02-build-setup.md | 16 ++++++---------- .../08-single-server-nginxproxy-example.md | 14 +++----------- images/custom/Containerfile | 13 ++++++------- images/layered/Containerfile | 14 ++++++-------- 4 files changed, 21 insertions(+), 36 deletions(-) diff --git a/docs/02-setup/02-build-setup.md b/docs/02-setup/02-build-setup.md index 93331280..1ffa35d9 100644 --- a/docs/02-setup/02-build-setup.md +++ b/docs/02-setup/02-build-setup.md @@ -42,23 +42,19 @@ To include custom apps in your image, create an `apps.json` file in the reposito ] ``` -Then generate a base64-encoded string from this file: - -```bash -export APPS_JSON_BASE64=$(base64 -w 0 apps.json) -``` - # Build the image Choose the appropriate build command based on your container runtime and desired image type. This example builds the `layered` image with the custom `apps.json` you created. +> **Security note:** The `apps.json` file is passed as a [BuildKit secret](https://docs.docker.com/build/building/secrets/) so that private repository tokens are **never** stored in image layer metadata. Do not use `--build-arg` for `apps.json` — build arguments are permanently visible via `docker image history`. + `Docker`: ```bash -docker build \ +DOCKER_BUILDKIT=1 docker build \ --build-arg=FRAPPE_PATH=https://github.com/frappe/frappe \ --build-arg=FRAPPE_BRANCH=version-15 \ - --build-arg=APPS_JSON_BASE64=$APPS_JSON_BASE64 \ + --secret=id=apps_json,src=apps.json \ --tag=custom:15 \ --file=images/layered/Containerfile . ``` @@ -69,7 +65,7 @@ docker build \ podman build \ --build-arg=FRAPPE_PATH=https://github.com/frappe/frappe \ --build-arg=FRAPPE_BRANCH=version-15 \ - --build-arg=APPS_JSON_BASE64=$APPS_JSON_BASE64 \ + --secret=id=apps_json,src=apps.json \ --tag=custom:15 \ --file=images/layered/Containerfile . ``` @@ -82,7 +78,7 @@ podman build \ | FRAPPE_PATH | Repository URL for Frappe framework source code. Defaults to https://github.com/frappe/frappe | | FRAPPE_BRANCH | Branch to use for Frappe framework. Defaults to version-15 | | **Custom Apps** | | -| APPS_JSON_BASE64 | Base64-encoded JSON string from apps.json defining apps to install | +| (secret) apps_json | Passed via `--secret=id=apps_json,src=apps.json`. Never use `--build-arg` for this file. | | **Dependencies** | | | PYTHON_VERSION | Python version for the base image | | NODE_VERSION | Node.js version | diff --git a/docs/02-setup/08-single-server-nginxproxy-example.md b/docs/02-setup/08-single-server-nginxproxy-example.md index 0049e39f..2cef110d 100644 --- a/docs/02-setup/08-single-server-nginxproxy-example.md +++ b/docs/02-setup/08-single-server-nginxproxy-example.md @@ -84,25 +84,17 @@ cat > ~/gitops/apps.json <<'EOF' EOF ``` -Generate the BASE64 value and build: +Build the image, passing `apps.json` as a [BuildKit secret](https://docs.docker.com/build/building/secrets/) so that private repo tokens are never stored in image layers: ```shell -export APPS_JSON_BASE64=$(base64 -w 0 ~/gitops/apps.json) - -docker build \ +DOCKER_BUILDKIT=1 docker build \ --build-arg=FRAPPE_PATH=https://github.com/frappe/frappe \ --build-arg=FRAPPE_BRANCH=version-16 \ - --build-arg=APPS_JSON_BASE64=$APPS_JSON_BASE64 \ + --secret=id=apps_json,src=$HOME/gitops/apps.json \ --tag=my-erpnext-prod-image:16.0.0 \ --file=images/layered/Containerfile . ``` -If `base64 -w 0` is not available on your system, use: - -```shell -export APPS_JSON_BASE64=$(base64 ~/gitops/apps.json | tr -d '\n') -``` - ### Configure environment Create an environment file for the bench: diff --git a/images/custom/Containerfile b/images/custom/Containerfile index a8298b52..8a4a58b2 100644 --- a/images/custom/Containerfile +++ b/images/custom/Containerfile @@ -113,18 +113,17 @@ RUN apt-get update \ libbz2-dev \ && rm -rf /var/lib/apt/lists/* -# apps.json includes -ARG APPS_JSON_BASE64 -RUN if [ -n "${APPS_JSON_BASE64}" ]; then \ - mkdir /opt/frappe && echo "${APPS_JSON_BASE64}" | base64 -d > /opt/frappe/apps.json; \ - fi +# apps.json is passed as a BuildKit secret so that private repo tokens +# are never baked into any image layer. The secret is mounted only for +# this RUN step and is not present in the final image. USER frappe ARG FRAPPE_BRANCH=version-16 ARG FRAPPE_PATH=https://github.com/frappe/frappe -RUN export APP_INSTALL_ARGS="" && \ - if [ -n "${APPS_JSON_BASE64}" ]; then \ +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}\ diff --git a/images/layered/Containerfile b/images/layered/Containerfile index 142c487a..117ab1ab 100644 --- a/images/layered/Containerfile +++ b/images/layered/Containerfile @@ -4,18 +4,16 @@ FROM frappe/build:${FRAPPE_BRANCH} AS builder ARG FRAPPE_BRANCH=version-16 ARG FRAPPE_PATH=https://github.com/frappe/frappe -ARG APPS_JSON_BASE64 -USER root - -RUN if [ -n "${APPS_JSON_BASE64}" ]; then \ - mkdir /opt/frappe && echo "${APPS_JSON_BASE64}" | base64 -d > /opt/frappe/apps.json; \ - fi +# apps.json is passed as a BuildKit secret so that private repo tokens +# are never baked into any image layer. The secret is mounted only for +# this RUN step and is not present in the final image. USER frappe -RUN export APP_INSTALL_ARGS="" && \ - if [ -n "${APPS_JSON_BASE64}" ]; then \ +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}\ From 8892908f5d3041d4c9383702265d88b4c8a08687 Mon Sep 17 00:00:00 2001 From: OmarElaraby26 Date: Tue, 7 Apr 2026 20:12:29 +0200 Subject: [PATCH 2/3] docs: require Docker Engine v23+ instead of setting DOCKER_BUILDKIT=1 BuildKit has been the default builder since Docker Engine 23.0 (Feb 2023), so prefixing the example build commands with DOCKER_BUILDKIT=1 is redundant on any supported install. Replace the prefix with an explicit prerequisite note so the requirement lives with the user's environment, not the example. The build relies on BuildKit secret mounts (--secret) to keep apps.json tokens out of image layers, which is why a real BuildKit-default engine is mandatory rather than merely recommended. Addresses review feedback on PR #1861. --- docs/02-setup/02-build-setup.md | 8 +++++--- docs/02-setup/08-single-server-nginxproxy-example.md | 6 +++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/02-setup/02-build-setup.md b/docs/02-setup/02-build-setup.md index 1ffa35d9..d411cab7 100644 --- a/docs/02-setup/02-build-setup.md +++ b/docs/02-setup/02-build-setup.md @@ -7,11 +7,13 @@ This guide walks you through building Frappe images from the repository resource # Prerequisites - git -- docker or podman +- docker (Engine **v23.0+**) or podman - docker compose v2 or podman compose > Install containerization software according to the official maintainer documentation. Avoid package managers when not recommended, as they frequently cause compatibility issues. +> **Why Docker Engine v23+?** The build uses [BuildKit secrets](https://docs.docker.com/build/building/secrets/) (`--secret`) to keep `apps.json` tokens out of image layers. BuildKit is the default builder starting with Docker Engine 23.0 — older releases will fail or silently fall back to the legacy builder, which does not support secret mounts. + # Clone this repo ```bash @@ -46,12 +48,12 @@ To include custom apps in your image, create an `apps.json` file in the reposito Choose the appropriate build command based on your container runtime and desired image type. This example builds the `layered` image with the custom `apps.json` you created. -> **Security note:** The `apps.json` file is passed as a [BuildKit secret](https://docs.docker.com/build/building/secrets/) so that private repository tokens are **never** stored in image layer metadata. Do not use `--build-arg` for `apps.json` — build arguments are permanently visible via `docker image history`. +> **Security note:** The `apps.json` file is passed as a [BuildKit secret](https://docs.docker.com/build/building/secrets/) so that private repository tokens are **never** stored in image layer metadata. Do not use `--build-arg` for `apps.json` — build arguments are permanently visible via `docker image history`. This requires **Docker Engine v23.0+** (where BuildKit is the default builder). `Docker`: ```bash -DOCKER_BUILDKIT=1 docker build \ +docker build \ --build-arg=FRAPPE_PATH=https://github.com/frappe/frappe \ --build-arg=FRAPPE_BRANCH=version-15 \ --secret=id=apps_json,src=apps.json \ diff --git a/docs/02-setup/08-single-server-nginxproxy-example.md b/docs/02-setup/08-single-server-nginxproxy-example.md index 2cef110d..4f1c212f 100644 --- a/docs/02-setup/08-single-server-nginxproxy-example.md +++ b/docs/02-setup/08-single-server-nginxproxy-example.md @@ -15,7 +15,7 @@ We will setup the following: ## Requirements -- A server that can run Docker (recommended: 2 vCPU, 4 GB RAM, 50 GB SSD). +- A server that can run Docker Engine **v23.0+** (recommended: 2 vCPU, 4 GB RAM, 50 GB SSD). The custom-image build below uses [BuildKit secrets](https://docs.docker.com/build/building/secrets/), which require BuildKit as the default builder (Docker Engine 23.0+). - A public domain with DNS control. - Two subdomains pointing to your server IP (A/AAAA records): - `erp.your-domain.com` @@ -84,10 +84,10 @@ cat > ~/gitops/apps.json <<'EOF' EOF ``` -Build the image, passing `apps.json` as a [BuildKit secret](https://docs.docker.com/build/building/secrets/) so that private repo tokens are never stored in image layers: +Build the image, passing `apps.json` as a [BuildKit secret](https://docs.docker.com/build/building/secrets/) so that private repo tokens are never stored in image layers. This requires **Docker Engine v23.0+**, where BuildKit is the default builder: ```shell -DOCKER_BUILDKIT=1 docker build \ +docker build \ --build-arg=FRAPPE_PATH=https://github.com/frappe/frappe \ --build-arg=FRAPPE_BRANCH=version-16 \ --secret=id=apps_json,src=$HOME/gitops/apps.json \ From 4e5f84fa29a1e0bd1cbed3a57e36f5766421260c Mon Sep 17 00:00:00 2001 From: RocketQuack <202538874+Rocket-Quack@users.noreply.github.com> Date: Wed, 15 Apr 2026 13:52:31 +0200 Subject: [PATCH 3/3] chore: remove comments about why BuildKit is being used to parse apps.json --- images/custom/Containerfile | 4 ---- images/layered/Containerfile | 4 ---- 2 files changed, 8 deletions(-) diff --git a/images/custom/Containerfile b/images/custom/Containerfile index 8a4a58b2..c7519e84 100644 --- a/images/custom/Containerfile +++ b/images/custom/Containerfile @@ -113,10 +113,6 @@ RUN apt-get update \ libbz2-dev \ && rm -rf /var/lib/apt/lists/* -# apps.json is passed as a BuildKit secret so that private repo tokens -# are never baked into any image layer. The secret is mounted only for -# this RUN step and is not present in the final image. - USER frappe ARG FRAPPE_BRANCH=version-16 diff --git a/images/layered/Containerfile b/images/layered/Containerfile index 117ab1ab..de5e835d 100644 --- a/images/layered/Containerfile +++ b/images/layered/Containerfile @@ -5,10 +5,6 @@ FROM frappe/build:${FRAPPE_BRANCH} AS builder ARG FRAPPE_BRANCH=version-16 ARG FRAPPE_PATH=https://github.com/frappe/frappe -# apps.json is passed as a BuildKit secret so that private repo tokens -# are never baked into any image layer. The secret is mounted only for -# this RUN step and is not present in the final image. - USER frappe RUN --mount=type=secret,id=apps_json,target=/opt/frappe/apps.json,uid=1000,gid=1000 \