The UDX handbook / 01

How to run WordPress on Linux, properly.

WordPress is a system you operate.
Here is how to make every layer hold.

WordPress drawn as a layered Linux system An edge arc at the top takes incoming requests. Below it, a row of identical stateless app containers sits on a Linux host base plate, beside a stacked cache layer with a served-from-cache check, a database with a replica, and an object storage bucket for media off to the side. A slim log stream flows out of the system to a magnifier ringed by a soft glow. EDGE LOGS APP LINUX HOST CACHE DATA media offload MEDIA
Edge to evidence. A system you can inspect.

The operating idea, in sixty seconds.

Installation is solved. Operation is not. The sites that fail are usually installed correctly; the decisions around them are what break under load.

  1. Give every layer an owner.Edge, application, cache, data, media, pipeline and evidence.
  2. Follow the evidence.Our 19 incidents trace failures to the system around WordPress.
  3. Use the complexity you need.Steady traffic and inexpensive downtime? A managed host may be enough.
7Layers, each with
a clear responsibility
19Incidents from our
operating record
65%Image clients with no successful
image delivery in one incident
24hEdits unseen behind
an incomplete purge map

Seven layers. One system.

One server, one folder, one person who knows how it works. That model holds until the first traffic spike, the second server, or the day that person leaves.

Think in responsibilities instead. Each layer has one job, a clear owner, and evidence that it is doing its job. Linux is the foundation, whether you run one virtual machine or a Kubernetes node pool.

The operating model

The WordPress-on-Linux layer stack Seven gently three-dimensional slabs stacked top to bottom - Edge, Application, Cache, Data, Media, Pipeline and Evidence - each carrying a small motif and a small-caps label, resting on a ridged Linux host base plate. Every slab and the base share the same left edge. EDGE APP CACHE DATA MEDIA PIPELINE EVIDENCE LINUX HOST

Seven responsibilities. One system.
Linux underneath.

A CDN answering anonymous page views, assets and bots before they reach PHP.

Without itEvery crawler and scanner lands on your servers, and your capacity goes to an audience your analytics never shows you.

02Application

Chapter 02

WordPress as identical, disposable containers: no uploads, no sessions, no local state.

Without itOne server becomes special, and the site can never be run twice.

An object cache for repeated queries, a page cache for rendered output, explicit rules for expiry.

Without itThe database absorbs traffic a cache should have answered, and the first popular page to expire starts a stampede.

A managed database with automated backups, a replica, and a snapshot before every risky change.

Without itThe night an update goes wrong is the night you learn what your backups actually contain.

Uploads in object storage with an image service in front, so the app tier never touches a file.

Without itThe second server cannot see the images the first one received.

06Pipeline

Chapter 07

Changes arrive through source control, built once, tested on staging or a preview, then promoted.

Without itProduction is edited by hand, and nobody can say what is running or how to put it back.

07Evidence

Chapter 05

Edge logs, application logs and metrics somewhere you can query after the fact.

Without itThe only record of an outage is a screenshot from a customer, and the fix is a guess.

Where production sites actually fail.

Real symptoms. Traceable causes. Nineteen incidents from sites we operate, fixes we shipped, and support for WP-Stateless. Client names stay private; the mechanisms do not.

19 incidents. Select a symptom to see the cause and evidence.

01503s whenever the cluster scales upApplication

ObservedBursts of 503 errors, about 1,850 a week, clustered at the minutes the cluster scaled up.

CauseThe load balancer health check watched a helper process that was ready in six seconds, while PHP behind it needed thirty-five. New pods took traffic before they could serve it, and the autoscaler flapped between two and four replicas.

Evidence & resolutionLoad balancer log: 903 of the last 1,000 errors were backend connections closed at scale-up. Fixed by probing the real PHP path and raising the replica floor.

02Logins fail after a CDN changeCache

ObservedLogins failing with 403 after a routine CDN configuration change.

CauseA security token was being served from a cached JSON response, so every visitor received the same expired token.

Evidence & resolutionA diff of the CDN behavior and the headers its cache key kept.

03Event listings stay stale for hoursCache

ObservedEvent listings up to 11.6 hours stale while the CDN purge count reached about 270 a day.

CauseA nightly sync re-saved about 150 events and each save fired its own purge, which overran the 30-second gateway timeout before the last ones ran.

Evidence & resolutionCDN invalidation history: 1,282 purges in five days. Fixed by skipping sync saves and issuing one wildcard purge.

04Content updates stop reaching visitorsCache

ObservedContent changes silently stopped reaching visitors.

CauseTwo plugins bundled different versions of the same cloud authentication library, and the cache purge call failed on an undefined method.

Evidence & resolutionOne line in the PHP error log. Fixed by namespacing the vendored library in our plugin and upgrading the fleet.

05Image URLs change on the first of the monthMedia

ObservedEvery image URL on a site changed to the current month on the first of the month.

CauseThe bucket had been switched to uniform access, so the per-file permission write failed, the file's metadata never saved, and the path was regenerated with today's date on every request.

Evidence & resolutionA plugin issue thread that reached 37 comments before the cause was isolated, and the bucket's access setting. Uniform access becomes permanent after 90 days.

06The image service runs out of memory dailyMedia

ObservedAn image service pod killed for memory once a day.

CauseThe CDN honored a 60-second cache lifetime instead of the application's four hours, so the origin re-optimized 3,160 images an hour for 303 distinct variants.

Evidence & resolutionCDN hit ratio alongside the container memory graph. Fixed with an origin-honoring cache policy and a memory arena cap.

07Images fail while monitoring stays greenMedia

ObservedImages failing for 65 percent of visitors over thirty days, with a p95 response time of 30 seconds.

CauseAn upstream image optimizer bug: one aborted request on a cold cache key wedged that key for every later visitor, and a year of cached HTML kept asking for an image width the site no longer generated.

Evidence & resolutionThirty days of CDN standard logs, 2,032 unique clients, status 000 for viewer disconnects. Nothing in application monitoring showed it.

08Scheduled tasks and newsletters stopEdge

ObservedScheduled tasks stopped and a newsletter queue froze.

CauseA redirect rule at the edge sent the cron ping to the homepage, so it never reached PHP.

Evidence & resolutionEdge logs showing 301 responses for the cron path. Fixed with a bypass header for internal callers.

09A deployed fix still renders incorrectlyCache

ObservedA page fix deployed, invalidated and confirmed fresh, yet the page kept rendering wrong.

CauseThe front end fetches each page a second time with a query flag for its layout data, and that variant is a separate cache object the invalidation never touched.

Evidence & resolutionThe exact request the application makes, not the one a human makes in a browser. Invalidate with wildcards.

10Some visitors get trapped in a redirect loopCache

ObservedA homepage redirect loop for some visitors while every monitor stayed green.

CauseThe CDN cache key ignored query strings but forwarded them to the origin. A crawler hit a junk legacy URL, WordPress answered with a canonical 301, and the CDN stored that redirect under the bare homepage key for a day.

Evidence & resolutionEdge logs: about 8 percent of homepage visitors at the affected edges, two of them looping 378 and 150 times. Fixed by forwarding only whitelisted parameters, then purging everything, because a fixed policy does not evict what is already poisoned.

11Edits stay invisible for up to a dayCache

ObservedEdits and releases invisible to visitors for up to a day.

CauseHTML cached for 24 hours, and the save hooks purged only the edited permalink. The homepage, menus, sitemaps and the pages affected by a deploy were never purged; the release flushed the object cache and nothing else.

Evidence & resolutionA purge coverage map built from the save hooks against the edge lifetimes. Fixed with purges in the release and a short HTML lifetime.

12A PHP upgrade breaks a formApplication

ObservedFatal errors on a form page after a PHP upgrade, 120 occurrences in a week.

CauseAn abandoned add-on used a function removed in PHP 8.

Evidence & resolutionApplication monitoring grouped by error. Fixed with a small shim while the add-on was replaced.

13PHP errors with no user reportsEdge

ObservedHundreds of PHP errors in monitoring with no user reports.

CauseScanners were fetching core bootstrap and sample configuration files directly, and the files answered.

Evidence & resolutionError groups by request path. Closed with a web server rule that returns 404 for those files.

14An attack writes a web shell to cacheSecurity

ObservedMore than 27,000 requests fleet-wide to one REST endpoint in a day, and a web shell written to a cache directory on one site.

CauseA core vulnerability before the patch was available, plus a web server that would execute any PHP file wherever it was written.

Evidence & resolutionEdge logs. The shell returned 404 only because the container filesystem is ephemeral. Fixed with a firewall rule and a web server deny for PHP under uploads and cache.

15PHP tuning has no effect under loadApplication

ObservedOut-of-memory kills and 502s in traffic spikes despite per-site PHP tuning.

CauseThe init script edited the PHP-FPM pool file as the wrong user, the error was swallowed, and every site silently ran the default of 15 workers.

Evidence & resolutionReading the live pool config inside the container, not the repository: 22 deployments affected. Fixed by mounting the config read-only and asserting it at boot.

16A fresh install never startsData

ObservedA fresh install that never came up.

CauseMySQL takes about 100 seconds to initialize on burst-starved nodes, and the liveness probe killed it at 60 to 90 seconds, every time.

Evidence & resolutionPod events: exit 137 at the same age on every attempt. Fixed with a five-minute startup probe.

17The login page fails only in productionApplication

ObservedLogin page returning 502 on production only.

CauseThe production web server had 64k FastCGI buffers while staging had 256k, and chunked responses without a content length also made the CDN skip compression.

Evidence & resolutionA configuration diff between the two environments. Fixed in infrastructure code for both, which is where the drift started.

18Production URLs point at developmentData

ObservedProduction canonical URLs pointing at a development hostname.

CauseA development environment had been pointed at the shared database and WordPress wrote its own hostname into rows.

Evidence & resolutionA database query for the development hostname: 57 rows, 54 bad sitemap entries. Fixed from a backup with a flush and a purge.

19A site appears to alternate between two versionsEvidence

ObservedA site that appeared to flip between two renderings for a week, with the object cache suspected.

CauseOur own observer was wrong: one screenshot suite alternated between staging and production against a single baseline. A controlled cache flush rebuilt cleanly and ruled out poisoning.

Evidence & resolutionFour days lost before the suite was split. Verify the instrument before the system.

Go deeper, one layer at a time.

Go deeper where your system needs it. Each chapter connects an operating decision to the failures it prevents.

Next in the handbook

How much of this do you need?

Keep it simple

Most sites do not need a cluster.

Steady traffic, occasional content changes, and an hour of downtime that costs an apology? A good managed host is probably right.

Keep the useful habits: tested backups, staging, media in object storage, and a CDN.

Take the next step

Operations become a business concern.

  • Traffic arrives in spikes you cannot schedule.
  • An outage costs revenue or public trust.
  • You run several sites and need repeatable releases.
  • You need proof of how the system is operated.
  • Your host’s answer is always a bigger plan.

The practice behind the handbook.

We run this architecture ourselves.

This site runs WordPress in stateless containers on Google Kubernetes Engine, with a React front end, media in Google Cloud Storage through WP-Stateless, and CloudFront at the edge. It deploys from Git through Rabbit, the same pipeline we use for client sites.

Our source material is the operating record: incidents, pull requests, and the things we would do differently. We publish mechanisms and numbers while protecting client identities and internal systems.

Before you commit.

Can WordPress run on Linux in production without a managed host?

Yes. WordPress runs on Linux everywhere, from a single virtual machine to a Kubernetes cluster. What determines whether it stays up is not the host but whether the layers around it are deliberate: a CDN at the edge, stateless application containers, an object cache, a managed database with backups, media in object storage, a deployment pipeline, and logs you can query.

Why is my WordPress server slow even though traffic is low?

The usual causes are a missing object cache so repeated queries hit the database, WP-Cron firing heavy scheduled jobs on visitor requests, a plugin writing to the options table on every page view, or PHP workers tied up by slow external calls. Diagnose from the edge inward: check what the CDN served, then PHP worker saturation, then slow queries.

Do I need Docker or Kubernetes to run WordPress well?

Not always. Containers make WordPress reproducible and are worth it for any site that is deployed more than occasionally. Kubernetes earns its complexity when traffic spikes, downtime is expensive, you operate several sites, or you need to prove how the site is operated for compliance. For a modest, steady site, a good managed host is the right answer.

Should WordPress uploads be stored on the server?

No. Uploads on local disk are the first thing that breaks when a second server exists, and they make backups and migrations fragile. Store media in object storage such as Google Cloud Storage or S3 and serve it through an image CDN, so the application tier never touches a media file. WP-Stateless does this for Google Cloud.

What is the minimum setup for a production WordPress site on Linux?

A CDN in front, media in object storage, a persistent object cache, automated database backups with a tested restore, a staging environment that matches production, and request logs retained somewhere you can search. Those six cost little and prevent most of the incidents in our operating record.

Apache or nginx for WordPress?

Either serves WordPress well. The web server is rarely where production sites fail. Spend the attention on the edge cache, the object cache and the pipeline instead, and pick the server your team can configure and debug confidently.

Make your WordPress operation dependable.

Tell us where your site runs and what keeps going wrong. Talk with an engineer about the missing layers and the work needed to put them in place.