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

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.


Was this article helpful?