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 mariadbSecure MariaDB
sudo mysql_secure_installation
# Answer: set root password, remove anonymous users,
# disallow remote root, remove test DB, reload privilegesCreate 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_hereStep 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 nginxStep 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-runCheck 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 nginxAlso 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 nginxThe 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 --activateWP-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.com502 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.