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
3.8 KiB
Local Development Setup
This guide covers setting up the dev environment from scratch after cloning the repo.
First-time setup
user/plugins/ and user/data/ are excluded from git but Grav requires them to exist. Create them once:
mkdir -p user/plugins user/data
Then run:
make setup
make setup = build → start → install-plugins → fix-perms. This builds the Docker image (Grav 2.0 baked in), starts the container, installs all plugins from plugins.txt, and fixes file ownership.
The dev server runs at http://localhost:8081.
After any docker compose down
Always run make setup — not just make start — to ensure permissions are correct.
docker compose restart (soft restart) preserves the image and is fine for quick restarts. Only make setup is needed after docker compose down.
Fix 500 errors after plugin install
If the site returns a 500 error after plugin installation or after recreating the container:
make fix-perms
This creates uid 1000 in the container, chowns /var/www/html to 1000:1000, and reloads Apache.
Upgrading the Grav core
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<version>.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:
- Bump both occurrences of the version in the
Dockerfilerelease URL (the/download/<ver>/path and thegrav-admin-v<ver>.zipfilename). Note: theGRAV_VERSIONin.env*does not drive this build — it only pins fresh remote installs. - Rebuild:
docker compose build grav. - Recreate the container.
docker compose up -dwon't replace an already-running container with a fixedcontainer_name(it errorsConflict … name … already in use), so remove it first — safe because./useris a bind mount:docker rm -f intotheeast_grav && docker compose up -d grav - Verify:
docker exec -w /var/www/html intotheeast_grav php bin/grav --version. - Refresh plugins and clear cache:
make install-pluginsthendocker 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.
Required system.yaml settings (Grav 2.0)
After upgrading, verify these are set in user/config/system.yaml:
accounts:
type: flex # required for Admin2 API
pages:
type: flex # required for Admin2 pages API
Admin user API permissions
The admin user account needs api.* permissions for Admin2. In user/accounts/<username>.yaml:
access:
admin:
login: true
super: true
api:
super: true
access: true
Disable the old admin plugin
Both admin and admin2 route to /admin and conflict. After installing admin2, disable the old one:
In user/plugins/admin/admin.yaml:
enabled: false
JWT secret
Leave jwt_secret: '' in user/plugins/api/api.yaml. It works for local dev; production installs generate a secure secret automatically during make remote-install.
Language URL prefix
If Grav redirects to /en/... URLs, ensure user/config/system.yaml contains:
languages:
supported: [en]
include_default_lang: false
Without include_default_lang: false, Grav adds a language prefix to all URLs even for single-language sites.