How to Install WordPress on a VPS: Ubuntu 24.04 Step-by-Step Guide

Devops & Infrastructure, Tutorials, VPS, and Wordpress

How to Install WordPress on a VPS: Ubuntu 24.04 Step-by-Step Guide

Setting up WordPress on a VPS gives you full control over performance, security, and scaling -- something shared hosting can never match. This guide walks you through every step on a fresh Ubuntu 24.04 server, using Nginx, PHP 8.3, MySQL 8.0, and Let's Encrypt SSL. By the end, you will have a production-ready WordPress installation with Redis object caching, OPcache tuning, and proper security hardening. Not sure if you're ready for the move? Here are 5 signs it's time to upgrade from shared hosting to automated deployments.

If you are new to deploying websites to servers, our beginner's guide to website deployment covers the fundamentals before you dive in here.

Prerequisites

Before you start, make sure you have:

  • A VPS running Ubuntu 24.04 LTS from any provider (DigitalOcean, Linode, Hetzner, Vultr -- the steps are identical)
  • A domain name with DNS A record pointing to your server's IP address
  • SSH access to the server as root or a sudo-capable user
  • A local terminal -- if you are on Windows, OpenSSH is now built in and works perfectly

Minimum server specs for a WordPress site handling moderate traffic: 1 vCPU, 1 GB RAM, 25 GB SSD. For sites expecting heavier load or running WooCommerce, start with 2 vCPUs and 2 GB RAM.

Initial Server Setup

SSH into your server:

ssh root@your-server-ip

Update the system

First, bring everything up to date. Ubuntu 24.04 ships with recent packages, but there are always security patches waiting:

apt update && apt upgrade -y

Create a deploy user

Running everything as root is a security risk. Create a dedicated user with sudo privileges:

adduser deploy
usermod -aG sudo deploy

Set up SSH key authentication

Copy your public key to the new user so you can log in without a password:

mkdir -p /home/deploy/.ssh
cp /root/.ssh/authorized_keys /home/deploy/.ssh/
chown -R deploy:deploy /home/deploy/.ssh
chmod 700 /home/deploy/.ssh
chmod 600 /home/deploy/.ssh/authorized_keys

Disable root login and password authentication

Edit the SSH config to lock things down:

nano /etc/ssh/sshd_config

Find and set these values:

PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes

Restart SSH to apply:

systemctl restart ssh

Test this from a new terminal window before closing your current session. If something went wrong, you still have your root session open to fix it:

ssh deploy@your-server-ip

From this point forward, all commands run as the deploy user with sudo.


Install Nginx

Apache dominated WordPress hosting for years, but Nginx handles concurrent connections far more efficiently. It uses an event-driven architecture instead of spawning a process per request, which means lower memory usage and faster static file serving -- exactly what WordPress needs.

sudo apt install nginx -y

Verify it is running:

sudo systemctl status nginx

Visit your server's IP in a browser. You should see the default Nginx welcome page.


Install PHP 8.3

Ubuntu 24.04 ships PHP 8.3 in its default repositories, so no third-party PPAs are needed. WordPress fully supports PHP 8.3, and it runs roughly 20% faster than PHP 8.1 thanks to JIT improvements and internal optimisations.

Install PHP-FPM and every extension WordPress core and major plugins expect:

sudo apt install php8.3-fpm php8.3-mysql php8.3-curl php8.3-gd \
  php8.3-mbstring php8.3-xml php8.3-zip php8.3-intl php8.3-opcache \
  php8.3-redis php8.3-imagick php8.3-bcmath -y

Here is what each extension does:

Extension Purpose
php-fpm FastCGI process manager -- Nginx delegates PHP processing to this
php-mysql MySQL/MariaDB database driver
php-curl HTTP requests (plugin updates, REST API calls)
php-gd Image manipulation (thumbnail generation, cropping)
php-mbstring Multi-byte string handling (UTF-8 content)
php-xml XML parsing (RSS feeds, sitemaps, XML-RPC)
php-zip Plugin and theme ZIP file handling
php-intl Internationalisation (date formatting, transliteration)
php-opcache Bytecode caching (massive performance boost)
php-redis Redis object cache integration
php-imagick Advanced image processing (WebP conversion, PDF thumbnails)
php-bcmath Arbitrary precision math (WooCommerce calculations)

Verify PHP is working:

php -v

You should see PHP 8.3.x in the output.


Install MySQL 8.0 and Create the WordPress Database

sudo apt install mysql-server -y

Secure the installation:

sudo mysql_secure_installation

Follow the prompts: set a root password, remove anonymous users, disable remote root login, and remove the test database. Say yes to all of them.

Now create a database and user specifically for WordPress. Never use the root MySQL user for your application -- if WordPress gets compromised, the attacker would have full database server access:

sudo mysql
CREATE DATABASE wordpress DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'wp_user'@'localhost' IDENTIFIED BY 'your-strong-password-here';
GRANT ALL PRIVILEGES ON wordpress.* TO 'wp_user'@'localhost';
FLUSH PRIVILEGES;
EXIT;

Replace your-strong-password-here with a genuinely strong password. Use openssl rand -base64 24 to generate one if you need inspiration.

The utf8mb4 character set supports the full Unicode range including emoji -- WordPress has required this since version 4.2.


Download and Configure WordPress

Download WordPress

cd /tmp
curl -O https://wordpress.org/latest.tar.gz
tar -xzf latest.tar.gz
sudo mv wordpress /var/www/wordpress

Set file ownership

Nginx runs as the www-data user, so WordPress needs to be owned by that user to handle uploads, plugin installs, and updates:

sudo chown -R www-data:www-data /var/www/wordpress

Configure wp-config.php

cd /var/www/wordpress
sudo cp wp-config-sample.php wp-config.php
sudo nano wp-config.php

Update the database connection settings:

define( 'DB_NAME', 'wordpress' );
define( 'DB_USER', 'wp_user' );
define( 'DB_PASSWORD', 'your-strong-password-here' );
define( 'DB_HOST', 'localhost' );
define( 'DB_CHARSET', 'utf8mb4' );
define( 'DB_COLLATE', '' );

Generate unique security salts

WordPress uses these to encrypt cookies and session tokens. Never leave the defaults in place:

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

Copy the entire output and paste it into wp-config.php, replacing the placeholder salt lines.

Add performance and security constants

Add these lines above the /* That's all, stop editing! */ comment:

/** Disable the file editor in wp-admin (security) */
define( 'DISALLOW_FILE_EDIT', true );

/** Limit post revisions to save database space */
define( 'WP_POST_REVISIONS', 10 );

/** Set WordPress memory limit */
define( 'WP_MEMORY_LIMIT', '256M' );

Configure the Nginx Server Block

This is where most guides get WordPress wrong. The Nginx configuration needs to handle pretty permalinks, pass PHP to FPM correctly, and block access to sensitive files.

Create the server block:

sudo nano /etc/nginx/sites-available/wordpress

Paste this configuration:

server {
    listen 80;
    listen [::]:80;

    server_name yourdomain.com www.yourdomain.com;
    root /var/www/wordpress;
    index index.php index.html;

    # Logging
    access_log /var/log/nginx/wordpress_access.log;
    error_log /var/log/nginx/wordpress_error.log;

    # Max upload size -- matches WordPress default
    client_max_body_size 64M;

    # Main location block -- handles pretty permalinks
    location / {
        try_files $uri $uri/ /index.php?$args;
    }

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

    # Deny access to sensitive files
    location ~ /\.ht {
        deny all;
    }

    location = /wp-config.php {
        deny all;
    }

    location ~* /(?:uploads|files)/.*\.php$ {
        deny all;
    }

    # Cache static assets
    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|webp|woff|woff2|ttf|eot)$ {
        expires 30d;
        add_header Cache-Control "public, immutable";
        access_log off;
    }

    # Disable XML-RPC (prevents brute force attacks)
    location = /xmlrpc.php {
        deny all;
        access_log off;
        log_not_found off;
    }
}

The try_files $uri $uri/ /index.php?$args; directive is the key line. It tells Nginx to first look for a matching file, then a directory, and if neither exists, pass the request to WordPress's index.php -- which is how pretty permalinks work.

Enable the site and test:

sudo ln -s /etc/nginx/sites-available/wordpress /etc/nginx/sites-enabled/
sudo rm /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl reload nginx

Visit http://yourdomain.com in a browser. You should see the WordPress installation wizard. Complete it, but do not log in yet -- we need to set up SSL first.


SSL with Let's Encrypt

Every site needs HTTPS. Let's Encrypt provides free, auto-renewing certificates, and Certbot makes the process trivial.

sudo apt install certbot python3-certbot-nginx -y

Request a certificate:

sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com

Certbot will:

  1. Verify you own the domain via an HTTP challenge
  2. Generate the SSL certificate
  3. Automatically update your Nginx config to redirect HTTP to HTTPS
  4. Set up a cron job for auto-renewal

Verify auto-renewal is working:

sudo certbot renew --dry-run

After SSL is active, add this line to your wp-config.php to force WordPress to use HTTPS:

define( 'FORCE_SSL_ADMIN', true );

Performance Tuning

A stock WordPress install is not slow -- but it is not fast either. These three changes will cut page load times significantly.

OPcache Configuration

OPcache stores compiled PHP bytecode in memory so PHP does not have to re-parse every file on every request. It is already installed, but the defaults are too conservative for WordPress:

sudo nano /etc/php/8.3/fpm/conf.d/10-opcache.ini
opcache.enable=1
opcache.memory_consumption=128
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=10000
opcache.revalidate_freq=60
opcache.save_comments=1
opcache.enable_cli=0

Key settings explained:

  • memory_consumption=128: 128 MB for cached scripts -- enough for WordPress core + 20-30 plugins
  • max_accelerated_files=10000: WordPress core alone has ~3,000 files; this leaves room for plugins
  • revalidate_freq=60: Check for file changes every 60 seconds instead of every request
  • save_comments=1: Required -- many WordPress plugins use docblock annotations

Redis Object Cache

WordPress makes dozens of database queries per page load. Redis stores these query results in memory, so repeated requests skip the database entirely. The difference is dramatic on pages with complex queries like WooCommerce product listings.

Install Redis:

sudo apt install redis-server -y

Configure Redis to use a Unix socket (faster than TCP for local connections):

sudo nano /etc/redis/redis.conf

Find and update these lines:

unixsocket /run/redis/redis-server.sock
unixsocketperm 770

Add the www-data user to the redis group so PHP-FPM can access the socket:

sudo usermod -aG redis www-data

Restart Redis:

sudo systemctl restart redis-server

Add Redis configuration to wp-config.php:

define( 'WP_REDIS_SCHEME', 'unix' );
define( 'WP_REDIS_PATH', '/run/redis/redis-server.sock' );

Install WP-CLI (the WordPress command-line interface) and activate the Redis plugin:

cd /tmp
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

cd /var/www/wordpress
sudo -u www-data wp plugin install redis-cache --activate
sudo -u www-data wp redis enable

Verify it is working:

sudo -u www-data wp redis status

You should see Status: Connected.

Nginx FastCGI Cache

FastCGI caching stores the full rendered HTML output in Nginx's memory, bypassing both PHP and WordPress entirely for cached pages. This turns a 200ms dynamic page into a 5ms static response.

Add a cache zone definition to the top of your Nginx config (outside the server block):

sudo nano /etc/nginx/sites-available/wordpress

Add above the server { block:

fastcgi_cache_path /tmp/nginx-cache levels=1:2 keys_zone=WORDPRESS:100m inactive=60m;
fastcgi_cache_key "$scheme$request_method$host$request_uri";

Inside the server block, update the PHP location:

location ~ \.php$ {
    include snippets/fastcgi-php.conf;
    fastcgi_pass unix:/run/php/php8.3-fpm.sock;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    include fastcgi_params;

    # FastCGI cache
    fastcgi_cache WORDPRESS;
    fastcgi_cache_valid 200 60m;
    fastcgi_cache_bypass $skip_cache;
    fastcgi_no_cache $skip_cache;
    add_header X-FastCGI-Cache $upstream_cache_status;
}

Add cache bypass rules before the location blocks (inside server):

set $skip_cache 0;

# Don't cache POST requests
if ($request_method = POST) {
    set $skip_cache 1;
}

# Don't cache URLs with query strings
if ($query_string != "") {
    set $skip_cache 1;
}

# Don't cache admin, login, or WooCommerce pages
if ($request_uri ~* "/wp-admin/|/wp-login.php|/cart/|/checkout/|/my-account/") {
    set $skip_cache 1;
}

# Don't cache logged-in users
if ($http_cookie ~* "wordpress_logged_in_|comment_author_|woocommerce_cart_hash") {
    set $skip_cache 1;
}

Test and reload:

sudo nginx -t
sudo systemctl reload nginx

Check the cache header on a page:

curl -I https://yourdomain.com/

Look for X-FastCGI-Cache: HIT on the second request. The first request will be a MISS because it populates the cache.

Restart PHP-FPM to apply all changes:

sudo systemctl restart php8.3-fpm

Security Hardening

A VPS exposed to the internet will start receiving automated attacks within minutes of going live. These steps block the most common attack vectors.

UFW Firewall

UFW (Uncomplicated Firewall) is the standard firewall on Ubuntu. Enable it and allow only the ports you need:

sudo ufw allow 22/tcp    # SSH
sudo ufw allow 80/tcp    # HTTP (for Certbot renewal and redirect)
sudo ufw allow 443/tcp   # HTTPS
sudo ufw enable

Verify the rules:

sudo ufw status verbose

If you are using a non-standard SSH port, update the SSH rule accordingly before enabling UFW. Locking yourself out of SSH is a very bad day.

Fail2ban

Fail2ban monitors log files and automatically bans IPs that show malicious behaviour -- repeated failed login attempts, exploit scanning, and similar patterns. It is one of the most effective server protection tools available.

sudo apt install fail2ban -y

Create a local configuration (so your settings survive package updates):

sudo nano /etc/fail2ban/jail.local
[DEFAULT]
bantime = 3600
findtime = 600
maxretry = 5

[sshd]
enabled = true

[nginx-http-auth]
enabled = true

[nginx-botsearch]
enabled = true
sudo systemctl enable fail2ban
sudo systemctl start fail2ban

For a deeper dive into Fail2ban configuration including WordPress-specific jails, read our comprehensive Fail2ban protection guide.

WordPress File Permissions

Incorrect file permissions are one of the most common WordPress security issues. Set them correctly:

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

Directories get 755 (owner can read/write/execute, everyone else can read/execute) and files get 644 (owner can read/write, everyone else can only read). This prevents other users on the server from modifying WordPress files while still allowing the web server to read them.

Disable XML-RPC

We already blocked xmlrpc.php in the Nginx config, but belt-and-suspenders is the right approach with security. Add this to wp-config.php as well:

add_filter( 'xmlrpc_enabled', '__return_false' );

XML-RPC is a legacy API that WordPress kept for backward compatibility with older clients. Modern WordPress uses the REST API instead. The only thing XML-RPC does in practice is serve as an attack surface for brute force and DDoS amplification attacks.

Automatic Security Updates

Ubuntu 24.04 has unattended-upgrades available. Enable it so security patches apply automatically:

sudo apt install unattended-upgrades -y
sudo dpkg-reconfigure -plow unattended-upgrades

Choose yes when prompted. This ensures critical patches for Nginx, PHP, MySQL, and the kernel install without manual intervention.


Automated Deployments with Git and DeployHQ

Managing WordPress on a VPS by editing files over SSH or uploading via SFTP works -- until you need to roll back a broken change, coordinate changes across a team, or deploy to staging and production simultaneously. If you prefer a graphical control panel for server management instead of the command line, see our guide to installing cPanel on Ubuntu.

A proper deployment workflow uses Git as the source of truth and a deployment tool to push changes to the server. This is how professional teams manage WordPress:

  1. Track your theme and plugin code in Git (not WordPress core -- that stays on the server)
  2. Push to your repository (GitHub, GitLab, Bitbucket)
  3. DeployHQ detects the push and deploys changes to your server automatically

DeployHQ handles SSH connections, file transfers, build commands (like npm run build for Webpack or Vite), and can run post-deployment scripts to clear caches. It also supports zero-downtime deployments so your site stays live during updates.

For the complete walkthrough on setting this up, including repository structure, deployment configuration, and build pipelines, read our dedicated guide: Automate WordPress Deployments with Git.

If you manage separate development, staging, and production servers, DeployHQ handles that too. Our guide on managing multiple environments covers the setup in detail.


Verification Checklist

Before you consider the server ready for production traffic, work through this list:

  • [ ] WordPress admin loads over HTTPS with no mixed content warnings
  • [ ] Permalinks work (Settings > Permalinks > Post name, save, test a post URL)
  • [ ] File uploads work (Media > Add New)
  • [ ] Plugin installation works from the admin dashboard
  • [ ] Redis shows Connected status (sudo -u www-data wp redis status)
  • [ ] FastCGI cache shows HIT headers on the second page load
  • [ ] UFW is active with only ports 22, 80, 443 open
  • [ ] Fail2ban is running (sudo fail2ban-client status)
  • [ ] SSL certificate auto-renewal works (sudo certbot renew --dry-run)
  • [ ] You can SSH in as the deploy user (not root)

What's Next

With a production WordPress server running, your next priorities should be:

  • Backups: Set up automated daily backups of both the database (mysqldump) and the wp-content directory. Store them off-server.
  • Monitoring: Use tools like Uptime Robot or Hetrixtools to alert you when the site goes down.
  • CDN: Cloudflare's free tier adds edge caching and DDoS protection. Point your DNS through Cloudflare and enable full SSL mode.
  • Staging environment: Never test changes on production. Clone this setup on a second VPS or use DeployHQ's environment management to deploy to staging first.

Deploy WordPress the Right Way

Setting up a VPS is the foundation. Keeping WordPress updated, deploying changes safely, and maintaining multiple environments is where DeployHQ fits in. Connect your Git repository, configure your server, and every push deploys automatically -- with rollback capability if anything goes wrong.

Start your free trial at deployhq.com


Have questions about deploying WordPress? Get in touch at support@deployhq.com or find us on Twitter/X.