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](https://deployhq.com/blog/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](https://www.deployhq.com/blog/beginners-guide-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](https://www.deployhq.com/blog/openssh-on-windows-why-this-changes-everything-for-your-deployments) 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](https://www.deployhq.com/blog/fail2ban-comprehensive-protection-for-your-servers).

### 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](https://deployhq.com/blog/installing-cpanel-on-ubuntu-22-04-and-deployhq).

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](https://www.deployhq.com) 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](https://www.deployhq.com/blog/zero-downtime-deployments-keeping-your-application-running-smoothly) 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](https://www.deployhq.com/blog/automate-wordpress-deployments-with-git).

If you manage separate development, staging, and production servers, [DeployHQ](https://www.deployhq.com) handles that too. Our guide on [managing multiple environments](https://www.deployhq.com/blog/managing-multiple-environments-with-deployhq-dev-staging-and-production) 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](https://www.deployhq.com/signup) 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](https://www.deployhq.com/signup)

* * *

_Have questions about deploying WordPress? Get in touch at [support@deployhq.com](mailto:support@deployhq.com) or find us on [Twitter/X](https://x.com/deployhq)._

