ddtcorex

29 Sept 2026 · 6 min read

Inside Govard: a Go orchestrator's engine and registries

How Govard's Go engine, command registry and framework blueprints turn one .govard.yml file into a running Magento stack

Part 3 of the Govard in Depth series — the machinery behind govard env up.

A single binary, no runtime to install

Govard ships as one compiled Go binary. There is no PHP runtime dependency, no Python virtualenv, no Node version to align before the tool itself runs. That choice shapes everything else: startup is milliseconds, distribution is a download-and-run, and the orchestration logic (configuration merge, blueprint rendering, container lifecycle) executes in-process instead of shelling out to an interpreter.

If you come from Docker Compose wrappers written in Bash or PHP, the difference shows up in error handling. A Go orchestrator can validate the full configuration before touching Docker, hold the resolved model in typed structs, and fail with a structured message naming the offending key. govard env up does not half-create three containers and then complain about a typo in the search version; it refuses early, with the key path attached.

The engine: config in, containers out

The engine is the pipeline from .govard.yml to running containers. Conceptually it has four stages, and each maps to a debugging question when something goes wrong.

First, load and merge configuration. The engine reads the base .govard.yml, applies profile overlays, local overrides, and environment variables in a fixed precedence order (the full layering story is the next post). The output is an effective configuration: one resolved document with no ambiguity left. govard config profile prints exactly this document, which is why it is always the first debugging command.

Second, detect and bind the framework. The framework: magento2 key selects a framework adapter that knows Magento's needs: PHP-FPM plus MariaDB plus Redis plus a search backend, the expected document root, the shape of bin/magento invocations, and which health checks mean "ready." Laravel, Symfony, and WordPress adapters make the same engine produce different stacks from the same pipeline.

Third, render the blueprint. A blueprint is a versioned template set that turns the effective configuration into concrete Docker artifacts: compose services, networks, volumes, proxy routes, and environment variables injected into containers. Blueprints carry the opinionated defaults (which PHP extensions, which MariaDB flags, which OpenSearch heap) so the YAML stays small. When Magento 2.4.8 needs a new PHP build, the blueprint updates; project files do not.

Fourth, reconcile against Docker. The engine compares the desired state (rendered blueprint) with actual state (running containers, existing volumes) and applies the minimal change: create what is missing, restart what drifted, leave the rest alone. This is what makes repeated govard env up runs safe and fast. The second run converges in seconds because there is nothing to do.

govard env up          # full pipeline: merge, bind, render, reconcile
govard env ps          # actual state: what really runs
govard config profile  # desired state: the merged configuration

When up and ps disagree, the gap is usually in stage four: a stale volume pinning old data, a port claimed by another project, or an image pull that failed silently behind a cached layer.

The command registry and CommandPath

Every CLI command Govard exposes is registered in a command registry keyed by a stable CommandPath. Think of it as the tool's public API surface: env/up, env/ps, db/dump, magento/cache-flush, each with a declared handler, flags, and help text. The registry is what makes govard --help truthful by construction rather than by documentation discipline; help output is rendered from the same registration that dispatches execution.

The CommandPath convention matters for two audiences. For users, it gives a predictable grammar: nouns first (env, db, config), verbs second (up, ps, dump, profile). Once you know govard db dump, govard db import needs no discovery. For framework adapters and future plugins, it gives a stable extension point: Magento-specific commands register under their own paths without touching the engine core, and capability listings can enumerate exactly which paths a project supports.

A simplified view of the flow:

CLI args → registry lookup (CommandPath) → flag validation
  → engine with effective config → framework adapter
  → blueprint render → Docker reconcile → structured result

Flag validation happens before any side effect, which is why govard db import --file /nonexistent.sql fails in milliseconds without stopping containers. The registry owns that ordering guarantee.

Registries beyond commands

Commands are the visible registry; two quieter ones do as much work. The framework registry maps the framework: key to its adapter (detection rules, default services, health checks, command extensions). Adding Symfony support meant registering a new adapter, not branching the engine. The blueprint registry maps framework-plus-version to template sets, so magento2 on PHP 8.2 and 8.3 can render different extension lists from the same engine code.

Versioning lives at these seams. The engine stamps rendered output with the blueprint version it used, and govard lock pins service images so a teammate pulling next month gets the same MariaDB minor. Drift between "what I rendered" and "what you rendered" becomes answerable: compare effective configs and blueprint versions, and the difference names itself.

Why Go and not a Compose wrapper

Three properties of Go earn their keep here. Static binaries remove the install story. Goroutines make parallel health checks and log streaming cheap: env up can wait on four containers concurrently with per-service timeouts instead of serial polling. And the type system carries the configuration model: the effective config is a struct, not a nested hash, so a misspelled key is a compile-time-shaped error at validation time rather than a nil-map panic at render time.

The tradeoff is template verbosity. Blueprint templates in Go are more explicit than their Bash heredoc equivalents, and small rendering changes require a rebuild rather than an edited script. Govard accepts that cost because the alternative (untyped shell rendering different stacks on different machines) is precisely the drift the tool exists to kill.

Debugging with the model in mind

When an environment misbehaves, walk the pipeline in order. Print the effective config first. Confirm the framework binding matches the project. Check which blueprint version rendered. Then compare desired versus actual container state. Nine times out of ten the answer appears before Docker logs are even needed: a profile override you forgot, a PHP version pinned somewhere unexpected, a search image the blueprint defaulted because the YAML left it blank.

govard config profile   # stage 1 output: merged config
govard env ps           # stage 4 output: actual containers
docker logs <container> # only now, with the model confirmed

That ordering is the practical payoff of an orchestrator architecture. The tool shows its work at every stage, so diagnosis follows the same path every time.

Next in this series: govard-config-profiles-layering

govardgoarchitecture