docs: capture local Grav core upgrade + refresh version-authority docs
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Np4cMQLF77i664CAQXySzU
This commit is contained in:
@@ -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/<ver>/ path and the grav-admin-v<ver>.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 <plugins.txt> -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.
|
||||
Reference in New Issue
Block a user