Web Hosting

How to host WordPress on a cloud VPS

Shared hosting is convenient but limits performance, configuration, and scalability. Moving WordPress to a VPS gives you dedicated resources, root access, and the ability to tune every layer — from PHP-FPM workers to nginx caching. This guide walks you through a production-ready LEMP stack on Ubuntu 22.04, from a fresh server to a TLS-secured WordPress site.

Step 1 — Choose and provision your VPS

WordPress is lightweight to start but grows with traffic and plugins. A sensible starting point:

  • 2 GB RAM minimum; 4 GB gives comfortable headroom for PHP processes and MySQL
  • 2 vCPU handles concurrent visitors without PHP-FPM timeouts
  • 25+ GB SSD for OS, database, media uploads, and backups
  • Ubuntu 22.04 LTS — LTS release, wide package support

Before anything else, follow our VPS security guide to create a sudo user, disable root SSH login, and enable UFW. Do that first — it takes 10 minutes and matters.

Step 2 — Install the LEMP stack

LEMP = Linux + nginx + MariaDB + PHP-FPM. MariaDB is a drop-in MySQL replacement with better performance on smaller servers.

sudo apt update && sudo apt upgrade -y
sudo apt install -y nginx mariadb-server php8.3-fpm \
  php8.3-mysql php8.3-xml php8.3-mbstring php8.3-curl \
  php8.3-zip php8.3-gd php8.3-intl php8.3-bcmath

# Start and enable services
sudo systemctl enable --now nginx php8.3-fpm mariadb

Secure MariaDB

sudo mysql_secure_installation
# Answer: set root password, remove anonymous users,
# disallow remote root, remove test DB, reload privileges

Create the WordPress database

sudo mysql -u root -p
-- Inside MariaDB shell:
CREATE DATABASE wordpress CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'wpuser'@'localhost' IDENTIFIED BY 'strong_password_here';
GRANT ALL PRIVILEGES ON wordpress.* TO 'wpuser'@'localhost';
FLUSH PRIVILEGES;
EXIT;

Step 3 — Download and configure WordPress

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

# Create wp-config.php from the sample
cd /var/www/yourdomain.com
sudo cp wp-config-sample.php wp-config.php
sudo nano wp-config.php
# Edit: DB_NAME=wordpress, DB_USER=wpuser, DB_PASSWORD=strong_password_here

Step 4 — Configure an nginx server block

sudo tee /etc/nginx/sites-available/yourdomain.com > /dev/null <<'EOF'
server {
    listen 80;
    server_name yourdomain.com www.yourdomain.com;
    root /var/www/yourdomain.com;
    index index.php index.html;

    location / {
        try_files $uri $uri/ /index.php?$args;
    }

    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
    }

    location ~ /\.ht {
        deny all;
    }

    # Cache static assets
    location ~* \.(jpg|jpeg|png|gif|ico|css|js|woff2)$ {
        expires 30d;
        add_header Cache-Control "public, no-transform";
    }
}
EOF

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

Step 5 — Free TLS with Certbot

Let's Encrypt issues free TLS certificates. Certbot automates issuance and renewal:

# Confirm DNS resolves to THIS server before requesting a certificate
dig +short yourdomain.com          # must print this server's IP
curl -s ifconfig.me                # this server's public IP

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

# Test auto-renewal
sudo certbot renew --dry-run

Check DNS first — a failed lookup can stick for 30 minutes

Certbot proves you control the domain by answering a request to it, so the record has to resolve before you run it. The trap is what happens if you run it too early: a “does not exist” answer gets cached. Cloudflare-hosted zones publish a 30-minute negative-cache TTL by default, so one premature lookup can make the name appear missing for half an hour on that resolver, long after the record is live. Verify with dig first; if you have already queried too early, sudo resolvectl flush-caches or simply wait it out.

Certbot patches your nginx config automatically to redirect HTTP to HTTPS and sets up a systemd timer for renewal. You should not need to touch the certificate again.

Step 6 — Basic hardening

# Disable XML-RPC if you don't use remote publishing
# Add to your nginx server block:
location = /xmlrpc.php {
    deny all;
}

# Limit wp-login.php to your IP (optional)
location = /wp-login.php {
    allow YOUR.IP.ADDRESS;
    deny all;
    include snippets/fastcgi-php.conf;
    fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}

sudo systemctl reload nginx

Also install a security plugin (Wordfence or Solid Security), keep WordPress, themes, and plugins updated, and configure automated daily database backups to off-site storage (e.g. Backblaze B2 or an S3-compatible bucket).

Troubleshooting — what actually goes wrong

We install WordPress on fresh, disposable VPS instances continuously to test our own provisioning, so we see the same handful of failures over and over. Below are the ones that come up most, with the exact error text each produces — because that string is what you will be pasting into a search box.

nginx won't start: address already in use

nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address already in use)

Something already holds port 80. On Ubuntu the usual culprit is Apache, pulled in as a dependency of an unrelated package and started automatically. It can equally be a second nginx, or a container publishing -p 80:80. Whatever binds the port first wins, and nginx simply refuses to start. Find the owner before changing any config:

sudo ss -ltnp | grep -E ':80|:443'    # who owns the ports

# If it is Apache and you are not using it:
sudo systemctl disable --now apache2
sudo systemctl restart nginx

The same rule bites in the other direction later: if you add anything that expects port 80 — a Docker container, another site's proxy — it will lose to nginx and fail with the mirror-image of this error. On a server with a reverse proxy, applications should listen on a high port and let the proxy route to them.

Plugin installs and auto-updates fail silently

Warning: Could not create directory. "/var/www/wordpress/wp-content/upgrade"
Warning: The 'woocommerce' plugin could not be found.
Error: No plugins installed.

This is a file-ownership problem, not a WordPress problem. PHP-FPM runs as www-data, so WordPress's own updater can only write files that user owns. The moment anything runs as root — an unpacked archive, a mkdir, a WP-CLI command run with sudo — it leaves root-owned files behind, and every later install or update fails on them. A root-owned wp-content/upgrade directory is worse than a missing one, because WordPress will happily create the missing one itself.

sudo chown -R www-data:www-data /var/www/wordpress
sudo find /var/www/wordpress -type d -exec chmod 755 {} \;
sudo find /var/www/wordpress -type f -exec chmod 644 {} \;

# If you use WP-CLI, run it AS that user — never as root:
sudo -u www-data wp plugin install woocommerce --activate

WP-CLI: “site you have requested is not installed”

Error: The site you have requested is not installed.
Run `wp core install` to create database tables.

Downloading WordPress and writing wp-config.php does not create the database schema — the browser installer normally does that on first visit. If you are scripting the setup instead, every other WP-CLI command will refuse until you run the install explicitly:

sudo -u www-data wp core install \
  --url="https://yourdomain.com" \
  --title="Your Site" \
  --admin_user=admin \
  --admin_password="$(openssl rand -hex 16)" \
  --admin_email=you@example.com

502 Bad Gateway after a clean install

nginx is running and answering, but cannot reach PHP. Nearly always the socket path in your server block names a different PHP version than the one actually installed — the config in Step 4 hardcodes php8.3-fpm.sock, so installing PHP 8.2 or 8.4 instead breaks it. Check what exists rather than what you expect:

ls /run/php/                          # the socket that actually exists
sudo nginx -t                         # config syntax + paths
sudo systemctl status php8.3-fpm      # is the pool even running?

One timing note worth knowing: on a brand-new site it is tempting to blame TLS when the page does not load. In our measurements Let's Encrypt issued a certificate for a fresh subdomain in about seven seconds, including account registration. The certificate is rarely the slow part — a 502 during the first minutes almost always means the backend has not finished starting.

Want a pre-configured server?

AgentOcean VPS plans come security-hardened and ready to install WordPress or any other stack — no manual OS configuration required.

Related