Skip to content

CLI Commands ​

This is the canonical CLI reference for Govard.


Aliases and Shortcuts ​

Root Lifecycle Shortcuts ​

ShortcutEquivalent
govard upgovard env up
govard downgovard env down
govard restartgovard env restart
govard psgovard env ps
govard logsgovard env logs

Command Aliases ​

AliasFull Command
govard bootgovard bootstrap
govard cfggovard config
govard dbggovard debug
govard guigovard desktop
govard diaggovard doctor
govard extgovard extensions
govard prjgovard project
govard rmtgovard remote
govard shgovard shell
govard snapgovard snapshot

govard tool Aliases ​

AliasFull Command
govard tool mrgovard tool magerun

govard sync Aliases ​

  • --from is an alias for --source
  • --to is an alias for --destination
  • -e, --environment remains a supported source-environment option

🌿 Environment Commands ​

govard audit ​

Run framework-declared, persistent project audits. lint via govard provider is supported for Magento 2, Laravel, Symfony, WordPress (4 frameworks); Magento 2 and Mage-OS also declare a Govard-native stock CSV profiler check. Later phases will add browser jobs without changing session semantics.

bash
govard audit run --checks lint               # Magento2
govard audit run --checks lint --mode project # Laravel/Symfony/WordPress (auto resolves)
govard audit run --checks profiler --url 'https://shop.test/category.html?product_list_limit=48'
govard audit run --checks lint,profiler --url 'https://shop.test/'
govard audit diff --base origin/master
govard audit rerun --session 20260816T010203Z-a1b2c3d4
govard audit status --session 20260816T010203Z-a1b2c3d4
govard audit result --session 20260816T010203Z-a1b2c3d4 --run run-0001
govard audit cleanup --older-than 168h

run defaults to --scope project, --checks lint, --lint-provider govard, --mode auto, and --lint-jobs min(nproc,4) (default 4 on typical hosts, clamped 2–8). The default text format streams live progress like vendor/bin/phpcs/vendor/bin/phpstan — phase validate → prepare → phpcs/phpstan, cache state (cold/warm/bypassed), and glint: php X.Y analyzed appear as they happen — then prints a compact human-readable summary: the verdict first (PASSED/FAILED/CANCELLED), then scope, duration, environment, per-PHP results with cache state and findings, plus next-step hints pointing at the persisted report and the exact rerun command. On an interactive TTY (and when NO_COLOR is unset) findings are colorized (tool bold light-blue, rule yellow, path:line:col cyan) and every finding is shown like the native CLIs; when piped or redirected the output stays plain and is capped at ten findings (... and N more) to keep CI logs readable. A completed run whose checks did not pass (failed or cancelled) still prints its summary and then exits non-zero, so scripts and CI observe the outcome. --format json writes one undecorated JSON object to stdout for AI agents; diagnostics, backend logs, and the ERROR audit run … reported failed checks exit hint remain on stderr or in the persisted govard-lint.log. Only text and json formats are accepted, and --lint-jobs must be between 1 and the number of PHP versions declared by the framework.

profiler requires an explicit absolute HTTP(S) --url on the first run and a whole Govard project target (standalone modules and module-only targets are rejected before runtime mutation). The exact URL is persisted in the run and reused by audit rerun, so before/after runs capture the same page. Govard uses Magento's stock MAGE_PROFILER=csvfile; it does not install a Magento module, depend on a third-party repository/image, or edit app/etc/env.php.

Run govard env up with the current Govard version before the first capture so the generated Compose stack mounts the project-owned custom configuration directory. During the leased capture Govard atomically creates one uniquely named include, reloads the active server, performs a bounded HTTP GET, collects var/log/profiler.csv through the PHP container, and restores both the include and runtime CSV. Nginx receives the temporary FastCGI parameter inside Magento's PHP location. Apache mode uses the web service; hybrid configures and reloads its apache service rather than nginx. The collected CSV is stored under runs/<run-id>/artifacts/profiler/profile.csv with a SHA-256 digest.

Example govard audit run summary:

== Audit run run-0001 / session 20260822T005406Z-14bd9570 ==
  Status:      FAILED
  Scope:       project
  Duration:    4.5s
  Environment: magento2 | nginx | Govard 1.63.0

  Checks
    - lint - failed - 4.5s - provider govard
      PHP 8.5 | failed | 3.9s | cache cold | 12 findings
      - phpcs Squiz.Classes.ClassFileName app/code/Acme/Catalog/Model/Item.php:12: Class name is not camel case

  What next
  Full findings: ~/.govard/audit/<project-id>/sessions/<session-id>/runs/run-0001/report.json
  Re-run:        govard audit rerun --session <session-id>

Each run creates ~/.govard/audit/<project-id>/sessions/<session-id>/manifest.json and writes its result atomically to ~/.govard/audit/<project-id>/sessions/<session-id>/runs/<run-id>/audit-result.json, alongside the provider's own report.json. rerun, status, and result require the exact --session value (and result also requires --run); Govard never chooses a latest session implicitly.

A rerun without an explicit --checks repeats the check selection of the latest run in that session — including its persisted profiler URL — instead of falling back to the lint default. Passing --checks reruns exactly what was requested; either form reconstructs only the backends that selection needs.

Target modes ​

--mode selects what is analyzed. auto (the default) classifies the current directory:

ModeResolved whenWhat is analyzed
projectThe directory tree contains a Magento project root (a bin/magento plus a Magento Composer requirement) and no enclosing moduleThe whole project
module_in_projectThe directory is a module — either etc/module.xml (how app/code modules are declared) or a Composer package of type magento2-module — inside a Magento projectOnly the module, with the whole project mounted read only so its autoloader resolves
standaloneThe directory is a module with no Magento project anywhere above itOnly the module; its dependencies are installed into a scratch worktree and scanned for symbols

--mode project, --mode module_in_project, and --mode standalone force a classification and fail when the directory does not support it.

bash
# project: run from the Magento project root
cd ~/projects/storefront
govard audit run

# module_in_project: run from inside an app/code module
cd ~/projects/storefront/app/code/Acme/Catalog
govard audit run

# module_in_project: run from inside a vendor package (Composer type magento2-module)
cd ~/projects/storefront/vendor/acme/module-catalog
govard audit run

# standalone: run from a module with no Magento project above it
cd ~/work/module-catalog
govard audit run --php 8.1,8.5

Each of these selects its mode automatically from the current directory — --mode only needs to be passed to force or refuse a classification (for example --mode project fails outside a project root instead of silently reclassifying).

--path <dir> resolves the target from <dir> instead of the current working directory, on audit run and audit diff alike, so one invocation can audit a directory it does not run in — a checklist row auditing a module below the project root, for example. A relative path resolves against the current working directory, and <dir> must exist: resolution fails when it does not, and the mode classification is applied to <dir> exactly as it would be to the working directory.

bash
# force a module target while running from the project root
govard audit run --mode module_in_project --path app/code/Acme/Catalog

PHP versions ​

The lint image provides 7.4, 8.0, 8.1, 8.2, 8.3, 8.4, and 8.5.

  • project and module_in_project targets analyze exactly one version: the project's active stack.php_version. Any of the seven is valid there. Passing --php is only accepted when it repeats that active version, and the run is refused outright when a running application container reports a different PHP than the configuration does.
  • standalone targets accept 8.1 through 8.5 and default to all five. 7.4 and 8.0 are not available for standalone modules and are rejected before any image is pulled, built, or run.

A rejected version fails with an unsupported_php: message and performs no container work at all.

Scanned paths ​

Both native analyzers skip trees that never contain shipped code: vendor/, generated/, var/, pub/static/, and pub/media/. Skipping user-content trees keeps full-project runs fast on content-heavy stores.

Because pub/media is skipped by the analyzers but is exactly where uploaded webshells land, every analyzed PHP version also runs a media guard phase: a name-only scan of pub/media for .php, .phtml, and .pht files. Each hit is reported as an M2-LINT-MEDIA finding with a path relative to the target root, and the phase fails the run. The id prefix comes from the framework (M2-LINT-* for Magento, LARAVEL-LINT-*, SYMFONY-LINT-*, WP-LINT-*). The scan costs milliseconds even on multi-gigabyte media trees; it inspects file names only and never reads file contents into the report.

Providers ​

--lint-provider selects the lint backend. govard (the default) is the Govard-owned native backend: it runs Govard's own Magento lint image, whose build context is embedded in the Govard binary. A release pins that image by immutable digest; when the pinned image cannot be pulled or its labels do not match the embedded context, Govard builds the embedded context locally instead and the run continues. There is no private registry and no external credential involved in the default path.

Any other value must name an entry under audit.lint.external_providers in the project configuration (see Configuration). External providers are never a fallback for the native backend and are never inferred: an unknown name is an error, and a native failure stays a native failure. A standalone target has no project configuration, so only govard is available there. --provider is a hidden alias for --lint-provider.

Scope (--scope, --base) ​

--scope project (default) audits the whole target. --scope diff --base <ref> records the requested base in the session manifest and lints only the changed files (git diff --name-only --diff-filter=ACMRT <base>...HEAD plus staged/unstaged vs HEAD, filtered to php/phtml under the target, vendor/generated/var/pub/media excluded via diff-files.txt mounted as GOVARD_LINT_DIFF_FILE); an empty diff short-circuits as passed with diff-empty cache without launching the container. Use --base auto to auto-detect the base via git merge-base HEAD origin/HEAD (and origin/master/origin/main fallbacks, then gh pr view --json baseRefName). git merge-base emits a commit SHA (used directly as the base); gh pr view normalizes to origin/<branch> when the prefix is missing. Example for the review skill:

bash
govard audit run --checks lint --mode project --scope diff --base auto --format json

Concurrency ​

Concurrent govard audit run invocations for the same project serialize on ~/.govard/audit/<projectId>/lock (via GovardHomeDir, respecting GOVARD_HOME_DIR). The second run waits up to 30s for the prior run to release the lock (audit run waiting for prior run) and then proceeds; runs are queued, not cancelled (fixing the earlier cancelled behaviour). If the lock remains held after 30s the run fails with a hint to remove the stale lock or run govard audit cleanup.

Xdebug guard ​

When stack.features.xdebug: true the lint audit exits with Xdebug enabled, ~10-20% tax; disable with govard debug off or pass --allow-xdebug unless --allow-xdebug is passed. The guard is enforced in the command and in the Govard lint backend. The container-free --checks integrity is exempt, and the --allow-xdebug warning goes to stderr so --format json stays one document.

Caching ​

Reusable lint state lives under ~/.govard/cache/audit/lint/<target-id>/ and is deliberately not removed by audit cleanup, which only prunes persisted sessions. Each target keeps one cache generation per toolchain identity (image, runner, PHP matrix, analyzer policy). Within a generation:

  • Changing composer.json, composer.lock, or an analyzer ruleset (phpcs.xml, phpstan.neon, and their .dist variants) discards the cached analyzer state but keeps the Composer download cache warm, so a lock change does not force a full dependency re-download.
  • --no-lint-result-cache ignores analyzer state for one run and is reported back as cache state bypassed. The Composer download cache is still kept.

Every run's evidence records the cache state (cold, warm, or bypassed) and its reason per PHP version, plus the immutable image digest, the toolchain digest, and timings for every phase.

Credentials and cancellation ​

Composer credentials at ~/.composer/auth.json are mounted read only when the file exists, and are linked into a private Composer home inside the container — never copied, logged, or written into a report. SSH agent forwarding is strictly opt-in through --allow-lint-ssh-agent; without that flag SSH_AUTH_SOCK is never forwarded even when it is set on the host. The source tree is always mounted read only.

Cancelling a run stops the lint container and then removes it, and the run is reported as cancelled rather than as an infrastructure failure.

diff fails when the base ref does not resolve to a commit. It records the requested base ref in the session manifest, but lint currently analyzes the full target; result evidence therefore reports effective_scope: project.

govard audit toolchain ​

Manage the machine-wide Govard lint image. These commands do not need to run inside a Govard project and never invoke an external lint provider. They do require a container runtime — without one they exit 3 with CAPABILITY_MISSING before touching Docker, like every other command whose manifest requirement is docker (Runs Without Docker).

bash
govard audit toolchain status
govard audit toolchain pull
govard audit toolchain build
  • status inspects local images only — it never pulls and never builds — and reports this build's embedded context digest, the pinned official reference, whether that official image is present and label-verified, and whether a local build already exists. When nothing usable is present it also prints what to run next.
  • pull resolves only the pinned official image. It never builds, so an unusable official path is reported rather than quietly replaced by a local image. A build with no pinned digest has nothing to pull and says so.
  • build builds only the embedded context and never pulls. The resulting image is content addressed, so an existing image for the same context digest is reused as-is.

govard init ​

Detect the project framework and generate .govard.yml.

bash
govard init
govard init --framework magento2
govard init --framework custom
govard init --migrate-from warden

When migrating from Warden, govard init --migrate-from warden maps WARDEN_TABLE_PREFIX to Govard's table_prefix field for Magento 2, Magento 1, and OpenMage projects.

govard bootstrap ​

Run bootstrap flows for clone or fresh-install setups.

bash
govard bootstrap
govard bootstrap --clone --environment staging --yes
govard bootstrap --framework magento2 --fresh --framework-version 2.4.9
govard bootstrap -e staging --no-pii --no-noise

Mode selection:

  • --fresh + --framework + --framework-version — fresh install via scaffolder. Without --framework-version, a fresh install uses the framework_version already in .govard.yml (for example from govard init --framework-version 2.4.6), and the plan and log say which version is used and where it came from; with neither it installs the latest. An explicit flag always wins.
  • --clone + --environment — rsync the whole source from a remote server

Source selection:

  • -e, --environment — source remote name; accepts standard names (staging, production, dev) and any custom identifier (qa, preprod, demo, client-uat)
  • --remote — alias for --environment
  • --db-dump — import database from a local SQL file path

sandbox is accepted too, and resolves from the running container the same way govard deploy --remote sandbox does. A non-plan run with no container stops with the sandbox's own message (no sandbox exists for this project; run 'govard sandbox up' to create it) rather than reporting the name as unconfigured, and it never offers to remote add sandbox — only govard sandbox up creates a container. That stop assumes there is no interactive prompt to fall back on: in a terminal without --yes the same message is printed and the remote-name picker opens instead, and the retry it then runs fails the same way. As with any unresolvable name, --plan stays non-fatal and prints no plan.

Privacy & performance filters:

FlagEffect
-N, --no-noiseExclude ephemeral data (logs, sessions, cache tags, cron history)
-P, --no-piiExclude sensitive data (customers, orders, admin users, passwords)
--deleteDelete destination files not present on source
--no-compressDisable rsync compression
-X, --excludeCustom rsync exclude patterns (repeatable)
--no-dbSkip database import
--no-mediaSkip media sync
--media [mode]Media sync mode (none, minimal, optimized, catalog (Magento), all)
--no-composerSkip composer install
--no-adminSkip admin user creation (Magento 2 only)
--no-stream-dbUse local temp file for DB transfer
--no-upSkip starting local containers before bootstrap steps
--code-onlyClone code only (skip DB/media, with --clone)
--fix-depsRun project fix-deps hook before bootstrap
--framework-versionFramework version for fresh install (e.g. 2.4.9 for Magento)

For Magento 2/Mage-OS, Magento 1/OpenMage, or PrestaShop projects with table_prefix set, DB privacy filters target prefixed table names automatically.

Magento special flags:

FlagEffect
--include-sampleInstall sample data (fresh install)
--hyva-installAuto-install Hyva theme

Plan & confirmation:

  • --plan — print plan and exit without executing
  • -y, --yes — skip interactive confirmation (CI/non-interactive)

govard env ​

Project lifecycle and Docker Compose wrapper.

bash
govard env up
govard env start
govard env stop
govard env restart
govard env down
govard env ps
govard env logs php -f
govard env pull
govard env build
govard env cleanup

govard env up flags:

FlagEffect
--pullPull images before starting
--fallback-local-buildBuild missing images locally (default true)
--remove-orphansRemove orphaned containers of this project. On by default for a full-stack start; pass --remove-orphans=false to keep them
--quickstartFastest startup path (minimal services)
--update-lockAuto-update govard.lock on mismatches
--no-tuningSkip framework auto-configuration prompts
--profile <name>Use a specific profile (.govard.<name>.yml) for this run
--force-recreateRecreate containers even if config unchanged

--profile is an inline alternative to govard config profile switch; the selected profile still requires govard env up to apply.

env up waits for every service's runtime readiness — up to ~90s per service. With wrong database credentials a service never turns healthy, so the failure arrives late: worst case one full wait per service. When up hangs, check the credentials first, not the orchestration.

govard env pull behavior:

Images are pulled one by one. If an image cannot be pulled (removed from the registry, unsupported tag, network failure), Govard retries the remaining images and builds Govard-managed failures locally instead of aborting the whole pull. Pass --no-fallback to disable the local build retry.

Search images: Govard-managed elasticsearch/opensearch tags track the minor version (e.g. 7.17), while search_version in .govard.yml accepts either minor (7.17) or full patch (7.17.28) versions. A patch version is pulled as-is when published; otherwise the minor image is used and the local fallback build targets the closest real upstream release.

Files re-rendered on env up:

  • ~/.govard/compose/<project-hash>.yml
  • ~/.govard/nginx/<project>/default.conf
  • ~/.govard/apache/<project>/httpd.conf
  • ~/.govard/nginx/<project>/mage-run-map.conf

govard env down flags:

  • -v, --volumes — remove volumes
  • --rmi local — remove local images

govard frontend ​

Manage the explicit, project-owned frontend development runtime for supported projects with stack.features.frontend_sync: true. Start the application with govard env up first. env up allocates no BrowserSync, LiveReload, Grunt, Tailwind watcher, or HTML-injection container; frontend resources exist only between explicit frontend start and frontend stop commands.

bash
govard frontend start
govard frontend logs -f
govard frontend logs watch-vendor-theme -f
govard frontend stop

start renders and starts only the dedicated frontend Compose services (sync, any discovered watch-<theme>, and inject), then waits for BrowserSync/Luma and every discovered watcher to be healthy. After health succeeds, it registers the active runtime through Caddy's Admin API. Both modes expose a narrow client-asset path on the application's own domain and run an HTML-injection proxy that shadows the application route (matching every path) so the client script appears on real page loads without editing project or theme files: Hyva exposes /browser-sync/* on port 3000 and injects <script src="/browser-sync/browser-sync-client.js"></script>; Luma exposes /livereload/* on port 35729 and injects <script src="/livereload/livereload.js?snipver=1&port=443&path=livereload/livereload"></script>. Both injectors buffer only HTML responses; everything else passes through unchanged.

stop removes the Caddy routes (including the injection proxy) before removing only the frontend services; dependency volumes are retained. Once removed, the application's own route resumes serving every request. If Caddy registration fails during start, Govard removes the newly started frontend services so no hidden runtime continues consuming resources. logs accepts only a discovered frontend service; omit the service to use sync.

govard svc ​

Manage global services (proxy, Mailpit, PHPMyAdmin, Portainer).

bash
govard svc up
govard svc restart --no-trust
govard svc logs --tail 50
govard svc sleep
govard svc wake

Portainer is accessible at https://portainer.govard.test Default login: admin / AdminGovard123$

govard domain ​

Manage extra local domains for the current project.

bash
govard domain add brand-b.test
govard domain remove brand-b.test
govard domain list

govard status ​

List running Govard environments across the workspace.

bash
govard status

govard verify ​

Run the 5-phase executable QA harness (replaces manual tick). Registry is the single source of truth — 74 static items across P1 15 · P2 15 · P3 15 · P4 21 · P5 8, plus the items each framework declares for itself. 10 static items carry When isMagento2 and are reported as skipped rows (not dropped) on Laravel/Symfony/WordPress, which get their dev loop back from the framework-declared items instead (below).

bash
govard verify --plan --json                 # dry-run all phases, machine JSON
govard verify --phase 1 --json              # single phase (1..5, 0=all)
govard verify --phase 5 --allow-destructive --json # destructive after snapshot
govard verify --phase 3 --project /path/to/project --json # target another project (a bare run refuses without --allow-destructive)

Flags: --phase 0..5, --json, --plan, --allow-destructive (--yes alias), --allow-remote-write, --allow-xdebug, --lint-jobs 4, --timeout auto|0|<dur>, --checks, --base, --remote, --project. --base, --remote, --allow-xdebug, --allow-destructive and --allow-remote-write change which commands run. --checks lint,profiler narrows the run to the items that exercise one of those checks: an item that declares no check is not check-specific and always runs, and an item the selection leaves out stays in the report as a skipped row naming the selection. P3-15 runs audit run --checks integrity and declares that check, so a lint or profiler selection leaves it out. Only the names audit run --checks accepts are valid — lint, profiler, integrity — so --checks lints is a usage error (exit 2) rather than an ignored flag that selects nothing and still reports passed. --lint-jobs N and --timeout <value> are threaded into the argv of the items that run the lint check — P3-10, P3-11, P3-13, P3-14 and P5-04 — and every one of those items also records the resolved argv in its evidence excerpt. P3-12 runs the profiler, so it takes neither flag. --base <ref> is the base of P3-11's diff scope and always wins; without it the row uses the first of origin/master, origin/main, master and main that exists in the project root, and skips with a reason naming --base when none does, because audit run --scope diff silently widens to the whole project when its base ref is missing. An empty --checks value is unset rather than a selection: verify --checks "" runs everything, while audit run --checks "" means lint. At their flag defaults (4, auto) verify treats them as unset: --lint-jobs is not forwarded, so audit run keeps its own engine.AuditRunJobs() worker count rather than verify's 4 overriding a host's auto-tuning — which makes --lint-jobs 4 the one request that cannot be expressed, and a value below 1 is treated as unset too instead of being forwarded for the child to reject — and --timeout falls back to the item's own value: auto for the phase-3 lint items, 0 (no deadline) for the phase-5 --no-lint-result-cache re-lint. A worker count above what the project's framework can honour is the child's call: audit run rejects it, so the item goes red with the child's own message rather than the flag being silently ignored.

Framework items: a framework declares its own checklist entries in its own package — VerifyToolItems on the framework definition names an id, a phase, a title and one govard tool <binary> <args> invocation — and RegistryFor composes them with the static registry, so internal/verify names no framework. Magento 2 declares P5-MAG-01 (setup:db:status after restore, the read-only detector P5-07 lacks); Laravel P3-LAR-01..03 + P5-LAR-01; Symfony P3-SYM-01..03 + P5-SYM-01; WordPress P3-WP-01..03 + P5-WP-01. Only commands the framework skeleton guarantees are declared, so an item is never permanently red for a missing optional bundle or plugin.

Remote items: every item that names a remote — P2-04..P2-08, P2-15, P4-01, P4-03..P4-07, P4-13..P4-15, P4-17 and P4-18 — takes it from --remote and nothing else. A run without --remote skips those rows with no --remote named: this item contacts a remote instead of guessing, so the checklist never opens a session to whatever the project happens to call staging/stage/stg; P4-16 (remote list) names no remote and keeps running. P4-13..P4-16 cover the read-only half of the remote surface — deploy plan, deploy status, deploy releases and remote list — which is safe to run against a production remote. The checklist is a preflight, so what writes stays out of it by design. deploy check is run by hand rather than as a row: it leaves nothing behind on the target, because its writability probe creates no path at all and tests the nearest existing parent instead, and the mv -T probe it runs on a symlink target creates a .dep scratch directory there (and, on a fresh host, the missing levels of the deploy path above it) and removes them again. deploy unlock/deploy rollback mutate the target's lock and release state, db/snapshot/open -e mutate a remote target (the only command that still offers an authorized_keys write to reach one is govard remote test, and that offer answers No unless you say yes; key setup is otherwise the explicit govard remote copy-id <remote>), and tunnel stop signals only the one tunnel govard recorded for the project, refusing any pid whose argv it cannot confirm it started.

Probe items: P2-13 and P4-11 dial the project's own site, so a project with no configured domain cannot be probed at all — they skip with no configured domain rather than guess localhost and report on some other environment. P3-12 has the same rule for the profiler URL: without a domain it skips with no domain configured instead of auditing a guessed host. P2-13 prefers https://<domain>/ and falls back to plain HTTP only when the TLS layer itself is unusable (an untrusted local CA, or a port that answers in clear text), and its evidence names the scheme it used; P4-11 wants a search-health payload with a real status, so an answer with no body is a failure, not a green.

Row verdicts: a row reports only what it ran. P1-06 (lock check, with lock diff for evidence on a failure) skips with a reason that names govard lock generate instead of generating a tracked file from a verify phase, while a lock that exists and disagrees with the project stays red; P3-09 (frontend start) skips on Magento 2 unless stack.features.frontend_sync is on, and the skip reason names that switch. Each item also carries a Requires note (what it assumes, such as P2-01 up) for people reading the registry: the runner never enforces it, and no skip reason quotes it.

Gates: Phase 5 requires a snapshot of this project — a real (non---plan) phase-4 run for the same project whose P4-08 exited 0 and whose recorded snapshot is still usable on disk — AND --allow-destructive. Without it → need snapshot create (P4-08) first; without the flag → need --allow-destructive for phase 5. A --plan run satisfies neither gate, runs nothing and changes no project state (it still writes a mode: "plan" artifact, which never satisfies a gate). Every gate is checked before any item runs, in human and --json mode alike: verify with neither --phase nor --allow-destructive exits 1 with the gate error and runs nothing (no environment is stopped and no snapshot is created), --phase 0 is gated like phase 5, and the --allow-destructive opt-in is checked before the snapshot gate. In an all-phases run the snapshot gate is checked when phase 5 is reached, because phase 4 creates the snapshot in that same run. A refusal before anything ran prints {"error": "<reason>"} on stdout under --json. When phase 5 refuses to start after phases 1 to 4 ran, an all-phases --json run still prints one document: the merged phase 1-4 result with the reason in an error string (an error field never appears in a run artifact), and the exit code is 1.

The run artifact is written whether or not --json is set, because --json only shapes stdout: verify --phase 4 followed by verify --phase 5 --allow-destructive works without --json. A runs directory that cannot be written costs the run nothing: the verdict is kept and a warning goes to stderr. The one place that matters is an all-phases run whose phase 4 record could not be written, since phase 5 then has no snapshot record to read; it is refused with phase 4 result could not be recorded: <cause>; phase 5 needs it (verify.ErrRunNotRecorded) rather than a bare need snapshot create message. P4-08 records the snapshot name it created and P5-05 restores exactly that name, so the restore cannot pick up a different snapshot created in between. A row marked skipped never satisfies the gate: it records that P4-08 exists, not that it ran.

Guard: every static item carries one of four labels (Item.Guard). The runner acts on DESTRUCTIVE-LOCAL and REMOTE-WRITE; REMOTE-PROBE is a label for the reader, not a protection, and plan mode is its only gate.

GuardWhat the argv claimsRunner actionItems
""local work, no remote, nothing irreversiblerun53
REMOTE-PROBEnames a remote, its argv writes nothing thererun when --remote names one; in a run without it the item keeps its row as a skip and does not probe a guessed remote (--plan still stubs it, and P4-16 names no remote so it always runs). No guard-level gate — --plan already replaces every item with a stub, so blocking these there would delete their coverage instead of protecting anything. On a live run the item reaches the remote unconditionally: the name says "probe" so nobody reads it as a guarantee15
DESTRUCTIVE-LOCALirreversible local destructionphase 5 only, on top of the snapshot gate and --allow-destructive3
REMOTE-WRITEwrites through a remoteskip unless --allow-remote-write is passed3: P2-05, P2-08, P2-15

P2-15 (deploy --remote <remote> --yes --force) is the end-to-end rehearsal against a sandbox remote: --yes removes its prompt, so with --allow-remote-write it runs unattended, and --force makes a rerun deploy again instead of exiting 0 with already runs <rev>; nothing to do. P4-17 (deploy check) and P4-18 (remote exec <remote> -- hostname) are probes that leave nothing behind on the target; P4-19 (remote audit stats) reads the local audit log, and P4-20/P4-21 run only the help of deploy rollback and deploy unlock, which mutate the target. P1-08..P1-15 are read-only host and project rows (capabilities --json, whose output must be valid JSON, config profile, domain list, custom list, project orphans, gateway status, audit toolchain status, sandbox status).

Not-applicable rows skip with a reason instead of failing: P4-10 and P4-11 need stack.services.cache / stack.services.search to name a service, the lint, profiler and integrity audit rows (P3-10..P3-15, P5-04) follow what the framework definition declares, P2-12 (tool composer validate --no-check-publish, so a project skeleton without a package name is judged on manifest and lock consistency only) needs a composer.json and P3-07 a vendor/bin/phpstan in the project root, and P1-03 (doctor trust, which needs sudo and has no terminal to ask on) needs passwordless sudo. P2-08 (the clone bootstrap) copies from the remote's live release, so it skips with a reason when deploy releases --remote <remote> --json reports that the remote has no release yet (run P2-15 first).

The two original REMOTE-WRITE items are the bootstrap … --no-noise runs. Their skip reason names the manual command, with <remote> standing for the remote you must name with --remote, and they run bootstrap -y: verify's children have no tty, so without -y the command would stop at its confirmation prompt and exit 1. With --allow-remote-write they therefore run unattended and really write through the remote. They also change the local project: a bootstrap rewrites the local framework configuration (on Magento 2, app/etc/env.php with the local database, cache and search settings), runs composer install (a clone bootstrap wipes vendor/ first), and fixes ownership and permissions of the project directories, which can change file modes in the checkout. Those rows need that configuration to do their job, so verify does not restore it: run phase 2 with --allow-remote-write on a disposable checkout, or review git status afterwards. A clone bootstrap does not copy the remote's .dep/ deploy record, and govard.lock is written only by govard lock generate (P5-01) and env up --update-lock, never by phase 2; it is a lock file meant to be committed, so no ignore rule is added for it.

Exit codes: 0 every item passed or was skipped; 1 any item failed or a phase gate blocked the run (the message names which). 2/3/4 keep their global meanings (usage / capability / config) — a failing checklist is an execution failure, never a usage error. Scripts should branch on this code and read per-item detail from --json.

Outputs: <govard home>/verify-runs/<project-id>/<ISO>-phaseN.json, where project-id is a hash of the canonical project path, with {govard_version, project_sha, project_id, phase, mode, status, items:[{id, command, duration_ms, exit_code, evidence_excerpt, json_valid, artifacts, skipped, skip_reason}]}. The per-item retries key of earlier releases is gone: it was always 0 and nothing read it, so a consumer that read it now sees it absent. evidence_excerpt is capped at 500 bytes: a passing item keeps the head, a failing one keeps head and tail around a [... output truncated ...] marker so the real error survives, and command names the resolved --base for P3-11 (<no base ref found> when none resolves). mode is run or plan; status is passed or failed; a run whose evidence no process produced carries fake: true on the document and on those items (the hermetic hook: whenever the binary that would run the item is a Go test binary it answers with a fake(test-binary): excerpt and no env var is needed, and GOVARD_VERIFY_FAKE=1 only changes that prefix to fake:; for any other binary both are ignored, so a stray GOVARD_VERIFY_FAKE=1 changes nothing in a real run), and status still says passed or failed; artifacts names what an item produced (the snapshot P4-08 created, which P5-05 restores). skipped marks an item that exists but did not apply in this run — a framework-gated item, for example — and skip_reason says why; the human table prints SKIP for it, it is counted in neither the passed nor the failed tally, and it never makes a phase red or changes the exit code, so a phase of nothing but skips stays passed and exits 0. Legacy ~/.govard/checklist-runs/ is migrated on first run; artifacts written before project scoping (flat in the verify-runs/ root) carry no project identity and never satisfy the gate. phase 0/all --json emits a single JSON with phase: "all", a recomputed status and combined items.

GOVARD_VERIFY_BIN pins the binary the checklist items execute. Unset, the invoked executable runs them, and PATH is only the last resort. To validate a source build, run that build directly and set this variable if the build is not the one the shell would find — otherwise a bare govard on PATH may be an older install and the checklist will report on it, not on your build.

Framework detection falls back to engine.DetectFramework when .govard.yml is missing (so fresh artisan/composer.json projects are detected as laravel etc.).

govard desktop ​

Launch the Wails desktop app.

bash
govard desktop
govard desktop --dev
govard desktop --background

See Desktop App for details.


🛠️ Development Commands ​

govard shell ​

Open a shell in the application container.

bash
govard shell
govard shell --no-tty
  • PHP frameworks → php container at /var/www/html
  • Node-first frameworks (Next.js, Emdash) → web container at /app

PHP containers intentionally do not include Node.js. Use govard tool npm … for Node package commands.

govard debug ​

Manage Xdebug status and sessions.

bash
govard debug status
govard debug on
govard debug off
govard debug shell

Requests route to php-debug only when the XDEBUG_SESSION cookie matches stack.xdebug_session.

govard test ​

Run project test tools inside the application container.

bash
govard test phpunit
govard test phpstan
govard test mftf
govard test unit
govard test integration

unit runs phpunit as unit alias; phpstan falls back to --level=0 with app/code+app/design (Magento 2) or app+src when no project phpstan.neon exists.

govard custom ​

Run custom commands from .govard/commands or ~/.govard/commands.

bash
govard custom list
govard custom hello
govard custom deploy -- --dry-run

govard project ​

Browse and manage known projects.

project list prints a Status column (running, stopped, or unknown) and lists running projects first; the rest keep registry order. It does not require Docker: when the container runtime cannot be reached, every status reads unknown and the registry order is kept.

bash
govard project list
govard project list --orphans
govard project open billing
govard project delete demo
govard project delete --yes demo

CAUTION

govard project delete removes persistent database volumes by default. Project source code is never deleted. It also removes the project's compose file and .hash, its varnish/rabbitmq/nginx/apache directories, its active-projects.json entry, its frontend containers and network, and its sandbox, and it finds a project that is down or no longer in the registry by exact name or path.

Deletion process:

  1. Runs pre-delete lifecycle hooks
  2. Executes docker compose down -v (removes containers + volumes)
  3. Unregisters proxy domains
  4. Removes project from registry (projects.json)
  5. Runs post-delete hooks

🔗 Remote, Sync, and Data Commands ​

govard remote ​

Manage named remotes for sync, deploy, shell, and database workflows.

bash
govard remote add staging --host staging.example.com --user deploy --path /var/www/app
govard remote copy-id staging                  # the only command that copies a public key
govard remote test staging
govard remote exec staging -- ls -la
govard remote list                        # configured remotes, plus the implicit sandbox row
govard remote audit tail --status failure --lines 50

For home-relative remote paths, quote the value:

bash
govard remote add staging --host staging.example.com --user deploy --path '~/public_html'

Key features:

  • Capabilities: files, media, db, deploy
  • Auth methods: keychain, ssh-agent, keyfile
  • Production write protection by default
  • Audit logs: ~/.govard/remote.log

remote exec runs whatever shell command you give it on the remote, and it sits outside the write-protection gate: nothing inspects the command, protected remotes included, so the operator owns what is run. The help example is read-only on purpose (govard remote exec staging -- "df -h /var/www"). remote exec, remote test and remote copy-id take an alias of a configured remote (stg, STAGE) and resolve it to the configured name first, so the key and auth settings stored for that remote are the ones used and the audit events carry the configured name.

remote add validates before it writes anything: an invalid name or an out-of-range --port (negative or above 65535) exits 4 with the config file and the auth store untouched, and a config file that cannot be written exits non-zero instead of printing a success line. The same holds for config set, domain add|remove, profile apply and debug on|off: a resulting config that fails validation exits 4 and leaves .govard.yml as it was, a failed write is an error, and debug on|off do not start the environment after a failed save.

remote list prints a NAME/HOST/CAPABILITIES/AUTH/KEY table over the configured remotes plus the synthetic sandbox (implicit) row, whose host column carries the state (running, dormant — …, absent — …) and whose capabilities column carries what a remotes.sandbox block configures. When a remotes.sandbox block is configured the sandbox still gets exactly one row, and stderr reports what the block does: its capabilities, protected and deploy settings are layered over the synthetic sandbox, while host, port, user and auth stay container-derived.

→ Full guide: Remotes and Sync

govard sync ​

Synchronize files, media, or databases between local and named remotes.

bash
govard sync --source staging --destination local --full --plan
govard sync --from staging --to local --media
govard sync -s prod --file --path app/etc/config.php
govard sync --db --no-noise --no-pii

Auto-selects staging remote if no --source is provided, falling back to dev. When --media is used without a mode, Govard defaults it to optimized.

sandbox is accepted for --source and --destination and resolves from the live container — the same remote govard deploy --remote sandbox uses — so no remotes.sandbox block is required, and a configured block can only shape the rehearsal (capabilities, protection, deploy settings), never repoint the transfer. A Docker-less host asked for -e sandbox exits 3 with CAPABILITY_MISSING; with Docker but no container it exits 1 naming govard sandbox up as the remedy.

sync needs a container runtime only for the database scope: --db and --full move the database through the local database container, so without one they exit 3 with CAPABILITY_MISSING before any remote is contacted, while --plan is exempt and a files or media sync needs no Docker. --plan and the confirmation summary show the database password as export MYSQL_PWD=***; (or export PGPASSWORD=***; for PostgreSQL) in place of the value; those lines are display text, and the command that runs still carries the real one.

Key flags:

FlagEffect
-s, --source / --fromSource environment
-d, --destination / --toDestination environment
--file, --media, --db, --fullScope selection
--planPrint plan and exit
-I, --includeRsync include pattern (repeatable)
-X, --excludeRsync exclude pattern (repeatable)
-m, --media [mode]Media sync scope (none, minimal, optimized, catalog (Magento only), all); bare --media defaults to optimized
-N, --no-noiseExclude ephemeral data
-P, --no-piiExclude sensitive data; refused when the table prefix is unknown, so it never yields an unfiltered dump
--resolve-symlinksOn a pull, copy the content of symlinks that point outside the synced path (default: such links are not followed)

govard db ​

Database utilities for local and remote-backed workflows.

bash
govard db connect
govard db dump
govard db dump -e staging --local
govard db query "SELECT COUNT(*) FROM sales_order"
govard db info
govard db top
govard db import --file backup.sql --drop
govard db import --stream-db -e staging --drop
govard db clone-volume warden_magento2_dbdata

A dump file is private to its owner. db dump and db import --stream-db create the local file with mode 0600, and an existing file that is broader is tightened to 0600 before anything is written to it. db dump -e <remote> without --local runs under umask 077 on the remote, so the dump is 0600 and any directory it creates (such as ~/backup) is 0700.

govard deploy ​

Deploy a git revision to a remote environment.

bash
govard deploy <remote>                   # deploy the local HEAD
govard deploy staging --revision <sha>   # deploy an exact commit (CI)
govard deploy build staging --output artifacts --revision <sha>   # build an artifact (CI)
govard deploy staging --artifact-dir artifacts --revision <sha>   # deploy that artifact
govard deploy plan staging               # print the plan, connect nowhere
govard deploy check staging              # preflight and report the publish strategy
govard deploy releases staging           # list the releases on the target
govard deploy status                     # what every environment is serving
govard deploy rollback staging           # put the previous release back
govard deploy rollback staging --to 12   # ... or a named one
govard deploy rollback staging --with-db --yes   # ... and its database dump
govard deploy unlock staging --force     # release a lock a failed run left behind

Managing the local sandbox — a container that plays the deployment target — is the top-level govard sandbox command, documented below:

bash
govard sandbox up --profile full --php 8.4   # a database, a cache, PHP 8.4
govard sandbox up --profile full --db mariadb:10.6   # choose the database series
govard sandbox status
govard sandbox down [--purge] [--volumes]

→ Worked configurations (Luma, Hyvä, several themes and store views, developer and production mode, symlinked and real webroots): Deployment case studies.

The target is a remote from .govard.yml. The branch, repository, deploy path and publish strategy come from the project deploy: block; a remote overrides them either through the topology fields on the remote (branch, repository, deploy_path, publish, local) or through remotes.<name>.deploy.<key> for keys of the deploy: block. Flags win over both.

deploy_path has no default and does not get one: a remote that omits it gets the layout the target already has (releases/, shared/, .dep/ or a current symlink), adopted only when exactly one candidate matches, and govard says which one it used. No layout, or several, is a configuration error (exit 4) naming what was probed. A configured deploy_path is never probed.

deploy:verify runs the live-revision check, the recipe's required shared files, the recipe's own checks — for Magento, setup:db:status, run in the served path so an in-place target checks the docroot rather than the release — and an HTTP request when deploy.verify.url is set. On an in-place target it also dry-runs the activation's own copy for every sync_paths entry, which is what proves the docroot holds what the build produced. A deploy that runs db:migrate with no verify URL prints a warning before its first step. maintenance:enable/disable are skipped for a symlink activation unless the plan also migrates or imports configuration, which is when a maintenance window is genuinely needed.

The pipeline is a fixed sequence of neutral tasks. A project customises it by anchoring hooks on a task id, on a stage alias (stage:build) or on another hook:

yaml
deploy:
  hooks:
    - { name: apache-reload, on: "publish:activate", position: after, order: 10, run: "touch ~/apache-reload" }

A framework contributes its own recipe rather than govard branching on a framework name: magento2 fills the build and publish tasks with Magento commands, and a framework without a recipe still deploys code through the neutral pipeline. Every step a recipe leaves empty is reported as skipped, not as a failure.

Flags: --remote, --branch, --revision, --tag (mutually exclusive), --build=auto|server|artifact, --artifact-dir, --publish=auto|symlink|in_place, --keep, --verify/--no-verify, --db-backup/--no-db-backup, --lock/--no-lock, --ignore-deployer-lock, --command-timeout, --resume, --from <task>, --force, --yes, --json, --verbose (stream each command's output live, indented under its task; a no-op with --json).

Build modes. --build=auto (the default) resolves by presence, never by sniffing the environment: an artifact directory — --artifact-dir <dir> or deploy.artifact_dir in the project — means the build already happened, so the mode is artifact; otherwise it is server. --build=artifact with no artifact directory is a usage error rather than a silent fall back to building on the server, which is the outcome the mode exists to avoid.

The two-job CI shape. The point of the artifact mode is that the job which touches production needs no toolchain:

JobImage needsCommand
buildPHP, Composer, Node — whatever the recipe's build tasks needgovard deploy build production --output artifacts --revision $CI_COMMIT_SHA
deploygovard, ssh, rsync — nothing elsegovard deploy production --artifact-dir artifacts --revision $CI_COMMIT_SHA --yes

govard deploy build materialises the revision into the output directory, runs the recipe's build tasks there, and writes a manifest.json recording the revision, the PHP version, the composer.lock hash and a sha256 list of every file. It needs no target and no container runtime. The deploy job verifies the manifest against the revision it is deploying and compares the recorded PHP version with the target's, refusing with an actionable message on a mismatch — so a CI image that does not match the server is caught before anything is published. An artifact deploy skips the five build tasks and runs deploy:artifact instead; govard deploy plan shows which branch is in effect.

An output directory that is not empty is refused, so a stale file from an earlier build cannot ship: pass --force to replace its contents.

govard deploy build flags: --remote, --output (required), --branch, --revision, --tag, --force, --command-timeout, --json, --runner (host, the default, or container), --no-cache. It needs no capability at all: none. Only the sandbox remote caches its artifact (.govard/sandbox/build-cache, keyed by commit, composer.lock hash, PHP series, mode, effective deploy settings, hooks and recipe commands, the builder's tool versions, and the govard binary); --no-cache forces a rebuild there and is accepted and ignored for every other remote. --runner container is the one flag that demands anything — the project's own app container, exit 3 without it — and it also requires --output to sit inside the project root, so the container can reach the artifact it builds; an output anywhere else is refused before Docker is even looked for, as a configuration error (exit 4).

The container carries only the app container's own toolchain, so the build mixes runners when it has to, and it checks before it changes anything. When the recipe's frontend step will run (deploy.settings.frontend_dir names at least one directory), the build looks for node and npm in the container before the first task. A container that has both keeps the step. One that lacks either runs the frontend step on the host (which then needs node and npm; the PHP and Composer steps stay in the container), the timeline prints a runner: line per step (for example runner: host (node not in container)), and the manifest records frontend_runner beside the container's php_version. Only when neither side has Node does it refuse with exit 3, naming what each side lacks and the way out (install Node in the container or on the host, since --runner host needs it on the host too; or, for a step that uses the placeholder below, an empty frontend_dir). The --error-json envelope of that refusal reports capability: "node", a value govard capabilities does not list because it is not a declarable capability. The check keys on the recipe's {{settings.frontend_dir_args}} placeholder, so a project whose frontend step (a recipe override or a hook-shaped command) does not use it is still checked for Node, and an empty frontend_dir does not skip that. A project container that is stopped or missing also exits 3 (hint: govard env up, or --runner host). Both refusals come before the output directory is touched, so a previous artifact survives. Ctrl-C and --command-timeout stop a step inside the container too: after the local docker exec client is swept, a second docker exec signals the step's process group, and the error says whether that teardown was attempted and whether it completed.

Settings and credentials. deploy.settings is validated against the recipe before anything runs: an unknown key, or a value with the wrong shape, exits 4 with the key named and the near miss suggested. String settings must be quoted if they look numeric — the engine reads them as strings. COMPOSER_AUTH from the environment is forwarded to the dependency step on its standard input (never in the command), and a shared/auth.json on the target works too; govard deploy check reports which source is in play and warns when a target-side build will need one and there is none.

deploy check. The preflight leaves nothing behind on the target, local or remote. Its writability probe creates no path: an absent deploy_path is tested at its nearest existing parent, on the target itself, and a local target's note names the parent that answered (a remote call gets no note, because an exit code cannot tell an absent path from an unwritable one). The mv -T probe it runs on a symlink target creates a .dep scratch directory under the deploy path, and on a fresh host the missing levels above it, then removes them again, stopping at the nearest ancestor that already existed and keeping any level that holds something. The preflight never looks at the deploy lock. Its notes print before the Target <remote> is deployable header, one per line, marked - for a fact and ! for a warning, and a real deploy prints the same notes in the output of its deploy:check step (on stderr under --json, so stdout stays one document).

Machine-readable output. With --json, stdout carries exactly one JSON document and the human timeline goes to stderr: schema_version, remote, branch, revision, release, build.mode, publish.strategy, publish.previous_release, verify, result, duration_ms and tasks[{id,stage,status,duration_ms}]. A failed deploy emits the same document with result: "failed" and error, and exits 1. The release record carries ci.pipeline/ci.job when the run is a CI run.

govard deploy plan --json emits a different document — kind: "plan", so a pipeline consuming both tells them apart by field rather than by shape — that describes what the run would do: schema_version, kind, remote, branch, revision, build.mode, publish.strategy with publish.decided_by (configuration, or target while the strategy is auto and only a connection could resolve it), and steps[{index,id,kind,stage,title,run_on,source,implementation,command,skipped,skip_reason,needs_application}]. implementation is engine, command or none; skipped mirrors the executor, so both a step the build mode skips and a task no recipe implements are skipped, each carrying its skip_reason. run_on is the fact the human tree cannot show: a hook declared run_on: local looks like every other step there. The document has no timestamp, so two runs of the same plan are byte-identical and a CI job can diff them. Nothing connects: deploy plan needs no ssh, no rsync and no Docker.

deploy.lock_stale_after (2h) and deploy.maintenance_timeout (15m) are the two timeouts beyond command_timeout: the first is how old a lock must be for deploy unlock to release it without --force, the second bounds one step inside the maintenance window.

The sandbox. govard sandbox up builds a container, publishes SSH on a free loopback port, generates a dedicated key under .govard/sandbox/ (gitignored), and mounts a read-only mirror of your local repository. The mirror is refreshed before every deploy, so a commit you have never pushed is deployable, and nothing in the pipeline knows it is talking to a container — a sandbox deploy is a production deploy pointed at one. While the container runs, sandbox resolves automatically as a remote for every command that takes one, with no identity ever written to a configuration file.

sandbox is a top-level command — deploy to it with the flag form: govard deploy --remote sandbox --yes.

There is no sandbox identity to write anywhere: host, port, user, path and auth for sandbox are read from the container govard sandbox up created, and govard remote add ignores them if you pass them. What you may configure is the rehearsal's shape — capabilities, protected and deploy settings — through govard remote add sandbox --capabilities db --protected or by hand in .govard.yml / .govard.local.yml:

yaml
remotes:
  sandbox:
    capabilities:
      db: false
    protected: true
    deploy:
      keep_releases: 3

The block is layered over the synthetic remote, never instead of it. Four deploy settings are pinned to the container because they describe what the image shipped and a deploy refuses to run when the target disagrees: owner, writable_mode, php_bin and php_version. The rest of what a per-remote override copies is yours to set — keep_releases, command_timeout, artifact_dir, repository, branch, publish, deploy_path, db_backup, verify.url, verify.timeout, hooks and every settings key (writable_permissions and composer_bin among them). deploy_path is the one that moves the rehearsal, and it only moves it inside the container. lock_stale_after, maintenance_timeout and verify.follow_redirects are project-level only: a remotes.sandbox block is ignored for them, so put them in the project-level deploy: block.

Profiles: basic (sshd, rsync, git), php (adds php-cli, composer, node) and full (adds a database and a cache), defaulting to php. --php <series> picks the PHP series the image provides (--php 8.4); without it a new sandbox is built for the project's stack.php_version when it names a series, and for the base distribution's version otherwise, while an existing sandbox keeps the series it ships. The series moves the php binary, its extensions, the remote's php_bin and the php_version the remote declares, and it is part of the image tag — so a project that needs a newer PHP than the base image carries is rehearsed against the right interpreter instead of failing mid-install. --docroot shapes the target so the publish strategy resolves the way you want to exercise it: absent or symlink (the default) selects the atomic swap, real selects in-place publishing. down removes the container — the implicit sandbox remote exists only while it does — keeping every data volume so a rehearsal resumes (--volumes deletes the derived volumes too); --purge also removes the image, the key and the mirror. reset wipes the target's deploy directories, and --layout=deployer seeds a target that looks like one the other deploy tool owns.

The php and full profiles also ship a web tier — nginx serving the served path plus the project's stack.web_root (/pub for Magento), and PHP-FPM running as the deploy user. up points the sandbox remote's deploy.verify.url at that port, so a sandbox deploy rehearses the HTTP half of deploy:verify; basic has no web tier and advertises no URL. The framework recipe contributes what its own commands need beyond the profile (for Magento: build libraries, PHP extensions and the database and cache services). A project that needs one more extension, an apt package, a service the image does not start, or a binary the engine knows how to install adds it to the matching deploy.settings.sandbox_* list — sandbox_packages, sandbox_extensions, sandbox_services, sandbox_tools — never to a flag. Each list replaces the recipe's rather than extending it, so a project on PostgreSQL names postgresql instead of mariadb, not in addition to it; the per-recipe defaults are listed under Deployment.

The sandbox is the only deploy command that needs docker.

--resume continues the newest release whose record is not ok; --from <task> starts at a named task or hook and reports everything before it as skipped. Both are recovery paths an operator asks for explicitly — neither is automatic. A --from past deploy:release also needs --resume: the run would never create a release number, so would name the directory holding every release.

govard deploy rollback never rebuilds: a symlink layout is re-pointed, and an in-place layout re-runs the publish tail from the release directory already on the server. --with-db restores the dump recorded by the release that ran after the one being restored — that deploy dumped the database before its migrations, which is the state the target expects — and destroys current data, so it needs --yes (or an interactive confirmation). The restore runs before the cache flush and the verification, which both read the database.

Exit codes: 0 success, 1 execution failure, 2 usage, 3 missing capability, 4 configuration. govard deploy and govard deploy rollback need ssh and rsync; deploy check, deploy releases, deploy status and deploy unlock need only ssh; deploy build and deploy plan need nothing. govard sandbox * is the exception: creating the fake server needs docker, and then govard talks to it over SSH like any other target.

deploy check and deploy status share the configuration and usage codes. A mistyped deploy.settings key, a project that cannot be loaded, or a project with no remotes is a configuration error (4); a positional remote that contradicts --remote is a usage error (2), as is a deploy check with no remote named. Exit 1 differs: for check it means the preflight failed or the named target could not be reached, for status it means no configured remote could be reached. deploy status --json still prints the array of per-remote rows (an unreachable row has status unknown and an error) but exits 1 when every remote is unreachable, as table mode does, with the reason on stderr and stdout still one document under --error-json. With no remotes configured, or a project that cannot be loaded, it exits 4 with an empty stdout.

govard sandbox ​

A container on your machine that plays the deployment target — the top-level lifecycle for the rehearsal target:

bash
govard sandbox up                      # create it (php profile by default)
govard sandbox up --profile basic      # sshd, rsync and git only
govard sandbox up --profile full --php 8.4   # a database, a cache, PHP 8.4
govard sandbox up --profile full --db mariadb:10.6   # choose the database series
govard sandbox up --docroot real       # a real docroot: in-place publishing
govard sandbox status
govard sandbox reset --layout deployer # seed a target the other tool owns
govard sandbox ssh                     # interactive shell
govard sandbox ssh -- php -v           # run one command
govard sandbox down [--purge] [--volumes]

sandbox ssh -- <command> runs the command in the sandbox without a forced tty, so piped input works (printf x | govard sandbox ssh -- 'cat > f'), and govard exits with the command's own exit status, because the process is replaced by ssh. This is deliberate for this command only: deploy steps still never leak a remote exit code (they exit 1 and keep the status in the message). The words after -- are joined with spaces into one remote command line, so quote a whole line as one argument; a command without -- is a usage error (exit 2). A bare sandbox ssh opens the interactive shell.

While the container runs, sandbox resolves automatically as a remote for every command that takes one (deploy, db, remote exec, sync): govard deploy --remote sandbox --yes, govard sync -e sandbox, govard remote exec sandbox -- <command>. No identity is written to any configuration file, and govard remote list shows the synthetic sandbox (implicit) | … row next to the configured remotes. The rehearsal's shape is configurable — see Configuring the sandbox remote. On a fresh default (symlink) sandbox, remote exec fails until the first deploy populates the current path — run the first deploy or use --docroot real.

--db picks the database the full profile provides, mariadb:<series> (for example mariadb:10.6) or default for the base distribution's own server. For a new sandbox the default comes from the project's stack.services.db and stack.db_version; an existing sandbox keeps the database it ships, and a disagreeing --db is refused unless you pass --recreate. The --recreate path validates the database first, so a refused one leaves the working container in place. The sandbox up summary prints a database: line (between php: and image:) naming the installed server and whether it was requested or is the base distribution's default; profiles without a database omit it.

The full profile keeps its database in the named volume <sandbox container name>-db: down keeps it, down --purge removes it, and a volume written by another database series is refused with a --purge hint. up seeds an empty database only; --reseed refreshes the database and the files from the origin (refused together with --no-seed), and --recreate keeps the volume.

→ Full guide: Deployment and its fast loop.

govard gateway ​

The shared SSH bastion (govard-proxy-sshd) started by govard svc up, listening on 127.0.0.1:2222 — one stable address for every sandbox instead of the ephemeral port each sandbox up picks:

bash
govard gateway allow-key "$(cat ~/.ssh/id_ed25519.pub)"
ssh -p 2222 <project-name>@127.0.0.1
sftp -P 2222 <project-name>@127.0.0.1
  • govard gateway status reports whether the bastion container is running and how many targets/keys it knows about.
  • govard gateway allow-key <public-key-line> / govard gateway revoke-key <fingerprint-or-comment> manage the allowlist; both work without Docker running (the registry is a local file).
  • sandbox up registers the project's username automatically and joins the bastion's network; sandbox down removes the registration. Neither operation fails if the gateway itself is not running — deploy, deploy check and remote * never go through it.

→ Full guide: Deployment.

govard snapshot ​

Manage local and remote snapshots for DB and media.

bash
govard snapshot create
govard snapshot create -e staging
govard snapshot list
govard snapshot list -e staging
govard snapshot restore latest
govard snapshot delete before-deploy
govard snapshot export latest ./backup.tar.gz
govard snapshot pull latest -e staging
govard snapshot push before-deploy -e prod

Subcommands: create, list, restore, delete, export, pull, push. restore overwrites existing data, so it asks for confirmation; pass -y/--yes to skip the prompt (required in non-interactive sessions). export writes a tar.gz archive locally; delete removes the named snapshot. pull/push transfer snapshots between local and a named remote (-e flag).

Snapshot files that hold a database dump are private to their owner: create writes db.sql.gz with mode 0600, and export creates the archive with mode 0600 (tightening a broader existing file). A remote create -e runs under umask 077, so the directories it creates, such as <path>/.govard/snapshots, are 0700 and the files in them 0600, and it finishes by writing the snapshot's metadata.yml (name, created_at, framework).

Where a remote snapshot lives: on a remote with a deploy layout (releases/, shared/ and a current link under the deploy path), create -e and push store it in <deploy path>/shared/.govard/snapshots, outside the served release and kept across release switches and pruning. The deploy path is remotes.<name>.deploy.deploy_path when set, otherwise the remote path (its parent when the path is the current link). A remote without that layout keeps the older location, <remote path>/.govard/snapshots, and create warns that it did. list, restore, delete and pull look in both locations (the deploy-layout one first), so snapshots made by older versions stay visible; list -e adds a LOCATION column (shared or legacy) and pull prints where it found the snapshot. delete removes a name from every location that holds it.

govard open ​

Open common browser targets.

bash
govard open app
govard open admin
govard open mail
govard open db
govard open db --pma
govard open db --client
govard open db -e staging

open admin opens /admin for frameworks without a stock admin route. For Laravel and Symfony, which ship no admin panel, it prints one notice line saying so and which path it opened; WordPress opens /wp-admin. There is no admin-path configuration key.

open db prints the connection URL with the password masked as *** (no password section when none is set); the database client is handed the full URL.

govard tunnel ​

Manage public tunnels (start requires cloudflared; stop and status do not). Govard registers the tunnel domain in Caddy as an alias, keeps the original Host header intact, and rewrites the framework base URL (Magento, Laravel, etc.) for the session — restoring it on tunnel stop or Ctrl+C.

bash
govard tunnel start
govard tunnel start https://my-tunnel.trycloudflare.com
govard tunnel start --provider cloudflare --no-tls-verify --plan
govard tunnel status
govard tunnel stop
FlagEffect
[url]Optional tunnel URL to register (otherwise auto-detected from cloudflared output)
--provider <name>Tunnel provider (cloudflare is the only provider today)
--no-tls-verifySkip TLS verification for the tunnel endpoint
--planPrint the start plan and exit without launching

start records the process it launches (PID plus the argv it was started with) under $GOVARD_HOME_DIR/tunnels/<project>.pid, and refuses to start a second tunnel while that one is still live. The record is created exclusively, so two starts racing for the same project cannot both win: the loser stops the provider process it launched, leaves the winner's record and base URL alone and fails with the same refusal, while a record whose process is gone, or whose PID now runs something else, is replaced by the next start. stop and status read that record rather than searching the host, so neither reaches a cloudflared govard did not start — unless it is running the very command line the record names, which another copy of the same binary is indistinguishable from: stop signals the recorded PID only after its argv still matches, and refuses with an error — signalling nothing — when it does not or when the argv cannot be read. status reports INACTIVE for any process govard cannot claim, because "govard has no tunnel here" is true even when something else owns the PID. With no record, stop is a no-op that exits 0.

::: important IMPORTANT The cloudflared binary must be installed separately. Install via the official Cloudflare repository or GitHub releases. :::

→ Full guide: Tunnel (new)


🔧 Tool Commands ​

Run framework CLIs inside project containers:

bash
govard tool magento [command]    # Magento 2
govard tool magerun [command]    # Magento 1 / Magento 2 (Shortcut: mr)
govard tool artisan [command]    # Laravel
govard tool drush [command]      # Drupal
govard tool symfony [command]    # Symfony
govard tool shopware [command]   # Shopware
govard tool cake [command]       # CakePHP
govard tool wp [command]         # WordPress
govard tool prestashop [command] # PrestaShop
govard tool dagster [command]    # Dagster
govard tool manage [command]     # Django (python manage.py)

# General tools
govard tool composer [command]
govard tool php [command]        # Run the PHP CLI directly (e.g. editor/IDE integrations)
govard tool npm [command]
govard tool yarn [command]
govard tool npx [command]
govard tool pnpm [command]
govard tool grunt [command]

Node package commands (npm, npx, yarn, pnpm, and grunt) run in a one-shot node:<stack.node_version>-alpine container with the project mounted at /var/www/html. This keeps builds aligned with Govard's frontend watchers and BrowserSync. govard shell still opens the framework runtime container and does not expose Node.js for PHP frameworks.

govard tool php requires the current directory to be the project root. For editor/IDE integrations (see below), use govard vscode instead.


🧩 Editor Integration Commands ​

govard vscode setup ​

Writes (or merges into) the VSCode settings needed to run PHP tooling inside the project's container instead of the host:

bash
# Run from inside a project (or any subdirectory of one)
govard vscode setup
#   -> .vscode/settings.json: intelephense.environment.phpVersion, phpstan.paths,
#                             phpunit.paths (if vendor/bin/phpunit exists),
#                             and (if vendor/bin/phpcs exists) phpcs.standard + phpcs.autoConfigSearch=false
#   -> .vscode/launch.json:   a "Listen for Xdebug (Govard)" configuration (port 9003)

# Run once, applies to every Govard project
govard vscode setup --global
#   -> creates ~/.govard/bin/govard-php, govard-php-cs-fixer, and govard-phpcs wrapper scripts
#   -> user settings.json: php.validate.executablePath, phpstan.binCommand,
#                          php-cs-fixer.executablePath, phpcs.executablePath, phpunit.command

Project mode also tunes search and indexing for the framework. search.useIgnoreFiles and search.useParentIgnoreFiles are set to false, because VSCode otherwise honours .gitignore and hides a gitignored vendor/ from search and quick open. vendor/ is never excluded. The framework's generated and runtime directories are excluded instead, from search.exclude, files.watcherExclude and intelephense.files.exclude (for example var/ and pub/static for Magento 2 and Mage-OS, storage/framework and bootstrap/cache for Laravel, var/cache and public/build for Symfony, wp-content/uploads and wp-content/cache for WordPress). Magento's generated/code is hidden from search but stays indexed, so Factory and Interceptor classes still resolve. intelephense.files.exclude replaces Intelephense's default rather than extending it, so the defaults are repeated (this keeps nested vendor/**/vendor/** and vendor tests out of the index). Any active search.exclude or files.exclude entry that hides vendor/ itself (**/vendor, vendor/**, ...) is removed and reported, because quick open (Ctrl+P) honours both and would otherwise keep vendor/ hidden. These three settings are merged into what is already in the file, so entries you added yourself are kept. vendor/ stays in the file watcher so Intelephense notices composer update; on a very large project that hits the inotify limit, add **/vendor/** to files.watcherExclude yourself.

Settings reflect the project's last-used profile (e.g. an upgrade profile pinning a newer PHP version) if one is registered, rather than always the base .govard.yml — so intelephense.environment.phpVersion matches whatever's actually running.

The PHPCS coding standard is auto-detected from composer.json (magento/magento-coding-standard -> Magento2, wp-coding-standards/wpcs -> WordPress, drupal/coder -> Drupal), falling back to PSR12. phpcs.autoConfigSearch is disabled because it would otherwise auto-detect a phpcs.xml/.dist ruleset and pass its host absolute path as --standard, which the container can't read.

If vendor/bin/phpstan exists but the project has no phpstan.neon/.dist/dist.neon of its own, setup sets phpstan.options to a --level=0 default (--autoload-file=vendor/autoload.php plus app/code+app/design for Magento 2 or app+src otherwise — the same convention govard test phpstan already falls back to) so PHPStan has something to analyze. This intentionally lives in .vscode/settings.json, not a generated phpstan.neon at the project root — that file is normally git-tracked and not ours to create. As soon as the project gets its own config, re-running setup removes phpstan.options again so it can never override the project's real rules — the project's config always wins.

phpunit.command (recca0120.vscode-phpunit) needs no wrapper script — it's a template the extension tokenizes itself, so it's set directly to govard vscode phpunit ${phpunitargs}. This gives you the Testing sidebar (run/rerun individual tests) without installing PHPUnit on the host. Debugging an individual test through this extension isn't wired up yet — it would need Xdebug environment variables forwarded into the docker exec call.

Each setting group requires a specific VSCode extension (Intelephense, PHPStan, PHP CS Fixer, PHPCS, PHPUnit, PHP Debug). If one isn't installed, setup warns and asks whether to install it now via code --install-extension — accept and the corresponding setting is still wired up in that same run. Pass --yes to install everything missing without asking (useful for scripting); with no TTY attached and no --yes, missing extensions are skipped without prompting.

Gangway (ddtcorex.gangway, SFTP hotfix editing) is also ensured in both modes. It contributes no VSCode settings, so nothing is written for it — once it is installed, setup points at Gangway: Import Remotes from Govard in the Command Palette to import your Govard remotes.

Existing keys and unrelated launch.json configurations are preserved — only the keys Govard manages are added or overwritten. Note: settings.json is parsed as plain JSON, so any comments in it are dropped when rewritten.

govard vscode <tool> ​

The underlying tool runners that the settings written by setup point to:

bash
govard vscode php [args]
govard vscode composer [args]
govard vscode phpstan [args]       # vendor/bin/phpstan
govard vscode php-cs-fixer [args]  # vendor/bin/php-cs-fixer
govard vscode phpcs [args]         # vendor/bin/phpcs
govard vscode phpunit [args]       # vendor/bin/phpunit, with memory_limit=-1

Unlike govard tool, these resolve the project by walking up from the current directory to find the nearest .govard.yml — editors often invoke tooling with a working directory that isn't the workspace root (e.g. the active file's directory), so an exact cwd match isn't reliable.


⚙️ Configuration Commands ​

bash
govard config get stack.php_version
govard config set stack.php_version 8.4
govard config set table_prefix demo_
govard config get deploy.settings.php_bin     # what the deploy will expand
govard config set deploy.keep_releases 3
govard config profile              # Show recommended profile for current framework
govard config profile --json      # Output profile as JSON
govard config profile apply       # Apply recommended profile to .govard.yml
govard config auto                # Magento 2: inject settings into env.php

config auto runs Magento's config-repair step (app:config:import, falling back to setup:upgrade when the import was not enough) once a command reports it needs one. When that step leaves app/etc/config.php holding the same lines in a different order, Govard writes the original bytes back so the tracked file stays clean, retry after an unblocked read-only search index included. A genuine module change is left alone, and a restore it cannot write — a read-only checkout, or a config.php the container owns — is reported as a warning, not as a failed Magento command.

config get reads the deploy: block as well as the project and stack keys: deploy.keep_releases (the effective count), deploy.command_timeout, deploy.maintenance_timeout, deploy.lock_stale_after, deploy.artifact_dir, deploy.db_backup, deploy.verify.url, deploy.verify.timeout, and any key under deploy.settings. — including one the project never wrote, which reads as empty rather than as an unknown key. Lists come back comma-separated, and config set writes them back as lists; a setting that is a plain string stays one. config set refuses a value that cannot be the type it claims (deploy.keep_releases must be a whole number, deploy.db_backup true or false) rather than storing a zero. Whether a deploy.settings key exists at all is the recipe's answer, so a typo is reported by govard deploy plan, not here.

govard config profile ​

Display the recommended runtime profile for the detected framework.

bash
govard config profile
govard config profile --json

Output includes detected framework, recommended PHP version, database, cache, search, and other service configurations.

govard config profile switch ​

Switch to a different environment profile. Profiles allow running the same project with different runtime configurations (e.g., PHP 8.2 for production testing, PHP 8.3 for development).

bash
govard config profile switch upgrade
govard config profile switch staging
govard config profile switch          # Interactive selection

Profile files are stored as .govard.<name>.yml in the project root. The selected profile is persisted per-project in ~/.govard/projects.json.

After switching, run govard env up to apply the new environment. You'll be prompted for confirmation when a profile change requires restarting containers.

govard config profile clear ​

Reset to the default profile (no profile active).

bash
govard config profile clear

govard extensions ​

Initialize .govard/* extension scaffolding.

bash
govard extensions init
govard extensions init --force

govard blueprint cache ​

Manage the remote blueprint registry cache.

bash
govard blueprint cache list
govard blueprint cache clear

🩺 Diagnostics ​

govard doctor ​

Run startup diagnostics with actionable remediation.

bash
govard doctor
govard doctor --fix
govard doctor --json
govard doctor --pack
govard doctor trust

Checks include: Docker, Compose, ports, disk sanity, Govard home, compose directory health, SSH agent, and outbound connectivity.

  • --fix — Automatically detect and repair common issues
  • trust — Install Root CA into system trust store + browser NSS

🔁 Utility Commands ​

govard lock ​

Generate or validate govard.lock snapshots for environment drift detection.

bash
govard lock generate
govard lock check
govard lock diff
govard lock generate --file .govard/govard.lock

govard self-update ​

Download release artifacts, verify checksums, and replace binaries atomically.

bash
govard self-update                    # update within your current channel
govard self-update --channel beta     # opt into beta releases (persists)
govard self-update --channel stable   # switch back to stable (persists)
govard self-update --version v1.60.0-beta.1  # install one specific version

The update channel is remembered across runs (both the CLI and Govard Desktop read the same setting), so a plain govard self-update keeps following whichever channel you last selected.

govard upgrade ​

Native framework upgrade pipeline.

bash
govard upgrade --version 2.4.8-p4     # Magento 2
govard upgrade --version 11            # Laravel
govard upgrade --version 7             # Symfony
govard upgrade --version 6.7           # WordPress
govard upgrade --version 11 --dry-run  # Preview steps

Flags:

FlagEffect
--versionTarget version (required)
--dry-runShow steps without executing
--no-db-upgradeSkip DB migrations
--no-env-updateSkip profile update and container restart
-y, --yesAuto-confirm all prompts

govard version ​

bash
govard version

govard redis ​

Smart shortcut for Redis/Valkey management.

bash
govard redis cli
govard redis flush
govard redis info

govard varnish ​

Smart shortcut for Varnish management.

bash
govard varnish log
govard varnish ban /.*
govard varnish stats

govard rabbitmq ​

Smart shortcut for RabbitMQ management.

bash
govard rabbitmq status
govard rabbitmq queues
govard rabbitmq cli list_exchanges

govard valkey / govard elasticsearch / govard opensearch ​

Direct service shortcuts — proxies to the compose service of the same name. govard env intelligently proxies any other compose command, but these top-level shortcuts are registered explicitly:

bash
govard valkey cli
govard elasticsearch _cluster/health
govard opensearch _cluster/health
# Search queries take a single path and always run as GET; extra args are ignored.
# For arbitrary curl flags, use host-side curl against http://<your-domain>:9200 instead.

Host access for search is also routed automatically via http://<your-domain>:9200 (see Configuration) — the shortcuts above exec inside the container, while the :9200 route is for host-side curl/browser.


🌐 Global Flags ​

All commands support:

  • -h, --help — Show help
  • --verbose — Enable verbose structured logging
  • --error-json — Print failures as a machine-readable JSON envelope on stdout (schema_version, code, capability, command, message, hint) instead of text, so scripts never parse the human form

Commands that forward their arguments to a tool (govard tool php ..., govard redis cli ...) cannot parse --error-json; their failures still carry the documented exit codes.

Exit codes are stable across the CLI: 0 success, 1 execution failure, 2 usage error, 3 CAPABILITY_MISSING (a declared runtime requirement is unavailable), 4 configuration error. See Runs Without Docker for the commands that need no container runtime and what the gate does when something is missing.


← Getting Started | Configuration →

Released under the MIT License.