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_SITEURLpointing 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
dbbut 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 — pullingdbover a code-owned setup silently overwrites the JSON source of truth. - Must-use and drop-in plugins.
wp-content/mu-plugins/anddb.phpstyle drop-ins are easy to miss because they are not in the active-plugin list. Iffilessync skips them, the site "works" locally but behaves differently. Verify the fullwp-content/tree came across, not justplugins/anduploads/. - Uploads overlap.
mediasync 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 fulluploads/history is rarely needed to reproduce a bug. - Plugin version drift. A synced
dbmay expect a plugin version you have not installed locally. Runwp plugin listright 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