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:
- Verify you own the domain via an HTTP challenge
- Generate the SSL certificate
- Automatically update your Nginx config to redirect HTTP to HTTPS
- 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 pluginsmax_accelerated_files=10000: WordPress core alone has ~3,000 files; this leaves room for pluginsrevalidate_freq=60: Check for file changes every 60 seconds instead of every requestsave_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:
- Track your theme and plugin code in Git (not WordPress core -- that stays on the server)
- Push to your repository (GitHub, GitLab, Bitbucket)
- 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
Connectedstatus (sudo -u www-data wp redis status) - [ ] FastCGI cache shows
HITheaders 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 thewp-contentdirectory. 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.