How to Host a CMS on a $5 VPS (Full Walkthrough)

Provision, harden, deploy, and back up a production CMS for about $6 a month — every command included

July 24, 2026 · 13 min read
How to Host a CMS on a $5 VPS (Full Walkthrough)

A $5 VPS in 2026 has more compute than the servers that ran million-visitor sites a decade ago. Yet most people still pay $30-50/month for managed CMS hosting because setting up a server feels like a dark art. It isn't. It's about 40 commands, and you only run them once.

This is the full walkthrough: provision a server, harden it, install the stack, deploy a CMS, get HTTPS, set up backups, and know when it's time to upgrade. Every command is copy-pasteable. Total time: about an hour if you've never done it, 20 minutes if you have.

TL;DR: A $5/month VPS (Hetzner CX22-class: 2 vCPU, 4GB RAM) comfortably runs a CMS site doing tens of thousands of visits per month. Stack: Ubuntu 24.04, Nginx, PHP 8.3-FPM, SQLite or MySQL. Add ufw, fail2ban, certbot, a cron line, and an rclone backup script, and you have production hosting for the price of two coffees.


What Does $5/Month Buy in 2026?

A $5/month VPS in 2026 typically gets you 1-2 vCPUs, 1-4GB of RAM, 25-40GB of NVMe storage, and 1-20TB of bandwidth. The spread between providers is wide, and Hetzner is the outlier on value — roughly 4x the RAM of the US providers at the same price.

Here's the budget tier as of mid-2026:

Provider Plan vCPU RAM Storage Bandwidth Price/mo
Hetzner CX22 2 4GB 40GB NVMe 20TB ~$5 (€4.59)
DigitalOcean Basic Droplet 1 1GB 25GB SSD 1TB $6
Vultr Cloud Compute 1 1GB 25GB SSD 1TB $5
Linode (Akamai) Nanode 1 1GB 25GB SSD 1TB $5

If you're in or near Europe, Hetzner is the obvious pick — 4GB of RAM means you can run Nginx, PHP-FPM, and MySQL together without swap pressure. The US providers' 1GB plans still work fine for a CMS, but you'll want SQLite instead of MySQL to keep memory headroom (more on that below).

Is a $5 VPS Enough for a Real Site?

Yes, with room to spare. Do the math: 50,000 visits per month averages out to about 1.2 requests per minute. Even with traffic spikes at 20x the average, that's 24 requests per minute — a properly configured PHP-FPM pool handles that without breaking 5% CPU.

The real-world bottleneck for a CMS on small hardware is almost never traffic. It's unoptimized images, missing opcache, or a bloated plugin stack. A lean Laravel-based CMS like UnfoldCMS serves a cached page in under 50ms on this class of hardware. If you're coming from a heavier headless setup, the Strapi alternatives comparison covers why lighter stacks fit small servers better — Strapi alone wants 2GB+ of RAM before you've served a single visitor.


Step 1: Provision the Server and Lock Down SSH

Create the server in your provider's panel: Ubuntu 24.04 LTS, smallest plan, and add your SSH public key during creation. If you don't have a key yet:

ssh-keygen -t ed25519 -C "[email protected]"
cat ~/.ssh/id_ed25519.pub   # paste this into the provider panel

First login and updates:

ssh root@YOUR_SERVER_IP
apt update && apt upgrade -y

Create a non-root user with sudo, and copy your key to it:

adduser deploy
usermod -aG sudo deploy
rsync --archive --chown=deploy:deploy ~/.ssh /home/deploy

Now disable password login entirely. This single change kills 99% of the automated attacks your server will see:

sudo nano /etc/ssh/sshd_config

Set these three lines:

PasswordAuthentication no
PermitRootLogin prohibit-password
PubkeyAuthentication yes

Restart SSH and test the key login from a second terminal before closing your current session:

sudo systemctl restart ssh
# from your local machine, in a NEW terminal:
ssh deploy@YOUR_SERVER_IP

If the new terminal gets in, you're safe to continue.


Step 2: Basic Hardening — Firewall, fail2ban, Auto-Updates

Three tools cover 90% of small-server security: ufw blocks every port except SSH and HTTP/S, fail2ban bans IPs that hammer your SSH login, and unattended-upgrades installs security patches while you sleep. Ten minutes of setup, then you never think about it again.

Firewall first:

sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status

fail2ban next. The defaults are sensible — it watches the SSH log and bans IPs after repeated failures:

sudo apt install -y fail2ban
sudo systemctl enable --now fail2ban
sudo fail2ban-client status sshd   # check it's watching

Automatic security updates:

sudo apt install -y unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades   # select Yes

That's the whole hardening pass. You can go deeper (change the SSH port, add CrowdSec, kernel tuning), but for a CMS site this baseline plus key-only SSH puts you ahead of most shared hosting accounts.


Step 3: Install the Stack — Nginx, PHP 8.3-FPM, Database

A modern PHP CMS needs three things: Nginx to accept requests, PHP-FPM to run the application, and a database — which can be a single SQLite file rather than a full MySQL server. On Ubuntu 24.04, PHP 8.3 is in the default repos, so installation is one apt line.

sudo apt install -y nginx php8.3-fpm php8.3-cli \
  php8.3-mbstring php8.3-xml php8.3-curl php8.3-zip \
  php8.3-gd php8.3-intl php8.3-sqlite3 php8.3-mysql

And Composer, which you'll need to install the CMS dependencies:

curl -sS https://getcomposer.org/installer | php
sudo mv composer.phar /usr/local/bin/composer

SQLite or MySQL?

For a single CMS site on a small VPS, SQLite is the right default. It's a file, not a service — zero RAM overhead, zero configuration, and one less thing to back up separately. SQLite handles thousands of reads per second, which is far beyond what a content site generates. The flat-file vs database CMS breakdown goes deeper on this trade-off, but the short version: read-heavy content sites are exactly what SQLite is good at.

Pick MySQL if you expect heavy concurrent writes (busy comment sections, many editors saving at once) or you're on the 4GB Hetzner box and want it anyway:

sudo apt install -y mysql-server
sudo mysql_secure_installation
sudo mysql -e "CREATE DATABASE cms; CREATE USER 'cms'@'localhost' IDENTIFIED BY 'CHANGE_ME'; GRANT ALL ON cms.* TO 'cms'@'localhost';"

On a 1GB droplet, skip MySQL. It idles at 350-400MB of RAM — over a third of your machine doing nothing.

One PHP tweak worth making — confirm opcache is on (it is by default on Ubuntu 24.04, but verify):

php -i | grep opcache.enable

Step 4: Deploy the CMS

Deployment is four moves: get the code onto the server, install PHP dependencies, configure the environment file, and run migrations. I'll use UnfoldCMS as the worked example — it's a Laravel 12 application running on PHP 8.3, and its frontend ships precompiled, so you don't need Node.js on the server at all. The same shape applies to any Laravel-based CMS.

Get the code into /var/www:

cd /var/www
# from a zip:
sudo unzip unfoldcms.zip -d cms && cd cms
# or from a git repo:
sudo git clone https://github.com/your-org/your-site.git cms && cd cms

Install dependencies (production mode — no dev packages):

composer install --no-dev --optimize-autoloader

Configure the environment:

cp .env.example .env
php artisan key:generate
nano .env

The lines that matter:

APP_ENV=production
APP_DEBUG=false
APP_URL=https://yourdomain.com

DB_CONNECTION=sqlite
# that's it for SQLite — Laravel finds database/database.sqlite automatically

Create the database file, run migrations, and cache config:

touch database/database.sqlite
php artisan migrate --force
php artisan config:cache && php artisan route:cache && php artisan view:cache

Finally, hand ownership to the web server user:

sudo chown -R www-data:www-data /var/www/cms
sudo chmod -R 775 /var/www/cms/storage /var/www/cms/database

That chmod on database/ matters with SQLite — the directory must be writable by www-data, not just the file, or you'll get "attempt to write a readonly database" errors.


Step 5: Nginx Server Block + Free HTTPS

Point your domain's A record at the server IP first (do this early — DNS propagation runs while you work). Then create the server block:

sudo nano /etc/nginx/sites-available/cms
server {
    listen 80;
    server_name yourdomain.com www.yourdomain.com;
    root /var/www/cms/public;
    index index.php;

    client_max_body_size 64M;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
    }

    location ~ /\.(?!well-known) {
        deny all;
    }
}

Enable it and reload:

sudo ln -s /etc/nginx/sites-available/cms /etc/nginx/sites-enabled/
sudo rm /etc/nginx/sites-enabled/default
sudo nginx -t && sudo systemctl reload nginx

HTTPS via Let's Encrypt takes two commands. Certbot edits the Nginx config for you and sets up auto-renewal:

sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com

Verify renewal works:

sudo certbot renew --dry-run

Your site is now live on HTTPS. Certificates renew themselves every 60 days via a systemd timer — nothing to maintain.


Step 6: Cron for the Scheduler

Laravel-based CMSs use a single cron entry to drive everything time-based. In UnfoldCMS, that's how scheduled publishing works — you set a publish date in the admin, and the scheduler flips the post live at that minute. Without this cron line, scheduled posts never go out.

sudo crontab -u www-data -e

Add one line:

* * * * * cd /var/www/cms && php artisan schedule:run >> /dev/null 2>&1

That's it. The scheduler wakes every minute, checks what's due, and runs it. One line replaces a whole queue of individual cron jobs.


Step 7: Backups — Nightly tar + rclone to Object Storage

A backup you haven't automated is a backup you don't have. The simple, reliable pattern for a small CMS: tar the application's user data nightly, copy the SQLite file safely, and push both to cheap object storage with rclone.

Install rclone and connect a remote (Backblaze B2, Cloudflare R2, and Hetzner Storage Box all work — B2 is about $6/TB/month, and a CMS backup set is usually under 1GB):

sudo apt install -y rclone
rclone config   # interactive — name the remote "backup"

Create the backup script:

sudo nano /usr/local/bin/cms-backup.sh
#!/usr/bin/env bash
set -euo pipefail
STAMP=$(date +%F)
TMP=/tmp/cms-backup-$STAMP

mkdir -p "$TMP"

# SQLite: use the .backup command, never plain cp on a live DB
sqlite3 /var/www/cms/database/database.sqlite ".backup '$TMP/database.sqlite'"

# Uploads + env file
tar -czf "$TMP/files.tar.gz" -C /var/www/cms storage/app/public .env

# Push to object storage, keep 14 days
rclone copy "$TMP" "backup:my-cms-backups/$STAMP"
rclone delete --min-age 14d "backup:my-cms-backups"

rm -rf "$TMP"

Make it executable and schedule it for 3 AM:

sudo chmod +x /usr/local/bin/cms-backup.sh
sudo crontab -e
# add:
0 3 * * * /usr/local/bin/cms-backup.sh

Test a restore once. Pull a backup down, load the SQLite file locally, open a few posts. A restore you've never tested is a guess.


Monitoring on a Budget (Read: Free)

You don't need Datadog for one server. Two free tools cover the question that actually matters — "is my site up?":

  1. UptimeRobot free tier — 50 monitors, 5-minute checks, email alerts. Point one at your homepage.
  2. Hetzner/DO built-in graphs — CPU, bandwidth, and disk are in the provider panel already.
  3. A weekly habit: SSH in, run df -h (disk) and free -m (RAM). Thirty seconds.

For log errors, php artisan apps write to storage/logs/. Set Laravel's log channel to daily so a single log file can't quietly eat your disk — a 261MB single-file log on a 25GB disk is a real failure mode, not a hypothetical.


When Should You Upgrade Past $5?

Upgrade when you see sustained RAM pressure (swap in regular use), CPU steal above ~10%, or response times that stay slow after caching. Traffic numbers alone aren't the trigger — a cached CMS page is cheap to serve at almost any volume.

The three signals, and how to read them:

  1. RAM pressure. Run free -m. If swap is in constant use (not just touched once at boot), PHP-FPM workers are fighting MySQL for memory. Either drop MySQL for SQLite or move up a tier.
  2. CPU steal. Run top and watch the st value. Steal above 10% means noisy neighbors are eating your shared vCPU — on budget tiers this is the most common "my site is randomly slow" cause. Resize or switch providers.
  3. Slow responses with caching on. If pages take over 500ms with opcache and application caching enabled, you've outgrown the tier. The next step up (~$10-12) doubles your resources.

Most content sites never hit any of these. The jump from "tens of thousands" to "hundreds of thousands" of monthly visits is where the $5 box starts to feel small — and even then, a CDN in front often delays the upgrade for another year.


Realistic Monthly Cost Breakdown

What does self-hosting a CMS actually cost per month?

Item Cost/mo
VPS (Hetzner CX22 or equivalent) $5.00
Domain (amortized from ~$12/yr) $1.00
Backups (Backblaze B2, ~1GB) $0.01
SSL (Let's Encrypt) $0.00
Uptime monitoring (UptimeRobot free) $0.00
Total ~$6.00

Compare that with $25-45/month for managed WordPress hosting or a headless CMS team plan, and the VPS pays for your time investment in the first two months. The trade: you own the updates and the 3 AM pager. For a content site with backups in place, that trade is lighter than it sounds — the stack above runs for months untouched.


FAQ

Can I run a CMS on a $5 VPS with only 1GB of RAM?

Yes. Use SQLite instead of MySQL, keep opcache on, and you'll have 600MB+ free for PHP-FPM workers. A 1GB droplet serves a cached CMS site doing 30-50k visits/month without strain. MySQL is the thing that doesn't fit, not the CMS.

Do I need Docker for this?

No. For a single PHP application on a single server, Docker adds a layer of complexity without buying you much. Plain Nginx + PHP-FPM via apt is simpler to debug, uses less RAM, and upgrades with apt upgrade.

Is a VPS harder to maintain than shared hosting?

After setup, barely. unattended-upgrades patches the OS, certbot renews SSL, cron runs backups. Budget 15 minutes a month for a manual check. UnfoldCMS also runs on shared hosting if you'd rather skip server management entirely — the PHP requirements are the same.

What about traffic spikes — a post hits Hacker News?

Enable full-page caching in your CMS and put Cloudflare's free tier in front. Cached pages are served from CDN edge, and your VPS only renders for logged-in users. A $5 box behind a CDN survives front-page spikes fine.


The Bottom Line

A $5 VPS plus an afternoon gives you hosting that's faster, more private, and 5-8x cheaper than managed platforms — and you learn exactly how your stack works. The setup above is the same shape we run unfoldcms.com on (one Hetzner box, Nginx, PHP 8.3, MySQL), so this isn't theory.

If self-hosting a server still isn't your thing, there are middle paths: deploying a CMS-backed site to Netlify keeps the frontend on free static hosting, and UnfoldCMS itself works on plain shared hosting with a one-time license — no monthly SaaS fee either way. See what's included and pricing if you want the worked example from this guide as your starting point.


Sources & methodology: provider specs and pricing from Hetzner, DigitalOcean, Vultr, and Akamai/Linode public pricing pages (June 2026); RAM figures from default MySQL 8.0 and PHP-FPM footprints on Ubuntu 24.04; performance observations from running unfoldcms.com on a Hetzner CX22. Prices in USD, rounded.

Free & Open Source

Own your CMS. No subscriptions.

Unfold CMS is free to download and self-host. Built on Laravel + React, full source code included.

Share this post:

Discussion

Comments (0)

Leave a Comment

Please log in to leave a comment.

Don't have an account? Register here

No comments yet. Be the first to share your thoughts!

Keep Reading

Related Posts

Back to all posts