# How to Deploy Headless WordPress as a CMS
WordPress still powers a huge share of the web, and editors love its admin experience. But its default PHP-rendered themes feel dated next to a React, Next.js, or Astro frontend. Headless WordPress is the compromise: keep WordPress as the content backend and editing interface, but serve content through an API to a decoupled frontend. This tutorial covers the architecture and deployment.
The architecture
Editors → WordPress admin (PHP + MySQL)
│ exposes content via REST or GraphQL
▼
Frontend (Next.js / Astro / etc.) → visitorsTwo deployable concerns:
- 1WordPress backend — PHP application + a MySQL database. Used by editors and as the content API. Often locked down so the public never hits it directly.
- 2Frontend — a modern app that fetches content at build time (SSG) or request time (SSR/ISR) and renders it.
Step 1: choose your API — REST vs WPGraphQL
WordPress ships a built-in REST API. The popular alternative is the WPGraphQL plugin.
| WP REST API | WPGraphQL | |
|---|---|---|
| Setup | Built in | Install plugin |
| Over/under-fetching | Common (fixed shapes) | Request exactly what you need |
| Nested data | Multiple requests | Single query |
| Learning curve | Low | GraphQL knowledge |
For a content-heavy site with related data (posts → authors → categories → media), WPGraphQL usually means fewer round trips. For simple needs, the built-in REST API is fine and requires no plugins.
# REST: fetch published posts
curl https://cms.example.com/wp-json/wp/v2/posts?status=publish# WPGraphQL: exactly the fields you need, in one query
query { posts { nodes { title slug author { node { name } } } } }Step 2: containerize WordPress
Use the official WordPress image and drive config via environment variables so no secrets live in the image:
FROM wordpress:php8.2-apache
# Add plugins/themes via COPY or a build step as needed
COPY ./wp-content/plugins /var/www/html/wp-content/pluginsWordPress reads its database config from environment variables:
WORDPRESS_DB_HOST=<managed-mysql-host>
WORDPRESS_DB_USER=<user>
WORDPRESS_DB_PASSWORD=<password>
WORDPRESS_DB_NAME=wordpress
WORDPRESS_CONFIG_EXTRA=define('WP_HOME','https://cms.example.com');Step 3: provision MySQL
WordPress needs MySQL. Provision a managed MySQL (5.7 or 8.x), create the database, and point WordPress at it via the env vars above. Managed MySQL gives you backups and a stable host instead of running the DB inside the same container (which would lose data on restart).
Step 4: handle media (the stateful gotcha)
The biggest headless-WordPress deployment trap is uploads. By default WordPress writes media to wp-content/uploads on local disk — which vanishes when a container restarts or reschedules. Two fixes:
- Offload media to object storage (S3-compatible) via a plugin, so uploads live outside the container.
- Or attach persistent storage to the WordPress container.
For a cloud-native deploy, offloading media to object storage is the cleaner approach.
Step 5: lock down the backend
In a headless setup, visitors should hit your frontend, not WordPress directly. Hardening steps:
- Restrict the WordPress admin to known IPs or behind SSO where possible.
- Disable the default themes/front-end rendering you do not need.
- Cache the API responses aggressively at the edge.
- Keep WordPress and plugins updated — it is a frequent attack target.
Step 6: build the frontend
Your frontend fetches from the WordPress API. With static generation, content is baked at build time for maximum speed:
// Next.js: fetch posts at build
export async function getStaticProps() {
const res = await fetch('https://cms.example.com/wp-json/wp/v2/posts');
const posts = await res.json();
return { props: { posts }, revalidate: 60 }; // ISR: refresh hourly-ish
}Use Incremental Static Regeneration (or scheduled rebuilds) so new content appears without a full redeploy.
Common pitfalls
- Local uploads lost on restart. Offload media to object storage.
- DB inside the container. Use a managed MySQL so data survives.
- WordPress URL mismatch. Set
WP_HOME/WP_SITEURLcorrectly or you get redirect loops. - Exposing the backend publicly unprotected. Headless WP is still WordPress — harden it.
- CORS. Allow your frontend origin if it calls the API from the browser.
Deploying on PandaStack
PandaStack builds containers from any Dockerfile, so the official WordPress image deploys directly. Provision a managed MySQL (5.7 or 8.x) for content and set the WORDPRESS_DB_* env vars — PandaStack manages the database with scheduled and manual backups, so your content is protected. Deploy your frontend as a static site (Next export, Astro, etc.) or a container; PandaStack auto-detects the framework and builds it. Custom domains get automatic SSL on both the CMS and the frontend, and edge functions or cronjobs can trigger rebuilds when content changes. Remember the media gotcha: offload uploads to object storage rather than container disk. The free tier (5 web services + 5 static sites + 1 database) can host both halves of a headless WordPress setup.
References
- [WordPress REST API handbook](https://developer.wordpress.org/rest-api/)
- [WPGraphQL documentation](https://www.wpgraphql.com/docs/introduction)
- [Official WordPress Docker image](https://hub.docker.com/_/wordpress)
- [WordPress hardening guide](https://developer.wordpress.org/advanced-administration/security/hardening/)
---
Headless WordPress keeps editors happy while giving you a modern frontend — the deploy is mostly about media and database persistence. PandaStack builds both halves and provides managed MySQL with backups — start free at [dashboard.pandastack.io](https://dashboard.pandastack.io).