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.
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.
Related reading
// new_articles
Get notified when new guides drop
Practical WordPress guides from a working agency owner. No filler. Unsubscribe any time.
Was this article helpful?
Thanks for the feedback!