WordPress on a VPS - Complete Setup Guide (Ubuntu + Nginx + PHP-FPM)

Set up WordPress on a VPS with full performance control. A production-ready Ubuntu + Nginx + PHP-FPM + MariaDB stack guide from scratch.

Dobromir Dechev
Dobromir WordPress agency owner

Quick answer

Running WordPress on a VPS means installing and maintaining Nginx, PHP-FPM, MariaDB and Let's Encrypt yourself. It is meaningfully cheaper and faster than managed hosting — provided you genuinely absorb the security patching and recovery responsibility.

Managed WordPress hosting handles the server for you - that is its value proposition. But VPS hosting gives you control that no managed host matches: custom server configuration, full SSH access, multiple PHP versions per site, and significantly lower cost per site at scale.

This guide covers setting up a production-ready WordPress stack on Ubuntu 24.04 LTS. The stack: Nginx + PHP-FPM + MariaDB + Let's Encrypt SSL.


Prerequisites

  • A VPS from DigitalOcean, Linode, Vultr, or Hetzner (2GB RAM minimum for a single site; 4GB+ for multiple sites)

Looking for an easier setup? If you want to run multiple WordPress sites on a Hetzner VPS without manually configuring Nginx and PHP-FPM for each site, see the Hetzner + CloudPanel guide — it achieves better benchmark results with a significantly faster site-creation workflow. This guide covers the manual setup for cases where you need full control over the stack.

  • Ubuntu 24.04 LTS
  • SSH access as root (we will create a non-root user)
  • A domain pointed at the VPS IP address

Step 1 - Initial server setup

Create a non-root user

adduser deploy
usermod -aG sudo deploy

# Copy SSH keys from root to new user
rsync --archive --chown=deploy:deploy ~/.ssh /home/deploy

Log out and log back in as deploy.

Configure the firewall

sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status

Update the system

sudo apt-get update && sudo apt-get upgrade -y
sudo apt-get install -y software-properties-common curl wget unzip git

Step 2 - Install Nginx

# Add official Nginx repository for latest stable version
curl https://nginx.org/keys/nginx_signing.key | gpg --dearmor | sudo tee /usr/share/keyrings/nginx-archive-keyring.gpg

echo "deb [signed-by=/usr/share/keyrings/nginx-archive-keyring.gpg] http://nginx.org/packages/ubuntu $(lsb_release -cs) nginx" | sudo tee /etc/apt/sources.list.d/nginx.list

sudo apt-get update
sudo apt-get install -y nginx

sudo systemctl enable nginx
sudo systemctl start nginx

Verify Nginx is running:

curl -I http://[your-server-ip]
# Should return: HTTP/1.1 200 OK

Step 3 - Install PHP-FPM

WordPress requires PHP. Install PHP 8.3 with all required extensions:

sudo add-apt-repository ppa:ondrej/php
sudo apt-get update

sudo apt-get install -y \
    php8.3-fpm \
    php8.3-mysql \
    php8.3-xml \
    php8.3-gd \
    php8.3-curl \
    php8.3-zip \
    php8.3-mbstring \
    php8.3-intl \
    php8.3-bcmath \
    php8.3-imagick \
    php8.3-opcache \
    php8.3-redis

Configure PHP-FPM for WordPress:

sudo nano /etc/php/8.3/fpm/php.ini

Key settings:

memory_limit = 256M
upload_max_filesize = 64M
post_max_size = 64M
max_execution_time = 300
max_input_vars = 5000
date.timezone = Europe/London

Configure PHP-FPM pool:

sudo nano /etc/php/8.3/fpm/pool.d/www.conf
user = www-data
group = www-data

; Dynamic process manager - adjust based on RAM
pm = dynamic
pm.max_children = 20
pm.start_servers = 5
pm.min_spare_servers = 5
pm.max_spare_servers = 10
pm.max_requests = 500

Restart PHP-FPM:

sudo systemctl restart php8.3-fpm

Step 4 - Install MariaDB

sudo apt-get install -y mariadb-server mariadb-client

sudo systemctl enable mariadb
sudo systemctl start mariadb

# Secure the installation
sudo mysql_secure_installation

Create the WordPress database:

sudo mysql -u root -p
CREATE DATABASE wordpress_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'wp_user'@'localhost' IDENTIFIED BY 'strong-password-here';
GRANT ALL PRIVILEGES ON wordpress_db.* TO 'wp_user'@'localhost';
FLUSH PRIVILEGES;
EXIT;

Step 5 - Configure Nginx for WordPress

Create the server block:

sudo mkdir -p /var/www/yourdomain.com
sudo chown -R www-data:www-data /var/www/yourdomain.com

sudo nano /etc/nginx/sites-available/yourdomain.com.conf
server {
    listen 80;
    server_name yourdomain.com www.yourdomain.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    http2 on;

    server_name yourdomain.com www.yourdomain.com;
    root /var/www/yourdomain.com;
    index index.php;

    # SSL (managed by Certbot)
    ssl_certificate     /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
    include             /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam         /etc/letsencrypt/ssl-dhparams.pem;

    # Logs
    access_log /var/log/nginx/yourdomain.com.access.log;
    error_log  /var/log/nginx/yourdomain.com.error.log warn;

    # Security headers
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

    # WordPress rewrite rules
    location / {
        try_files $uri $uri/ /index.php?$args;
    }

    # PHP processing
    location ~ \.php$ {
        include fastcgi_params;
        fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_read_timeout 300;
    }

    # Security: block sensitive files
    location = /wp-config.php { deny all; }
    location ~* /\.ht { deny all; }
    location = /xmlrpc.php { deny all; access_log off; }
    location ~* /wp-content/uploads/.*\.php$ { deny all; }

    # Static file caching
    location ~* \.(jpg|jpeg|gif|png|ico|webp|avif|svg|css|js|woff2?)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
        access_log off;
    }

    # Rate limit login page
    location = /wp-login.php {
        limit_req zone=login burst=3 nodelay;
        include fastcgi_params;
        fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }

    # Gzip
    gzip on;
    gzip_types text/plain text/css application/json application/javascript text/xml application/xml image/svg+xml;
    gzip_comp_level 6;
}

Add the rate limit zone to /etc/nginx/nginx.conf inside the http block:

limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;

Enable the site:

sudo ln -s /etc/nginx/sites-available/yourdomain.com.conf /etc/nginx/sites-enabled/
sudo nginx -t
sudo nginx -s reload

Step 6 - Install SSL with Let's Encrypt

sudo apt-get install -y certbot python3-certbot-nginx

sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com \
    --non-interactive --agree-tos --email [email protected]

Certbot automatically modifies the Nginx config and sets up auto-renewal. Verify renewal:

sudo certbot renew --dry-run

Step 7 - Install WordPress

cd /tmp
curl -O https://wordpress.org/latest.tar.gz
tar xzf latest.tar.gz
sudo mv wordpress/* /var/www/yourdomain.com/
sudo chown -R www-data:www-data /var/www/yourdomain.com
sudo chmod -R 755 /var/www/yourdomain.com

Install WP-CLI:

curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
chmod +x wp-cli.phar
sudo mv wp-cli.phar /usr/local/bin/wp

Configure WordPress:

cd /var/www/yourdomain.com

wp config create \
    --dbname=wordpress_db \
    --dbuser=wp_user \
    --dbpass='strong-password-here' \
    --dbhost=localhost \
    --allow-root

wp core install \
    --url=https://yourdomain.com \
    --title="Your Site Title" \
    --admin_user=your_admin \
    --admin_password='admin-strong-password' \
    [email protected] \
    --allow-root

Step 8 - Install Redis for object cache

sudo apt-get install -y redis-server php8.3-redis

sudo systemctl enable redis-server
sudo systemctl start redis-server

Install the Redis Object Cache plugin via WP-CLI:

wp plugin install redis-cache --activate --allow-root
wp redis enable --allow-root

Add to wp-config.php:

define( 'WP_CACHE', true );
define( 'WP_REDIS_HOST', '127.0.0.1' );
define( 'WP_REDIS_PORT', 6379 );

Step 9 - Set up automated backups

# Install WP-CLI package for backup
wp package install wp-cli/wp-cli-bundle --allow-root

# Create backup script
sudo nano /usr/local/bin/wp-backup.sh
#!/bin/bash
SITE_PATH="/var/www/yourdomain.com"
BACKUP_DIR="/var/backups/wordpress"
DATE=$(date +%Y%m%d-%H%M%S)

mkdir -p "$BACKUP_DIR"

# Database backup
wp db export "$BACKUP_DIR/db-$DATE.sql" --path="$SITE_PATH" --allow-root

# Compress
gzip "$BACKUP_DIR/db-$DATE.sql"

# Keep only 30 days
find "$BACKUP_DIR" -name "db-*.sql.gz" -mtime +30 -delete
sudo chmod +x /usr/local/bin/wp-backup.sh

# Schedule daily at 2am
echo "0 2 * * * root /usr/local/bin/wp-backup.sh" | sudo tee /etc/cron.d/wp-backup

Add a step to sync to off-site storage (S3, Backblaze) using rclone or the AWS CLI.


Monitoring and maintenance

# Check Nginx error log for issues
sudo tail -f /var/log/nginx/yourdomain.com.error.log

# Check PHP-FPM
sudo systemctl status php8.3-fpm

# Check disk usage
df -h

# Check memory
free -m

Set up UptimeRobot (free) to alert you when the site goes down. Configure it to ping https://yourdomain.com every 5 minutes.


Frequently Asked Questions

Is a VPS better than managed WordPress hosting?
On performance per euro, yes — a well-configured VPS beats managed hosting at a fraction of the price. What you take on is OS patching, security configuration, backups, monitoring and recovery at 2am. Managed hosting is correct when that operational burden costs you more than the price difference.
What stack should I run WordPress on?
Nginx with PHP-FPM, MariaDB, and Let's Encrypt for SSL is the standard, well-documented combination. Add Redis for object caching and a full-page cache layer. The stack matters less than keeping it patched — an unmaintained modern stack is worse than a maintained conventional one.
How much VPS do I need for WordPress?
A small instance with 2GB RAM comfortably runs several low-traffic sites. Requirements scale with uncached traffic rather than site count, since cached pages are cheap and PHP execution is not — a single busy WooCommerce store can demand more than ten brochure sites combined.
Is a self-managed VPS secure enough for client sites?
It can be more secure than shared hosting, since you are not sharing a server with unknown neighbours. That depends entirely on you applying OS updates, using key-based SSH, configuring a firewall, and monitoring. An unpatched VPS is considerably worse than any managed host.
Do I need a control panel like CloudPanel for a WordPress VPS?
Not strictly, but it removes most of the repetitive work — virtual hosts, SSL renewal, database creation and PHP version switching through a UI rather than by hand. For anyone not administering servers daily, a control panel is the difference between a VPS you maintain and one you quietly stop maintaining.

Was this article helpful?