ddtcorex

23 Aug 2026 · 6 min read

Headless & classic WordPress with Govard

How Govard spins up a consistent local WordPress stack and pulls a live site down safely — classic or headless.

WordPress is everywhere, and it shows up in two very different shapes. A "classic" WordPress site is the familiar monolith: the PHP theme, the admin, and the REST API all live in one document root. A "headless" WordPress keeps the admin and content engine on the server but serves the front end from somewhere else — a Next.js app, a static generator, or a mobile client — over the REST API or WPGraphQL. Govard supports both topologies through the same local orchestration layer, so the day-to-day work (bring the stack up, pull a site down, run tests) is identical regardless of how the front end is wired.

Classic vs headless WP topologies

A classic install is the simplest case Govard handles. The framework definition detects a wp-config.php and a wp-content/ directory, then provisions the standard services: php-fpm, a MySQL or MariaDB container, and a web server in front of it. Your theme and plugins run exactly where they would in production, which makes debugging theme PHP and editor behaviour straightforward.

A headless setup detects the same WordPress core but expects no traffic on the theme layer — the public face is a separate app outside the container. Govard still runs the full WordPress back end, because you need wp-admin, cron, and the content API to behave normally. The difference is in how you point your front end at it: instead of browsing the container's web root, you configure your front-end build to call the container's API over the local network. The WordPress URL stored in the database stays the canonical origin; your front-end app just reads from it.

Tip: For headless projects, keep WP_HOME/WP_SITEURL pointing at the container, not at your front-end dev server. Point the front end at the API and leave WordPress believing it is the origin — that avoids a whole class of "mixed content" and CORS surprises later.

The same local stack serves both shapes, which is the point: you do not learn a second toolchain when a client asks you to decouple the front end. Govard's framework discovery decides what to boot, and the WordPress package already knows how to wire php-fpm, the database, and wp-cli together.

Local stack (php-fpm, MySQL/MariaDB, wp-cli alias)

After Govard detects a WordPress project, govard env up — aliased as govard up — brings the whole stack online:

# From the project root, with a WordPress install present
govard env up
govard env ps      # confirm php-fpm, db, and the web service are healthy
govard doctor      # sanity-check ports, versions, and mounts

The framework package registers a wp alias so you can run WP-CLI against the running container without remembering connection flags:

govard sh -- wp core version
govard sh -- wp plugin list --status=active
govard sh -- wp media regenerate --yes

Under the hood this is a real php-fpm process talking to a real MySQL/MariaDB instance, not a bundled PHP CLI pretending to be a server. That matters for plugins that behave differently between CLI and FPM contexts (anything touching uploads, cron, or opcache). You get the FPM path locally, so what you test is what you ship.

If you prefer a GUI to watch the stack, govard desktop opens the Wails-based desktop app where you can start, stop, and inspect services; govard vscode setup --global wires the editor to the same environment so terminals and debug sessions resolve to the container. Either way the backing services are identical, which keeps "works on my machine" from drifting between teammates.

Pulling a live site down safely (govard remote add, govard remote sync --capabilities files,media,db, search-replace)

The most fragile moment in any WordPress engagement is the first pull: you want the client's content and uploads, but you do not want to clobber your local config or import a database full of the production domain. Govard models this as a remote plus a sync with explicit capabilities, so nothing moves unless you ask for it.

First, register the remote once:

govard remote add staging \
  --host staging.example.com \
  --user deploy \
  --path /var/www/wordpress \
  --capabilities files,media,db

Then preview what a sync would do before it touches anything:

govard remote sync --plan
govard remote test   # verify the remote connection and credentials

The --capabilities flag is the safety boundary. files pulls the code tree (theme/plugin PHP), media pulls wp-content/uploads, and db pulls the database. You can omit db on a first pass to inspect code only, or omit files if you already have the theme in Git and just need content.

When you are ready, run the real sync and then normalize the domain. A pulled database still points at the production host, so a search-replace rewrites URLs to your local origin:

govard remote sync --capabilities files,media,db
govard sh -- wp search-replace 'https://staging.example.com' \
  'http://localhost:8080' --all-tables --skip-columns=guid

The --skip-columns=guid guard keeps WordPress GUIDs stable — changing them can break RSS readers and some migration plugins. After the replace, log in through wp-admin on your local URL and confirm permalinks and media resolve. The full workflow, including capability flags and planning mode, is documented in Remotes & sync.

ACF / plugin & upload sync gotchas

A few WordPress-specific traps show up the moment you sync real content, and they are worth naming up front.

  • Advanced Custom Fields (ACF). ACF field definitions live in the database, not in code. If you sync db but a teammate deploys a theme that registers fields in PHP, you can end up with duplicate or conflicting field groups. Decide whether field groups are code-owned (acf-json/) or admin-owned, and sync accordingly — pulling db over a code-owned setup silently overwrites the JSON source of truth.
  • Must-use and drop-in plugins. wp-content/mu-plugins/ and db.php style drop-ins are easy to miss because they are not in the active-plugin list. If files sync skips them, the site "works" locally but behaves differently. Verify the full wp-content/ tree came across, not just plugins/ and uploads/.
  • Uploads overlap. media sync can be large. For a first local bring-up, consider syncing only the latest uploads or a subset, then pulling the rest in the background — a full uploads/ history is rarely needed to reproduce a bug.
  • Plugin version drift. A synced db may expect a plugin version you have not installed locally. Run wp plugin list right after sync and align versions before trusting any admin behaviour.

None of these are Govard limitations — they are properties of WordPress itself. Govard's job is to make the pull explicit and reversible: --plan shows the scope, capabilities bound the blast radius, and the database search-replace is a single, auditable command rather than a scatter of find-and-replace edits across tables.

Next in this series: A team workflow with Govard — /blog/govard-team-workflow

govardwordpressphp