How to Set Up a Self-Hosted CMS (2026 Step-by-Step)
Renting your CMS is like renting a server for life. At some point, the math stops working in your favor — and you realize the platform owns your data, your pricing, and your deployment timeline.
Setting up a self-hosted CMS takes a few hours. Once it's running, you own it: the stack, the data, the uptime, and the roadmap. This guide walks through the full setup from a blank VPS to a production-ready CMS — no prior Linux admin experience required, but you should be comfortable with a terminal.
TL;DR: Pick a VPS (Hetzner CX22 or DigitalOcean Droplet), install Nginx + PHP 8.3 + MySQL 8, deploy UnfoldCMS via Git, point your domain, add SSL, set up daily backups. The whole process takes 2–4 hours. Self-hosted beats SaaS on cost after month 6 for most teams.
What You'll Need Before You Start
Before touching a server, make sure you have:
- A domain name (or subdomain) pointed to your server IP
- A VPS account — Hetzner, DigitalOcean, Vultr, or Linode all work
- A local machine with SSH client and basic terminal comfort
- 30–60 minutes of uninterrupted time for the initial setup
- UnfoldCMS — the open-source CMS this guide installs
This guide installs UnfoldCMS — a Laravel 12 CMS with a React + shadcn/ui admin, built for developers who want full ownership of their stack. The server setup steps (Nginx, PHP, MySQL, SSL) apply to any PHP CMS, but the deployment commands are UnfoldCMS-specific.
Step 1: Choose and Provision Your Server
The benefits of self-hosting a CMS start with choosing infrastructure you actually control. Don't overthink this step — start small and scale.
Recommended specs for a new CMS install:
| Use Case | RAM | CPU | Storage | Provider |
|---|---|---|---|---|
| Personal / dev blog | 2 GB | 1 vCPU | 40 GB SSD | Hetzner CX22 (~€4/mo) |
| Small business site | 4 GB | 2 vCPU | 80 GB SSD | Hetzner CX32 (~€9/mo) |
| Agency / multi-site | 8 GB | 4 vCPU | 160 GB SSD | Hetzner CX42 (~€18/mo) |
| High-traffic production | 16 GB | 8 vCPU | 240 GB SSD | Hetzner CX52 (~€35/mo) |
Hetzner is the best value for EU-based teams. DigitalOcean has better documentation and a larger US community. Both work fine.
After provisioning:
- Log in as root:
ssh root@YOUR_SERVER_IP - Create a non-root user with sudo access:
adduser deploy
usermod -aG sudo deploy
- Copy your SSH key to the new user:
rsync --archive --chown=deploy:deploy ~/.ssh /home/deploy
- Disable root SSH login (edit
/etc/ssh/sshd_config, setPermitRootLogin no) and restart SSH.
From here, use deploy for all operations.
Step 2: Install the Server Stack
A self-hosted CMS needs a web server, a PHP runtime, and a database. This is the stack that handles every request your site receives.
# Update package list
sudo apt update && sudo apt upgrade -y
# Install Nginx
sudo apt install nginx -y
# Install PHP 8.3 + required extensions
sudo apt install php8.3-fpm php8.3-mysql php8.3-mbstring \
php8.3-xml php8.3-curl php8.3-zip php8.3-bcmath \
php8.3-intl php8.3-gd php8.3-redis -y
# Install MySQL 8
sudo apt install mysql-server -y
sudo mysql_secure_installation
# Install Composer
curl -sS https://getcomposer.org/installer | php
sudo mv composer.phar /usr/local/bin/composer
Configure PHP-FPM — edit /etc/php/8.3/fpm/php.ini:
upload_max_filesize = 64M
post_max_size = 64M
max_execution_time = 120
memory_limit = 256M
Restart PHP-FPM: sudo systemctl restart php8.3-fpm
Create the MySQL database:
sudo mysql -u root -p
CREATE DATABASE cms CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'cmsuser'@'localhost' IDENTIFIED BY 'YOUR_STRONG_PASSWORD';
GRANT ALL PRIVILEGES ON cms.* TO 'cmsuser'@'localhost';
FLUSH PRIVILEGES;
EXIT;
Step 3: Deploy the CMS
With the stack running, pull your CMS code onto the server.
# Create the web root directory
sudo mkdir -p /var/www/mysite
sudo chown deploy:deploy /var/www/mysite
# Clone UnfoldCMS
cd /var/www
git clone https://github.com/hpakdaman/unfoldcms.git mysite
cd mysite
# Install PHP dependencies
composer install --no-dev --optimize-autoloader
# Copy environment file and configure it
cp .env.example .env
nano .env
Key .env values to set:
APP_ENV=production
APP_DEBUG=false
APP_URL=https://yourdomain.com
DB_DATABASE=cms
DB_USERNAME=cmsuser
DB_PASSWORD=YOUR_STRONG_PASSWORD
CACHE_DRIVER=file
SESSION_DRIVER=file
QUEUE_CONNECTION=sync
Run the install commands:
php artisan key:generate
php artisan migrate --force
php artisan db:seed --force # if your CMS has a seeder
php artisan config:cache
php artisan route:cache
php artisan view:cache
Fix file permissions — this is where most failed setups go wrong:
sudo chown -R www-data:www-data /var/www/mysite/storage
sudo chown -R www-data:www-data /var/www/mysite/bootstrap/cache
sudo chmod -R 775 /var/www/mysite/storage
sudo chmod -R 775 /var/www/mysite/bootstrap/cache
Step 4: Configure Nginx
Nginx needs a server block that routes requests to your CMS. Create /etc/nginx/sites-available/mysite:
server {
listen 80;
server_name yourdomain.com www.yourdomain.com;
root /var/www/mysite/public;
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
index index.php;
charset utf-8;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location = /favicon.ico { access_log off; log_not_found off; }
location = /robots.txt { access_log off; log_not_found off; }
error_page 404 /index.php;
location ~ \.php$ {
fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
}
location ~ /\.(?!well-known).* {
deny all;
}
# Cache static assets
location ~* \.(jpg|jpeg|png|gif|ico|css|js|webp|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}
Enable the site and test:
sudo ln -s /etc/nginx/sites-available/mysite /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
Your CMS should now be accessible over HTTP. Next: SSL.
Step 5: Add SSL with Let's Encrypt
Running a CMS without HTTPS in 2026 is a security risk and an SEO penalty. Let's Encrypt gives you a free, auto-renewing certificate in under two minutes.
sudo apt install certbot python3-certbot-nginx -y
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com
Certbot will modify your Nginx config to redirect HTTP → HTTPS and install the certificate automatically. Verify auto-renewal works:
sudo certbot renew --dry-run
Certbot installs a systemd timer that renews certificates before they expire. You don't need to do anything manually after this.
Force HTTPS in your CMS .env:
APP_URL=https://yourdomain.com
FORCE_HTTPS=true
Step 6: Set Up Automated Backups
A self-hosted CMS without backups is a liability. Most hosting providers snapshot VMs at the block level, but that's not a substitute for application-level backups — you want to be able to restore a specific database state from 3 days ago without rolling back your entire server.
Database backup script — save as /home/deploy/backup-db.sh:
#!/bin/bash
DATE=$(date +%Y-%m-%d-%H%M)
BACKUP_DIR="/home/deploy/backups/db"
mkdir -p $BACKUP_DIR
mysqldump -u cmsuser -pYOUR_PASSWORD cms | gzip > "$BACKUP_DIR/cms-$DATE.sql.gz"
# Keep only last 14 days
find $BACKUP_DIR -name "*.sql.gz" -mtime +14 -delete
Make it executable and add to cron:
chmod +x /home/deploy/backup-db.sh
crontab -e
# Add this line:
0 2 * * * /home/deploy/backup-db.sh
Files backup — for uploaded media and user content:
# Add to crontab
0 3 * * * rsync -az /var/www/mysite/storage/app/public/ /home/deploy/backups/media/
For off-site backups, push to an S3-compatible bucket (Backblaze B2 is cheapest at $0.006/GB):
# Install rclone, configure B2
rclone copy /home/deploy/backups/ b2:your-bucket/server-backups/
Data ownership only means something if you can actually restore from it. Test your backup by restoring to a staging server at least once.
Step 7: Performance and Security Hardening
With the basics running, a few quick wins get you to production-grade.
Enable OPcache — PHP's bytecode cache cuts response time by 30–50% for free. Edit /etc/php/8.3/fpm/php.ini:
opcache.enable=1
opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=10000
opcache.revalidate_freq=0
opcache.validate_timestamps=0
Restart PHP-FPM after changing this.
Add a firewall — only allow ports you actually need:
sudo ufw allow ssh
sudo ufw allow 'Nginx Full'
sudo ufw enable
Fail2ban — blocks IP addresses after repeated failed SSH login attempts:
sudo apt install fail2ban -y
sudo systemctl enable fail2ban
sudo systemctl start fail2ban
Set up the CMS scheduler — most CMSes have scheduled tasks (publishing queued posts, clearing caches, sending emails). Add one cron entry:
* * * * * cd /var/www/mysite && php artisan schedule:run >> /dev/null 2>&1
For a deeper look at security hardening specific to self-hosted setups, the self-hosted CMS security guide covers GDPR compliance, rate limiting, and audit logging in detail.
Common Setup Mistakes (And How to Avoid Them)
Most failed self-hosted CMS installs come down to five mistakes:
1. Wrong file permissions. If your CMS can't write to storage/ or bootstrap/cache/, you'll see blank pages or 500 errors. Always set ownership to www-data:www-data and permissions to 775 for writable directories.
2. Skipping OPcache. A PHP CMS without OPcache is 2–5x slower than it needs to be. This is the single highest-impact performance change you can make, and it costs nothing.
3. Not testing backups. Writing a backup script is not the same as having a backup. Run a restore on a staging server within the first week. Most people discover their backup is broken after they need it.
4. Debug mode left on in production. APP_DEBUG=true exposes database credentials, file paths, and environment variables to anyone who triggers an error. Always set APP_DEBUG=false before going live.
5. Using shared hosting for a Laravel CMS. Shared hosting doesn't support PHP-FPM, can't run cron jobs reliably, and typically runs PHP 7.x. A €4/month Hetzner VPS is cheaper than most shared hosting plans and gives you full control.
Comparing Self-Hosted CMS Setup to Managed Alternatives
The full cost comparison between self-hosted and managed CMS shows self-hosting saves money after about 6 months. Here's the quick version:
| Self-Hosted (VPS) | Managed SaaS CMS | |
|---|---|---|
| Monthly cost | €4–18/month (server only) | $99–$500+/month |
| Setup time | 2–4 hours (one-time) | Minutes |
| Data control | Full — your server, your DB | Vendor holds your data |
| Scaling | Manual (resize VPS or add load balancer) | Automatic (billed per usage) |
| Maintenance | You run updates and patches | Vendor handles it |
| GDPR compliance | Straightforward — data stays in EU | Depends on vendor's DPA |
If you're evaluating options before committing to a setup, comparing self-hosted vs SaaS CMS platforms covers the decision criteria in depth.
What UnfoldCMS Looks Like When Installed
If you're evaluating a self-hosted CMS built for developers, UnfoldCMS runs on this exact stack: Laravel 12 + PHP 8.3 + MySQL 8 + Nginx. The setup steps above are the actual steps from the production deployment guide.
What you get after setup:
- A React + shadcn/ui admin with 205 admin pages, no SaaS dependency
- Full data ownership — your server, your MySQL database
- One-command deploy via
php artisan deploy - Built-in SEO, media library, newsletter forms, social posting
- No per-seat pricing, no API call limits, no vendor lock-in
See pricing and features or request a demo.
Frequently Asked Questions
How long does it take to set up a self-hosted CMS?
2–4 hours for a first-time setup, including server provisioning, stack installation, CMS deployment, SSL, and basic hardening. Once you've done it once, the process takes under an hour for subsequent installs. Using a setup script or deployment tool can bring this down to 20–30 minutes.
Do I need to know Linux to self-host a CMS?
You need basic terminal comfort — copying commands, editing text files with nano or vim, and reading error output. You don't need to be a sysadmin. The commands in this guide work on any Ubuntu 22.04 or 24.04 server without modification.
What's the cheapest server that can run a self-hosted CMS?
A Hetzner CX22 (2 GB RAM, 1 vCPU, 40 GB SSD) at around €4/month handles a typical blog or small business site with up to ~50,000 monthly visits without issues, especially with OPcache enabled and static assets served from cache.
How do I handle CMS updates on a self-hosted server?
Pull the latest code from your Git repo, run composer install, run any pending migrations, and clear caches. Most modern CMS platforms include an update command that handles this in one step. UnfoldCMS uses php artisan deploy for the full update workflow.
Is self-hosting a CMS safe for GDPR compliance?
Yes — self-hosting actually makes GDPR compliance easier. Your data stays on your server in your chosen EU region, under your control. You write your own DPA instead of relying on a vendor's. The self-hosted CMS and GDPR guide covers data residency, retention, and subject access requests in detail.
Methodology and Sources
The server configuration and performance recommendations in this post come from:
- Laravel Documentation — deployment, environment configuration, and scheduling
- Nginx documentation — server block configuration and caching headers
- Let's Encrypt / Certbot documentation — SSL certificate installation
- Hetzner Cloud pricing — server cost figures (verified May 2026)
- PHP OPcache documentation — configuration parameters and performance impact
- Backblaze B2 pricing — off-site backup cost figures
This post is published on the UnfoldCMS blog. The setup steps work with any PHP-based self-hosted CMS, not only UnfoldCMS. We've run this exact configuration in production since 2025.
Step 8: Monitor Uptime and Resource Usage
A self-hosted CMS running blind is a site waiting to go down at the worst possible moment. Basic monitoring costs nothing and takes 15 minutes to set up.
Uptime monitoring — use a free external service to ping your site every minute and alert you if it goes down. UptimeRobot (free tier), Better Uptime, and Freshping all work. Create an account, add your domain, and set an email or Slack alert. You'll know about downtime before your users do.
Server resource monitoring — install htop and ncdu for quick manual checks:
sudo apt install htop ncdu -y
htop # real-time CPU + memory
ncdu / # disk usage breakdown
For persistent metrics, netdata installs in one command and gives you a real-time dashboard at port 19999:
wget -O /tmp/netdata-kickstart.sh https://my-netdata.io/kickstart.sh
sh /tmp/netdata-kickstart.sh
Block port 19999 from public access with ufw and use an SSH tunnel to view it locally: ssh -L 19999:localhost:19999 deploy@yourserver.
Log rotation — PHP, Nginx, and your CMS all write logs. Without rotation, they eventually fill your disk. Most Ubuntu installations have logrotate pre-installed, but verify your CMS logs are included:
# Add CMS log rotation
sudo nano /etc/logrotate.d/cms
/var/www/mysite/storage/logs/*.log {
daily
missingok
rotate 14
compress
delaycompress
notifempty
}
Keeping your CMS updated — security patches matter more on a self-hosted server because you're responsible for applying them. Set a monthly calendar reminder to:
- Pull the latest CMS code:
git pull origin main - Install dependency updates:
composer install --no-dev - Run pending migrations:
php artisan migrate --force - Clear caches:
php artisan optimize - Restart PHP-FPM:
sudo systemctl restart php8.3-fpm
Most teams automate this with a deployment script or CI/CD pipeline (GitHub Actions → SSH deploy on merge to main). UnfoldCMS includes php artisan deploy which handles all five steps in sequence with pre-flight checks.
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: