diff --git a/CLAUDE.md b/CLAUDE.md index ffaa7b0..ac2389b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,7 +15,7 @@ ### Current stack -- **Grav:** 2.0.4 stable (baked into the custom Docker image via `Dockerfile`; server upgrades in place via `bin/gpm self-upgrade`) +- **Grav:** 2.0.7 stable (baked into the custom Docker image via `Dockerfile`; server upgrades in place via `bin/gpm self-upgrade`) - **Admin:** Admin2 v2.0.10 (plugin slug: `admin2`, NOT `admin`) - **GPM channel:** `stable` — set in `user/config/system.yaml` → `gpm.releases` (authoritative). `GRAV_CHANNEL=production` in `docker-compose.yml` is cosmetic/consistency only - **Plugin management:** `admin2`, `api`, and `flex-objects` are now **GPM-managed via `plugins.txt`** (installed by `make install-plugins`), no longer hand-extracted from the core bundle. `git-sync` stays **remote-only** — never in `plugins.txt` diff --git a/Makefile b/Makefile index 370ed30..9998605 100644 --- a/Makefile +++ b/Makefile @@ -90,7 +90,10 @@ build: docker compose build build-assets: - docker run --rm \ + # --user: outputs (node_modules, js/ bundles, css-compiled/) land in the + # tracked theme tree owned by the host user, not root. HOME=/tmp gives npm + # a writable cache when running as a non-root uid. + docker run --rm --user $(HOST_UID):$(HOST_GID) -e HOME=/tmp \ -v $(PWD)/user/themes/intotheeast:/app \ -w /app node:20-alpine \ sh -c "npm install && npm run build" diff --git a/docs/solutions/integration-issues/docker-exec-root-owned-bind-mount-files.md b/docs/solutions/integration-issues/docker-exec-root-owned-bind-mount-files.md index add838b..b22e0df 100644 --- a/docs/solutions/integration-issues/docker-exec-root-owned-bind-mount-files.md +++ b/docs/solutions/integration-issues/docker-exec-root-owned-bind-mount-files.md @@ -102,11 +102,11 @@ install-plugins: $(MAKE) apply-plugin-patches ``` -### The `build-assets` vector (same principle, `docker run`) — still open +### The `build-assets` vector (same principle, `docker run`) — fixed 2026-07-08 -The 2026-07-08 fix (`209b804`) hardened `install-plugins` only. `build-assets` remains a root-writing target and surfaced later: `git worktree remove` aborted with `Permission denied` on root-owned esbuild bundles under `user/themes/intotheeast/js/post/`, and earlier a `build-assets` run had produced a root-owned `css-compiled/` dir that blocked a `git merge` on the main checkout. (session history) +The first 2026-07-08 fix (`209b804`) hardened `install-plugins` only. `build-assets` remained a root-writing target and surfaced later: `git worktree remove` aborted with `Permission denied` on root-owned esbuild bundles under `user/themes/intotheeast/js/post/`, and earlier a `build-assets` run had produced a root-owned `css-compiled/` dir that blocked a `git merge` on the main checkout. (session history) -Apply the same drop-privileges principle — with `--user` on `docker run`: +The same drop-privileges principle applies — with `--user` on `docker run` (fixed later the same day): ```makefile # Before — writes root-owned node_modules + bundles into the tracked theme tree @@ -116,15 +116,16 @@ build-assets: -w /app node:20-alpine \ sh -c "npm install && npm run build" -# After — outputs owned by the host user (uid 1000) +# After — outputs owned by the host user; HOME=/tmp gives npm a writable +# cache when running as a non-root uid build-assets: - docker run --rm --user $(HOST_UID):$(HOST_GID) \ + docker run --rm --user $(HOST_UID):$(HOST_GID) -e HOME=/tmp \ -v $(PWD)/user/themes/intotheeast:/app \ -w /app node:20-alpine \ sh -c "npm install && npm run build" ``` -Caveat: run as a non-root uid, npm needs a writable `$HOME`/cache. If the build errors on a read-only home dir, add `-e HOME=/tmp` (or `-e npm_config_cache=/tmp/.npm`). Recovery for the existing root-owned output is the same as anywhere else — `chown -R $(HOST_UID):$(HOST_GID)` from a container that already has root, then `rm`. +Verified: `make build-assets` with the fix completes clean (esbuild bundles emitted), `find user/themes/intotheeast -uid 0` counts zero, and the output bundles are byte-identical to the previously committed ones. Recovery for any pre-existing root-owned output is the same as anywhere else — `chown -R $(HOST_UID):$(HOST_GID)` from a container that already has root, then `rm`. ## Why This Works @@ -138,7 +139,7 @@ The preliminary chown of `cache/` and `tmp/` is container-internal: those paths The reusable principle, worth internalizing beyond this one repo: -- **Any make/CI target that writes files into a host bind mount must drop privileges — whether it uses `docker exec` (`-u $(HOST_UID):$(HOST_GID)`) or `docker run` (`--user $(HOST_UID):$(HOST_GID)`).** A container booting as root does *not* mean the commands you run in it must write as root. `build-assets` (a `docker run`) is the easy one to miss, because the original fix only patched the `docker exec` targets — so audit `docker run` invocations too, not just `docker exec`. +- **Any make/CI target that writes files into a host bind mount must drop privileges — whether it uses `docker exec` (`-u $(HOST_UID):$(HOST_GID)`) or `docker run` (`--user $(HOST_UID):$(HOST_GID)`).** A container booting as root does *not* mean the commands you run in it must write as root. `build-assets` (a `docker run`) was the easy one to miss, because the original fix only patched the `docker exec` targets — so audit `docker run` invocations too, not just `docker exec`. - **Derive host identity once in the Makefile and reuse it:** `HOST_UID := $(shell id -u)` / `HOST_GID := $(shell id -g)`. - **Don't rely on `APACHE_RUN_USER` or compose-level `UID`/`GID` env vars to fix exec ownership** — they don't apply to `docker exec`. `APACHE_RUN_USER` only affects Apache workers; compose `user:`/env vars only affect services wired to consume them. - **You can't just add `user:` to a service whose entrypoint needs root** (to bind privileged ports, set up cron, etc.). Drop privileges per-exec instead of per-container.