30 Sept 2026 · 5 min read
Profile-layered configuration in Govard
How Govard merges base, profile, local and env layers into one effective config, and how govard config profile exposes every override
Part 4 of the Govard in Depth series — one file per concern, one command to see the truth.
Why layers instead of one big file
A Magento project serves several masters. The base stack must be identical for everyone, or onboarding and CI diverge. Review environments need heavier search or extra memory. Individual developers need local tweaks (a different hostname, an exposed port, Xdebug always on) that must never be committed. And one-off runs need a temporary override without editing anything.
One file cannot serve all four without constant merge conflicts and accidental commits of personal settings. Govard splits the problem into layers with fixed precedence: base, profile, local, environment. Each layer has one job and one place, and the engine merges them deterministically on every run.
The four layers in order
Precedence runs from weakest to strongest. Later layers override earlier ones, key by key, never by wholesale replacement.
Base is .govard.yml at the repo root, committed, reviewed like code. It pins the framework, PHP version, and service images every environment shares:
# .govard.yml (base layer)
framework: magento2
php:
version: "8.3"
services:
database: mariadb:11.4
cache: redis:7
search: opensearch:2
Profile overlays are files like .govard.review.yml or .govard-heavy.yml, also committed. They describe named variations: more memory for search, an extra consumer, a different domain suffix. Nothing activates them by default; you opt in per run:
govard env up --profile review
Local overrides are untracked files (conventionally .govard.local.yml, git-ignored) for personal settings: your hostname, your port mapping, your Xdebug preference. Because the file never reaches the repo, your tweaks cannot leak into a teammate's environment through a careless commit.
Environment variables (notably GOVARD_ENV and targeted GOVARD_* keys) sit at the top for one-off runs. They let CI set a build-specific value or let you test a PHP bump for ten minutes without touching any file. Anything set here wins, which is exactly why it should stay ephemeral: permanent settings belong in a reviewable file.
A concrete layering example
Suppose the team runs PHP 8.3 and MariaDB 11.4 in base, review environments need OpenSearch with double heap, you personally develop on a custom hostname with Xdebug on, and today you want to trial PHP 8.4 quickly. The layers compose:
# .govard.review.yml (profile layer, committed)
services:
search: opensearch:2
search:
heap: "2g"
consumers:
enabled: true
# .govard.local.yml (local layer, git-ignored)
domains:
app: app.toan.test
xdebug:
enabled: true
# env layer: one-off trial, nothing written to disk
GOVARD_PHP_VERSION=8.4 govard env up --profile review
The effective configuration for that run is base plus review plus local plus the PHP override. Tomorrow, without the env var, the same command renders 8.3 again. The trial left no residue because it never touched a file.
Seeing the truth: effective-config debugging
Layering only works if you can inspect the merged result. Govard's answer is a single command that prints the effective configuration after all layers merge:
govard config profile # full merged document
govard config profile --keys php # narrow to one subtree when hunting
govard env ps # confirm what actually runs
Build the habit of running govard config profile before govard env up whenever behavior surprises you. The five questions it answers cover nearly every configuration mystery: which profile was active, whether the local file injected something forgotten, whether an env var from an earlier session is still exported, which image tags resolved, and which blueprint version rendered them.
A typical session looks like this. Search behaves oddly in review; print the config and see heap: 1g where the profile promised 2g. The profile flag was missing from the up command, so base rendered. Re-run with --profile review, print again, confirm 2g, restart. Total diagnosis time: two commands, no file reading, no guessing.
Rules that keep layers healthy
Keep base minimal and complete. Every key a fresh clone needs to boot belongs here: framework, PHP, database, cache, search. If git clone plus govard env up does not produce a working skeleton, base is missing something, and no profile should have to compensate.
Keep profiles named after purposes, not people. review, heavy, ci describe why the variation exists; toan and staging2 describe accidents waiting to happen. Each profile gets a one-line comment at the top stating when to use it, because a profile nobody can explain is a profile nobody dares delete.
Never commit local overrides. Add the local filename to .gitignore on the day you introduce layering, and add a CI check if the team is large enough to need one. The first leaked personal hostname in a shared config costs more trust than any feature earns.
Reserve env vars for the ephemeral. If a GOVARD_* export survives longer than a day, promote it into the local file (personal) or a profile (shared). Long-lived exports are invisible configuration: they change behavior without appearing in any file, which defeats the reproducibility layering exists to provide.
Pitfalls of layered config
- Forgotten profile flag. The commonest surprise: expecting review behavior without passing
--profile review. The effective config shows base values; the fix is the flag, not the file. - Stale env exports. A
GOVARD_*variable exported last week silently overrides today's files. When the merged config contradicts every file, runenv | grep GOVARDbefore anything else. - Local file committed by accident. Force-adds and hurried commits bypass gitignore. Review diffs for the local filename during migration season.
- Deep-merge assumptions. Overrides merge key by key; they do not append to lists unless documented. If a profile's service list replaces rather than extends, restructure so base holds the full list and profiles adjust scalars.
- Debugging the containers before the config. Reading Docker logs when the merged config is wrong wastes the hour. Config first, containers second, every time.