Bui Van Dat

Hosting 8 WordPress Sites and 7 Apps on One VPS With Docker

How I run 8 isolated WordPress sites and 7 apps on one 6-core server: container layout, memory limits, Cloudflare, nightly security checks, and pitfalls.

4 min read
In this article
  1. One site, four containers
  2. One front door
  3. Cloudflare in front of everything
  4. Page caching does the heavy lifting
  5. Security checks every night
  6. Disk is a resource too
  7. Mistakes to avoid
  8. Is one server enough?

All of my websites and apps run on one virtual server with 6 vCPUs, about 11 GB of RAM, and a 193 GB disk. That includes eight WordPress sites, this site, the DatPilot website and portal, a crawling service, a headless browser service for audits, and a mail container. Here is how it is laid out, and why it stays calm.

One site, four containers

Each WordPress site is fully isolated in its own set of containers:

Container Role Memory limit
web nginx, serves static files and caches pages 128 MB
php PHP-FPM running WordPress 512 MB
db MariaDB, one database per site 512 MB
redis object cache 96 MB

Isolation means one site’s traffic spike, plugin bug, or compromised account cannot reach another site’s database or files. It also means I can upgrade, back up, or delete one site without touching the rest.

The limits look tight, but real usage is far below them. A snapshot of one site at a normal moment showed PHP at about 65 MB, the database at about 44 MB, and nginx and Redis under 10 MB each. Limits are not a performance target. They are a fuse, so a runaway process gets stopped instead of starving its neighbors.

I manage the WordPress stacks with an off-the-shelf management tool built around this layout. It handles creating sites, backups, restores, and image upgrades, which leaves me free to focus on what runs inside the sites.

One front door

Only one container listens on ports 80 and 443: a reverse proxy that routes traffic by domain name to the right site. Nothing else publishes ports. When I add a new app, it joins the proxy’s network and declares its domain in an environment variable. It never claims a port of its own, because that would collide with every site on the box.

Every app container also gets:

  • mem_limit and cpus limits
  • no-new-privileges so processes cannot escalate
  • restart: unless-stopped so a reboot brings everything back

Static sites like this one are tiny: an nginx container with a 64 MB limit, using about 6 MB.

Cloudflare in front of everything

All domains are proxied through Cloudflare. The origin uses long-lived self-signed certificates, and Cloudflare connects to it in Full mode. Visitors get Cloudflare’s certificate and CDN, and the server’s real IP address is not published in DNS.

That last point is worth protecting. When I planned a self-hosted mail server, I noticed that adding an A record for mail and an SPF record with the server IP would reveal the address behind every site at once, and let anyone bypass Cloudflare. The options are to send mail through a relay service, or to keep self-hosted mail but firewall ports 80 and 443 so only Cloudflare’s IP ranges can reach them. Decide before you publish the record, not after.

Page caching does the heavy lifting

Most visitors read articles, and articles rarely change. nginx serves cached HTML for those requests without touching PHP. This is why eight content sites fit comfortably on modest hardware.

The catch: after changing a setting that affects HTML, clear the cache. I once switched a site from noindex to indexable, confirmed it in the database, and the cache kept serving the old noindex page. Now every settings change is followed by a cache purge and a check on the live page.

Security checks every night

A nightly job inspects each site and alerts me immediately if it finds:

  • a site returning errors or noindex
  • a new administrator account that I did not create
  • PHP files inside the uploads folder, a common sign of compromise
  • previously closed endpoints that became exposed again
  • an SSL certificate expiring within 14 days

Anything less urgent goes into a daily report. The bot stays silent unless something matters.

Disk is a resource too

With heavy data collection, disk fills faster than you expect. Two things helped:

  • Compressing cached crawl data with gzip inside the pipeline, since raw HTML compresses very well.
  • Cleaning old pre-publish backups automatically. The first cleanup freed about 775 MB, most of it from a single site.

The disk now sits at about 25% used.

Mistakes to avoid

  • Exposing ports on app containers. One ports: line can take down every site behind the proxy.
  • No limits. A single memory leak without a limit can freeze the whole server.
  • Trusting the database over the page. Always check what the live URL actually returns.
  • Leaking the origin IP through mail records, direct-IP subdomains, or error pages.
  • Restarting things during a long job. I once redeployed an app while a customer’s site scan was running. The scan hung. Now unfinished jobs resume on startup, and anything stuck for more than 300 seconds is marked as failed instead of hanging forever.

Is one server enough?

For content sites with good caching, yes, for a long time. The weak point is not CPU or memory. It is the single point of failure. Backups run on a schedule and restoring a site is one command, and the next item on my list is keeping more of those copies off the server, so a dead machine costs hours, not data.

When a project outgrows this, it will be because of traffic I would be happy to have. Until then, one well-organized server is cheaper, simpler, and easier to reason about than a cluster.

  • WordPress hosting
  • Docker
  • VPS
  • Cloudflare
  • DevOps