From 0f6b1e69cd82f169a48a1d414459e96d8d9a4ca1 Mon Sep 17 00:00:00 2001 From: Mischa Date: Sun, 5 Jul 2026 12:05:51 +0200 Subject: [PATCH] docs: capture local Grav core upgrade + refresh version-authority docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New learning: docs/solutions/tooling-decisions/upgrade-local-grav-core-rebuild-docker-image.md — the local Grav core is baked into the Docker image (only ./user is bind-mounted), so it upgrades by a Dockerfile URL bump + image rebuild + `docker rm -f` recreate, not the `gpm self-upgrade` the servers use (non-durable in-container). Refreshed three docs this exposed as stale/incomplete: - local-setup.md: rewrote the stale "newer Grav RC" section with the durable rebuild procedure (recreate gotcha, verify, plugin refresh, non-durability note). - deploy-cycle.md: Phase 0 now upgrades the local core; state-model notes the image as a fourth surface beyond the three server layers. - stale-grav-version-blocks-api-plugin-install.md: version-authority surfaces 3 -> 4 (hardcoded Dockerfile URL); clarified .env* GRAV_VERSION governs fresh remote installs only, never the local Docker core. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01Np4cMQLF77i664CAQXySzU --- docs/guides/deploy-cycle.md | 8 +- docs/guides/local-setup.md | 29 ++++- ...-grav-version-blocks-api-plugin-install.md | 2 +- ...de-local-grav-core-rebuild-docker-image.md | 115 ++++++++++++++++++ 4 files changed, 148 insertions(+), 6 deletions(-) create mode 100644 docs/solutions/tooling-decisions/upgrade-local-grav-core-rebuild-docker-image.md diff --git a/docs/guides/deploy-cycle.md b/docs/guides/deploy-cycle.md index ba26b63..88abee0 100644 --- a/docs/guides/deploy-cycle.md +++ b/docs/guides/deploy-cycle.md @@ -34,6 +34,11 @@ Referenced gotcha docs: - `docs/solutions/architecture-patterns/git-sync-secret-exposure-and-tracked-file-boomerang.md` — gitignore is the sync boundary; env-tree leak. - `docs/solutions/conventions/grav-plugin-config-must-be-tracked-override.md` — plugin config must live in the tracked override. +These three layers describe the **servers**. Locally there is a fourth: the Grav +**core** is baked into the Docker **image** (`Dockerfile`), not in any layer above — +so the local core upgrades by an image rebuild, never by the `gpm self-upgrade` the +servers use. See `docs/solutions/tooling-decisions/upgrade-local-grav-core-rebuild-docker-image.md`. + --- ## Phase 0 — Local (author + prove the change) @@ -42,7 +47,8 @@ Referenced gotcha docs: - GPM channel: `gpm.releases: stable` in `user/config/system.yaml` (authoritative; reaches servers via content pull, so it must be right **before** any server GPM op). - `plugins.txt` — the GPM-managed set only. **Never** add `git-sync` (it is remote-only). - Prod-only overrides (Twig cache/debug, `debugger.shutdown.close_connection: false`) in `deploy/env/prod/system.yaml` — **never** commit prod values into `user/config/system.yaml`. - - **Bump `GRAV_VERSION` in `.env.test` and `.env.prod`** to the target version. A stale value here installs the wrong core (an rc), which then blocks the `api` plugin and 404s admin. + - **Bump `GRAV_VERSION` in `.env.test` and `.env.prod`** to the target version. A stale value here installs the wrong core (an rc), which then blocks the `api` plugin and 404s admin. This governs fresh **remote** installs only. + - **If the core version is changing, upgrade the local dev core too** so you prove the change against the target version — bump the hardcoded `grav-admin-v.zip` URL in `Dockerfile`, `docker compose build grav`, then `docker rm -f intotheeast_grav && docker compose up -d grav`. The local core is baked into the image, so `.env GRAV_VERSION` does *not* touch it and an in-container `gpm self-upgrade` is non-durable. See `docs/solutions/tooling-decisions/upgrade-local-grav-core-rebuild-docker-image.md`. 2. `make build-assets` if you touched `js/src/*` (never hand-edit the bundled `js/*.js`). 3. Run the dev server (`docker compose … up`) and the Playwright suite. 4. Pre-flight assertions: diff --git a/docs/guides/local-setup.md b/docs/guides/local-setup.md index 66cac1c..54d76ea 100644 --- a/docs/guides/local-setup.md +++ b/docs/guides/local-setup.md @@ -44,13 +44,34 @@ This creates uid 1000 in the container, chowns `/var/www/html` to 1000:1000, and --- -## Upgrading to a newer Grav RC +## Upgrading the Grav core -Grav 2.0 is baked into the custom Docker image via `Dockerfile`. The base `getgrav/grav` image ships 1.7 — the `Dockerfile` downloads the 2.0 RC bundle from GitHub and overwrites the core files at build time. +The Grav core is baked into the custom Docker image via `Dockerfile`. The base +`getgrav/grav` image ships 1.7 — the `Dockerfile` downloads the pinned stable bundle +(`grav-admin-v.zip`) from GitHub and overwrites the core files at build time. +`docker-compose.yml` volume-mounts only `./user`, so the core lives in the **image +layer**. That means you upgrade the core by rebuilding the image, **not** by running +`gpm self-upgrade` inside the container — an in-container self-upgrade is lost on the +next rebuild. (The servers are the opposite: no image, so they self-upgrade in place.) To upgrade: -1. Update the bundle URL in `Dockerfile` -2. Run `make setup` — Docker rebuilds the image layer automatically +1. Bump **both** occurrences of the version in the `Dockerfile` release URL (the + `/download//` path and the `grav-admin-v.zip` filename). Note: the + `GRAV_VERSION` in `.env*` does **not** drive this build — it only pins fresh + *remote* installs. +2. Rebuild: `docker compose build grav`. +3. Recreate the container. `docker compose up -d` won't replace an already-running + container with a fixed `container_name` (it errors `Conflict … name … already in + use`), so remove it first — safe because `./user` is a bind mount: + ```bash + docker rm -f intotheeast_grav && docker compose up -d grav + ``` +4. Verify: `docker exec -w /var/www/html intotheeast_grav php bin/grav --version`. +5. Refresh plugins and clear cache: `make install-plugins` then + `docker exec -w /var/www/html intotheeast_grav php bin/grav cache`. + +Full rationale and the server-vs-local contrast: +`docs/solutions/tooling-decisions/upgrade-local-grav-core-rebuild-docker-image.md`. --- diff --git a/docs/solutions/integration-issues/stale-grav-version-blocks-api-plugin-install.md b/docs/solutions/integration-issues/stale-grav-version-blocks-api-plugin-install.md index abd6989..cdd4991 100644 --- a/docs/solutions/integration-issues/stale-grav-version-blocks-api-plugin-install.md +++ b/docs/solutions/integration-issues/stale-grav-version-blocks-api-plugin-install.md @@ -79,7 +79,7 @@ GPM (Grav Package Manager) only offers a plugin version whose declared Grav requ ## Prevention -- **Keep `.env.` `GRAV_VERSION` current.** It is the version a *fresh* `make remote-install-` bakes in; a stale value silently installs an old core. After any core upgrade, bump the env file too — self-upgrade only moves the running server. Note this is a *third* version-authority surface alongside `user/config/system.yaml` `gpm.releases` (channel) and `plugins.txt` — they must stay in sync. +- **Keep `.env.` `GRAV_VERSION` current.** It is the version a *fresh* `make remote-install-` bakes in; a stale value silently installs an old core. After any core upgrade, bump the env file too — self-upgrade only moves the running server. Note this is a *third* version-authority surface alongside `user/config/system.yaml` `gpm.releases` (channel) and `plugins.txt` — they must stay in sync. A **fourth** surface governs the *local Docker* core: the hardcoded `grav-admin-v.zip` URL in `Dockerfile`. `.env. GRAV_VERSION` governs fresh **remote** installs only — it never touches the local Docker core (which upgrades by an image rebuild, not self-upgrade). See `docs/solutions/tooling-decisions/upgrade-local-grav-core-rebuild-docker-image.md`. - **When GPM says "package not found" for a package you know is on your channel, check the target's Grav core version first** (`php bin/grav --version` on the server, or `make remote-diag-`). GPM filters by core compatibility; "not found" often means "no version compatible with your core," not "missing from the index." `gpm index -f` will not help. - **Don't trust a top-level install "Success" to mean dependencies installed.** A fresh install can leave a plugin's declared dependency unsatisfied (here `admin2` installed but its `api` dependency didn't). Verify with `ls user/plugins/`. The same `ls` guards a *second*, distinct way a plugin ends up non-functional: its **code folder can be missing while its config persists** (e.g. in the per-host env tree), so it looks configured but never loads. Checking `ls user/plugins/` catches both the missing-dependency and the config-without-code cases — see `grav-plugin-config-without-code-wont-enable.md`. - **Know the Admin2 ⇄ api coupling.** Admin2 authenticates via the api plugin's `/api/v1` endpoints; a missing or unrouted api plugin makes admin login fail *silently* (login POST 404s, nothing logged). A quick `curl /api/v1/pages` expecting `401` (not `404`) is a good post-deploy smoke check. diff --git a/docs/solutions/tooling-decisions/upgrade-local-grav-core-rebuild-docker-image.md b/docs/solutions/tooling-decisions/upgrade-local-grav-core-rebuild-docker-image.md new file mode 100644 index 0000000..7f16b64 --- /dev/null +++ b/docs/solutions/tooling-decisions/upgrade-local-grav-core-rebuild-docker-image.md @@ -0,0 +1,115 @@ +--- +title: Upgrade the local Grav core by rebuilding the Docker image, not gpm self-upgrade +date: 2026-07-05 +category: docs/solutions/tooling-decisions/ +module: docker-dev-env +problem_type: tooling_decision +component: tooling +severity: medium +applies_when: + - Upgrading the Grav core in the local Docker dev environment + - App core is baked into the image while only user content is bind-mounted + - Deciding between an image rebuild and an in-container package upgrade + - "docker compose up refuses to recreate a fixed container_name" +tags: [grav, docker, dockerfile, image-rebuild, gpm, upgrade, container-recreate] +--- + +# Upgrade the local Grav core by rebuilding the Docker image, not gpm self-upgrade + +## Context + +The test and prod servers had already self-upgraded to Grav 2.0.7 in place, but the local Docker dev environment was still on 2.0.4. The question was how to bring local to 2.0.7 **durably** — in a way that survives the next image rebuild and keeps local reproducible from the repo. + +The instinct is to do what the servers do: `php bin/gpm self-upgrade` inside the running container. That is the wrong tool for the local box, and understanding why is the whole point of this note. + +## Guidance + +**The local Grav core is baked into the Docker image, so you upgrade it by editing the `Dockerfile` and rebuilding — never by upgrading inside a running container.** + +The dev image (`Dockerfile`) `curl`s a specific release zip and copies its `system/`, `vendor/`, `bin/`, `index.php`, etc. into the image at build time: + +```dockerfile +RUN curl -sL 'https://github.com/getgrav/grav/releases/download/2.0.7/grav-admin-v2.0.7.zip' ... +``` + +The version is **hardcoded in the URL** — there is no `ARG`, so the `GRAV_VERSION` variable in `.env*` does **not** feed the local build (it only pins the base zip for a *fresh remote install*). `docker-compose.yml` volume-mounts **only** `./user:/var/www/html/user` (plus a php.ini). Everything else — the entire core — lives in the immutable image layer. + +The durable local upgrade sequence: + +```bash +# 1. Bump BOTH occurrences of the version in the Dockerfile release URL +# (the /download// path and the grav-admin-v.zip filename) + +# 2. Rebuild the image (the FROM getgrav/grav layer is cached; the RUN +# layer re-fetches the new zip in a few seconds) +docker compose build grav + +# 3. Recreate the container. `up -d` may refuse (see gotcha below); if so: +docker rm -f intotheeast_grav && docker compose up -d grav + +# 4. Verify the core version +docker exec -w /var/www/html intotheeast_grav php bin/grav --version # -> Grav CLI Application 2.0.7 + +# 5. Refresh plugins to match the servers, then clear cache +make install-plugins # docker exec ... php bin/gpm install -y +docker exec -w /var/www/html intotheeast_grav php bin/grav cache +``` + +**Gotcha — `docker compose up` won't replace a running fixed-name container.** The service pins `container_name: intotheeast_grav`, so `docker compose up -d` (and even `--force-recreate`) fails with `Conflict. The container name "/intotheeast_grav" is already in use`. Remove the old container first: `docker rm -f intotheeast_grav`, then `up -d`. This is **data-safe** because all persistent content lives in the `./user` bind mount, which is untouched by removing/recreating the container. (This same singleton collision bit an earlier upgrade session when the running container from the main checkout held the name+port. — session history) + +## Why This Matters + +**An in-container `gpm self-upgrade` is non-durable locally.** It writes into the image's filesystem layer, not the `./user` volume, so the upgraded core evaporates on the next `docker compose build` / container recreate. The image, not the running container, is the source of truth for the core — so the core version must be baked into the `Dockerfile` to persist and to stay reproducible from the repo. + +**Local and server upgrade by deliberately different mechanisms:** + +- **Servers** are native webroot installs with no image, so `bin/gpm self-upgrade` mutates the install in place and *is* durable there. (Note: `bin/grav upgrade` does **not** exist — the correct verb is `bin/gpm self-upgrade`. — session history) +- **Local** is rebuilt from an image, so only a `Dockerfile` bump persists. + +A consequence worth remembering (accepted risk, flagged in the original upgrade session): the local gate never exercises the server's in-place `self-upgrade` path — a fresh image bakes a clean core and reinstalls plugins clean, whereas the server mutates an existing core in place. A green local build proves the clean-install path, not the in-place upgrade path; the remote upgrade is the first real test of that. (session history) + +**Same mental model applies beyond version upgrades.** Because the core/runtime is baked and only `./user` is mounted, *any* runtime capability lives in the image. Adding server-side HEIC support (ImageMagick/libheif) would likewise require a custom image rebuild — which is why HEIC was handled client-side instead. "The core is in the image; only `./user` is a volume" is the reusable principle. (session history) + +## When to Apply + +- Any time the **local** Grav core version needs to change (upgrade or, rarely, a pinned downgrade — note `gpm self-upgrade` is forward-only and cannot downgrade). +- Whenever you catch yourself about to run `gpm self-upgrade` inside the dev container "to match the server" — stop and bump the `Dockerfile` instead. +- When `docker compose up`/`--force-recreate` reports a container-name conflict for a service with a fixed `container_name`. + +## Examples + +Concrete run from the 2.0.4 → 2.0.7 local upgrade (2026-07-05): + +``` +# Dockerfile line 3: .../download/2.0.4/grav-admin-v2.0.4.zip +# -> .../download/2.0.7/grav-admin-v2.0.7.zip + +$ docker compose build grav + => CACHED [1/2] FROM docker.io/getgrav/grav:latest + => [2/2] RUN curl -sL '.../2.0.7/grav-admin-v2.0.7.zip' ... 3.4s + +$ docker compose up -d grav + Error response from daemon: Conflict. The container name + "/intotheeast_grav" is already in use ... + +$ docker rm -f intotheeast_grav && docker compose up -d grav + Container intotheeast_grav Started + +$ docker exec -w /var/www/html intotheeast_grav php bin/grav --version + Grav CLI Application 2.0.7 + +# Smoke test from INSIDE the container (the host has no curl): +$ docker exec intotheeast_grav sh -c \ + 'for p in / /admin /gpx-manager; do curl -s -o /dev/null -w "%{http_code}\n" "http://localhost:80$p"; done' + 200 + 200 + 200 +``` + +**Config caveat:** a server-side `gpm self-upgrade` runs Grav's schema migration and rewrites `system.yaml` `strict_mode` flags (`twig_compat` → `twig2_compat`/`twig3_compat`). A fresh-image rebuild does **not** trigger that migration, so `user/config/system.yaml` in the repo must already carry the intended Twig-3 flags (it does, from an earlier reconciliation). If it didn't, local and server config would silently drift. This is another reason the image-rebuild path depends on the repo config being the source of truth. + +## Related + +- `docs/guides/local-setup.md` — "Upgrading to a newer Grav" section documents the same bump-and-rebuild procedure, but with stale "RC bundle" wording and without the `docker rm -f`, version-verify, plugin-refresh, or non-durability details. **Refresh candidate** — fold these operational steps in and drop the "RC" language (`Dockerfile` now pins stable `grav-admin-v2.0.7.zip`). +- `docs/solutions/integration-issues/stale-grav-version-blocks-api-plugin-install.md` — the server/`.env`/fresh-install counterpart. That doc frames `GRAV_VERSION` as a version-authority surface for *remote* installs; this doc adds the **fourth** authority surface (the hardcoded release-zip URL in `Dockerfile`) and clarifies that `.env* GRAV_VERSION` never touches the local Docker core. Servers correctly self-upgrade because they have no image; local Docker cannot. +- `docs/guides/deploy-cycle.md` — the local→test→prod runbook. Its Phase 0 (Local) covers bumping `GRAV_VERSION` for remote installs but not upgrading the local Docker core; its "where state lives" table omits that the local core lives in the Docker image.