Back to tutorials
Tutorial

WordPress VPS setup guide tutorial (2026): Deploy WordPress on Ubuntu with Nginx, PHP-FPM, SSL, and Backups

WordPress VPS setup guide tutorial for 2026: Ubuntu, Nginx, PHP-FPM, free SSL, hardening, and backup/restore checks.

By Anurag Singh
Updated on Aug 25, 2026
Category: Tutorial
Share article
WordPress VPS setup guide tutorial (2026): Deploy WordPress on Ubuntu with Nginx, PHP-FPM, SSL, and Backups

A fresh WordPress install can feel “done” when the homepage loads. On a VPS, that’s when the real work starts.

You need sane permissions, PHP-FPM limits that won’t melt under load, HTTPS that renews itself, and backups you can restore. You also need guardrails, so one bad plugin doesn’t take the whole server down.

This WordPress VPS setup guide tutorial walks you through a production-ready build on Ubuntu with Nginx + PHP-FPM. The defaults here stay simple, but they hold up in real use.

If you want the same stack without managing the OS day-to-day, start with a managed VPS hosting plan. If you prefer full control (and you’re comfortable living in SSH), a HostMyCode VPS gives you dedicated resources and predictable performance.

What you’ll build (and what you’ll skip)

You’ll set up:

  • Ubuntu Server (tested workflow for Ubuntu 24.04 LTS-class systems commonly used in 2026)
  • Nginx + PHP-FPM with a WordPress-friendly pool configuration
  • MariaDB (local) with a locked-down WordPress database user
  • Let’s Encrypt SSL with auto-renew and a working ACME path
  • Basic hardening: firewall, SSH hygiene, file permissions, and safe headers
  • Backups (files + database) with a quick restore verification routine

You’ll skip Kubernetes, Docker orchestration, and other platform patterns. They add complexity without making a single WordPress VPS more reliable.

Prerequisites checklist before you touch the server

  • A VPS with at least 1 vCPU / 2 GB RAM for a small-to-medium WordPress site (add RAM if you run WooCommerce or heavy page builders).
  • A domain name you control. If you need one, register and manage it via HostMyCode domains.
  • DNS A/AAAA records pointing to your VPS IP.
  • SSH access as root (initially) or a sudo user.

Tip: Before cutover, drop DNS TTL to 300 seconds. It makes last-mile changes easier to undo.

If you’re migrating an existing site, keep a rollback plan nearby. The workflow in this hosting migration checklist tutorial is a solid baseline.

Step 1 — Update Ubuntu and set a sane baseline

SSH in and patch first. Don’t build a web stack on a box you haven’t updated.

sudo apt update
sudo apt -y full-upgrade
sudo reboot

After reboot:

sudo apt update
sudo apt -y install curl wget unzip ca-certificates lsb-release ufw

If SSH isn’t hardened yet, fix that now. Use keys. Disable password auth for admin. Keep sudo access tight.

HostMyCode lays out the full checklist in this server hardening tutorial.

Step 2 — Install Nginx, MariaDB, and PHP-FPM

Install the core packages. PHP versions vary by repo and policy.

For new WordPress builds in 2026, PHP 8.3+ is common.

sudo apt -y install nginx mariadb-server
sudo apt -y install php-fpm php-mysql php-curl php-gd php-mbstring php-xml php-zip php-intl php-bcmath

Enable services:

sudo systemctl enable --now nginx
sudo systemctl enable --now mariadb

At the firewall, open only what you need. Start with SSH and web traffic:

sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw --force enable
sudo ufw status

Step 3 — Create the WordPress database and user (least privilege)

Run the MariaDB secure setup. Then create a dedicated database and user.

WordPress should never log in as root.

sudo mariadb-secure-installation

Then log in and create a database:

sudo mariadb
CREATE DATABASE wordpress CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

CREATE USER 'wpuser'@'localhost' IDENTIFIED BY 'REPLACE_WITH_A_LONG_RANDOM_PASSWORD';

GRANT SELECT,INSERT,UPDATE,DELETE,CREATE,DROP,INDEX,ALTER,CREATE TEMPORARY TABLES,LOCK TABLES
ON wordpress.* TO 'wpuser'@'localhost';

FLUSH PRIVILEGES;
EXIT;

Practical rule: Save the database password in your password manager. Also store it in a root-only file on the server.

Don’t store it in a notes app or chat.

Step 4 — Create your web root with correct ownership and permissions

Use a predictable layout. For a single site:

sudo mkdir -p /var/www/example.com/public
sudo chown -R www-data:www-data /var/www/example.com
sudo find /var/www/example.com -type d -exec chmod 755 {} \;
sudo find /var/www/example.com -type f -exec chmod 644 {} \;

You’ll tighten write access after WordPress is installed. For now, these permissions keep setup straightforward.

Step 5 — Download WordPress and wire up wp-config.php

Download and extract the latest WordPress release:

cd /tmp
curl -LO https://wordpress.org/latest.tar.gz
tar -xzf latest.tar.gz

Copy into your web root:

sudo rsync -avP /tmp/wordpress/ /var/www/example.com/public/

Create wp-config.php and prepare to add strong keys:

cd /var/www/example.com/public
sudo cp wp-config-sample.php wp-config.php
sudo chown www-data:www-data wp-config.php

Edit wp-config.php:

sudo nano /var/www/example.com/public/wp-config.php

Set these values:

define('DB_NAME', 'wordpress');
define('DB_USER', 'wpuser');
define('DB_PASSWORD', 'REPLACE_WITH_A_LONG_RANDOM_PASSWORD');
define('DB_HOST', 'localhost');

define('FS_METHOD', 'direct');

Then paste fresh salts from WordPress.org:

curl -s https://api.wordpress.org/secret-key/1.1/salt/

Optional but recommended: Disable the built-in editor.

If an admin account is compromised, this removes an easy path to code injection:

define('DISALLOW_FILE_EDIT', true);

Step 6 — Configure Nginx server block for WordPress

Create a site config. This example keeps permalinks clean and routes PHP correctly.

It also blocks a few common “why is this exposed?” files.

sudo nano /etc/nginx/sites-available/example.com
server {
    listen 80;
    listen [::]:80;

    server_name example.com www.example.com;
    root /var/www/example.com/public;
    index index.php index.html;

    # Basic hardening
    location = /xmlrpc.php { deny all; }
    location ~* /(wp-config.php|readme.html|license.txt) { deny all; }
    location ~* /(?:uploads|files)/.*\.php$ { deny all; }

    # WordPress pretty permalinks
    location / {
        try_files $uri $uri/ /index.php?$args;
    }

    # PHP-FPM
    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_pass unix:/run/php/php-fpm.sock;
    }

    # Cache static assets a bit (safe and low-risk)
    location ~* \.(css|js|jpg|jpeg|png|gif|svg|ico|webp)$ {
        expires 7d;
        add_header Cache-Control "public";
        try_files $uri =404;
    }
}

Important: The PHP socket path varies by distro and PHP version.

On Ubuntu you’ll commonly see something like /run/php/php8.3-fpm.sock. Check what’s available:

ls -l /run/php/

If you see php8.3-fpm.sock, update fastcgi_pass to:

fastcgi_pass unix:/run/php/php8.3-fpm.sock;

Enable the site and reload Nginx:

sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/example.com
sudo nginx -t
sudo systemctl reload nginx

If you want a more complete Nginx baseline (timeouts, buffers, gzip, and log structure), follow this Nginx setup guide tutorial and then return here.

Step 7 — Tune PHP-FPM for WordPress (small but meaningful)

PHP-FPM defaults are cautious. That’s fine for a dev VM.

On a VPS, set clear limits. Spikes should not turn into 502s.

Find your pool file (typically /etc/php/8.3/fpm/pool.d/www.conf):

php -v
sudo ls /etc/php/*/fpm/pool.d/

Edit the pool:

sudo nano /etc/php/8.3/fpm/pool.d/www.conf

Start with these values on a 2 GB VPS. Adjust later based on RAM and traffic:

pm = dynamic
pm.max_children = 20
pm.start_servers = 4
pm.min_spare_servers = 2
pm.max_spare_servers = 6
pm.max_requests = 500

Then give PHP enough memory for updates and heavier plugins. Edit:

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

Set:

memory_limit = 256M
upload_max_filesize = 64M
post_max_size = 64M
max_execution_time = 120

Reload PHP-FPM:

sudo systemctl reload php8.3-fpm

Quick diagnostic: If you see 502 errors under load, check for pool exhaustion or crashes:

sudo journalctl -u php8.3-fpm --since "30 min ago" | tail -n 80

Step 8 — Add Let’s Encrypt SSL and make renewals boring

Install Certbot for Nginx and request a certificate:

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

Choose the redirect to HTTPS when prompted. Then verify renewal:

sudo certbot renew --dry-run

If renewals fail later, the usual causes are DNS, firewall rules, or a broken ACME location.

Follow this Let’s Encrypt setup guide tutorial for troubleshooting and a clean recovery path.

Step 9 — Finish WordPress install and lock down file writes

Open https://example.com and complete the installer.

Use a unique admin username (not “admin”) and a long password.

Once the site works, tighten file permissions. The goal is simple.

If a plugin is compromised, it shouldn’t be able to overwrite core PHP files.

A practical baseline for single-site servers:

  • Keep WordPress files owned by root
  • Allow www-data to write only to wp-content/uploads (and sometimes cache directories)
sudo chown -R root:root /var/www/example.com/public
sudo chown -R www-data:www-data /var/www/example.com/public/wp-content/uploads
sudo find /var/www/example.com/public -type d -exec chmod 755 {} \;
sudo find /var/www/example.com/public -type f -exec chmod 644 {} \;

If your caching plugin needs write access (common), grant it only to the specific cache directory it uses. Don’t “chmod 777” anything.

That shortcut tends to show up again at 3 a.m.

Step 10 — Add security headers (without breaking the admin area)

Security headers won’t compensate for weak passwords. They do reduce browser-side attack surface.

Keep this first pass simple and low-risk.

Add these headers inside your SSL server block (or in the server block if you’re not splitting configs):

sudo nano /etc/nginx/sites-available/example.com
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;

Reload and validate:

sudo nginx -t
sudo systemctl reload nginx

If you want a stricter CSP and HSTS plan (and you want to avoid locking yourself into HTTPS during testing), use this Nginx security headers configuration tutorial.

Step 11 — Set up backups you can restore (files + DB)

Backups usually fail in two ways. Either they don’t run, or they run but nobody tests a restore.

This section addresses both.

Option A: Lightweight on-server backups (good baseline)

Create a root-only backup directory and a simple script.

This isn’t your final 3-2-1 plan. It is an immediate safety net.

sudo mkdir -p /root/backups
sudo chmod 700 /root/backups

Create /root/backups/wp-backup.sh:

sudo nano /root/backups/wp-backup.sh
#!/usr/bin/env bash
set -euo pipefail

SITE="example.com"
WEBROOT="/var/www/${SITE}/public"
BACKUP_DIR="/root/backups"
DATE="$(date +%F_%H%M)"

DB="wordpress"
DB_USER="wpuser"
DB_PASS="REPLACE_WITH_A_LONG_RANDOM_PASSWORD"

# Database dump
mysqldump -u"${DB_USER}" -p"${DB_PASS}" --single-transaction --quick "${DB}" \
  | gzip > "${BACKUP_DIR}/${SITE}_db_${DATE}.sql.gz"

# Files archive (exclude cache)
tar --exclude='wp-content/cache' --exclude='wp-content/uploads/cache' \
  -czf "${BACKUP_DIR}/${SITE}_files_${DATE}.tar.gz" -C "${WEBROOT}" .

# Retention: keep 14 days
find "${BACKUP_DIR}" -type f -name "${SITE}_*" -mtime +14 -delete

Lock permissions and test:

sudo chmod 700 /root/backups/wp-backup.sh
sudo /root/backups/wp-backup.sh
sudo ls -lh /root/backups | tail

Schedule daily backups via cron:

sudo crontab -e
15 2 * * * /root/backups/wp-backup.sh >/var/log/wp-backup.log 2>&1

Reality check: These backups live on the same VPS.

They help with “oops, I deleted something,” not disk failure or ransomware.

For offsite copies, encryption, and retention that won’t fill your server, follow HostMyCode’s VPS backup strategy tutorial.

Restore verification (10 minutes, saves hours later)

Pick a temporary folder and confirm you can extract the tarball:

sudo mkdir -p /root/restore-test
sudo tar -xzf /root/backups/example.com_files_*.tar.gz -C /root/restore-test
sudo ls -la /root/restore-test | head

For database restore testing, restore into a throwaway database on the same server (or a staging VPS):

sudo mariadb -e "CREATE DATABASE wp_restore_test CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
gunzip -c /root/backups/example.com_db_*.sql.gz | sudo mariadb wp_restore_test

You’re not proving perfection here. You’re proving the backups aren’t empty, corrupted, or encrypted with a key you can’t find.

Step 12 — Performance quick wins that don’t create new problems

A WordPress VPS doesn’t need 30 tuning knobs. It needs a few settings that reliably reduce CPU spikes and PHP work.

  • Enable a page cache (plugin-based is fine on single-server setups). Aim to reduce PHP requests for anonymous traffic by 80–95%.
  • Keep PHP-FPM worker counts realistic. Too many workers can OOM your VPS and make everything slower than “not enough workers.”
  • Use HTTP/2 and TLS defaults from Certbot. Don’t hand-edit cipher lists unless you have a specific reason.
  • Turn on object caching only if you have a plan (Redis adds another service to patch and monitor). For many small sites, page caching is the bigger win.

If you want a structured tuning runbook (including measuring TTFB changes and spotting slow PHP requests), use this VPS performance optimization tutorial.

Step 13 — Common mistakes and fast troubleshooting

502 Bad Gateway after enabling the site

  • Confirm PHP-FPM is running: systemctl status php8.3-fpm
  • Confirm socket path matches fastcgi_pass: ls -l /run/php/
  • Check recent errors: sudo tail -n 120 /var/log/nginx/error.log

SSL issues: redirect loops or mixed content

  • Confirm WordPress Address/Site Address are both https://.
  • If you’re behind a proxy/CDN, ensure WordPress sees the correct scheme. Otherwise it may “think” it’s on HTTP.

WordPress can’t write to uploads/themes

  • Check ownership of wp-content/uploads.
  • Don’t open permissions globally. Fix the one directory that needs it.

High CPU during login or wp-admin use

  • Install a basic caching plugin and confirm it bypasses cache for logged-in users.
  • Check for brute force attempts. If you see thousands of hits to /wp-login.php, rate-limiting helps.

Wrap-up: your production-ready baseline

You now have a WordPress server that’s set up like a server.

You have Nginx + PHP-FPM, HTTPS with renewals, tighter file permissions, and backups you’ve sanity-checked.

That gap—between “it loads” and “it stays up”—is the whole point.

If you want a clean VPS built for hosting workloads, start with a HostMyCode VPS. If you’d rather have patching, monitoring, and recovery handled with you, choose managed VPS hosting from HostMyCode (Affordable & Reliable Hosting) and keep your attention on the site.

If you’re setting up WordPress for a client site or a business site, prioritize stability over clever tweaks. HostMyCode offers VPS plans with consistent resources, plus managed VPS hosting if you want help with updates, security, and uptime essentials. For WordPress-first deployments, you can also consider HostMyCode WordPress hosting when a simpler setup fits your needs.

FAQ

Should you run WordPress on shared hosting or a VPS?

Shared hosting fits small sites with predictable traffic and minimal customization. A VPS is the better choice when you need stable CPU/RAM, custom Nginx/PHP tuning, or stricter isolation for client sites.

What’s the minimum VPS size for WordPress in 2026?

For a basic site, 1 vCPU and 2 GB RAM is a realistic starting point. WooCommerce, heavy plugins, or higher concurrency usually need 2+ vCPU and 4 GB+ RAM.

Do you need Redis for WordPress performance?

Not always. Page caching typically delivers the biggest improvement first. Add Redis only if you’ve measured slow dynamic requests and you’re ready to maintain another service.

How do you confirm Let’s Encrypt renewals won’t fail?

Run sudo certbot renew --dry-run. Then confirm ports 80 and 443 are reachable publicly and that your DNS A/AAAA records point to the VPS.

What’s the safest way to test backups?

Extract a file archive to a temporary directory and restore a database dump into a throwaway database (or a staging VPS). If you can’t restore, you don’t have backups.