Why Bare Metal for WordPress

WordPress powers 43% of the web. Most of those sites run on shared hosting where they compete with hundreds of other sites for CPU, RAM, and disk I/O. The result: 3-8 second page loads, random timeouts during traffic spikes, and a TTFB that makes Google's Core Web Vitals cry.

On bare metal, WordPress gets everything: dedicated CPU cores, dedicated RAM, NVMe storage, and a network stack that isn't shared with anyone. Combined with a properly tuned LEMP stack (Linux, Nginx, MySQL, PHP), you can serve WordPress pages in under 200ms. With FastCGI caching, under 20ms.

Shared Hosting vs Bare Metal

MetricShared HostingBare Metal (RAW)
TTFB800ms-3s15-50ms (cached)
Full page load3-8 seconds0.3-0.8 seconds
Concurrent users50-100 (then crashes)5,000+
CPUShared (throttled)2-8 dedicated cores
RAM512 MB-1 GB4-16 GB
StorageHDD or slow SSDNVMe (3,200 MB/s)
Root accessNoFull SSH
SSLOften extra $$Free (Certbot)
Cost$5-30/mo$6/mo

RAW raw-2 ARM: 2 vCPU, 4 GB RAM, 40 GB NVMe. Enough for most WordPress sites.

Step 1: Provision Your Server

# Deploy a RAW server (Ubuntu 24.04)
npx rawhq deploy

# SSH in
ssh root@your-server-ip

# Update system
apt update && apt upgrade -y

Step 2: Install the LEMP Stack

LEMP = Linux + Nginx + MySQL + PHP. This is the gold standard for WordPress performance.

Install Nginx

# Install Nginx
apt install -y nginx

# Start and enable
systemctl enable nginx
systemctl start nginx

# Verify — visit http://your-server-ip in a browser
curl -I http://localhost

Install MySQL 8

# Install MySQL
apt install -y mysql-server

# Secure the installation
mysql_secure_installation
# - Set root password
# - Remove anonymous users: Y
# - Disallow root login remotely: Y
# - Remove test database: Y
# - Reload privilege tables: Y

# Create WordPress database and user
mysql -u root -p << 'EOF'
CREATE DATABASE wordpress CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'wpuser'@'localhost' IDENTIFIED BY 'your-strong-password-here';
GRANT ALL PRIVILEGES ON wordpress.* TO 'wpuser'@'localhost';
FLUSH PRIVILEGES;
EOF

Install PHP 8.3 with Extensions

# Add PHP repository
apt install -y software-properties-common
add-apt-repository -y ppa:ondrej/php
apt update

# Install PHP-FPM and required extensions
apt install -y \
  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-imagick \
  php8.3-redis \
  php8.3-opcache

# Verify PHP
php -v

Step 3: Install WordPress

# Download WordPress
cd /tmp
curl -O https://wordpress.org/latest.tar.gz
tar -xzf latest.tar.gz

# Move to web root
mv wordpress /var/www/wordpress
chown -R www-data:www-data /var/www/wordpress
chmod -R 755 /var/www/wordpress

# Create wp-config.php
cd /var/www/wordpress
cp wp-config-sample.php wp-config.php

Edit the config with your database credentials:

# Edit wp-config.php
nano /var/www/wordpress/wp-config.php

# Update these lines:
# define('DB_NAME', 'wordpress');
# define('DB_USER', 'wpuser');
# define('DB_PASSWORD', 'your-strong-password-here');
# define('DB_HOST', 'localhost');

# Generate fresh security keys from:
# https://api.wordpress.org/secret-key/1.1/salt/
# Replace the placeholder keys in the config

Step 4: Configure Nginx for WordPress

This is where the performance magic happens. Nginx handles static files directly and passes PHP requests to PHP-FPM via FastCGI:

# /etc/nginx/sites-available/wordpress
server {
    listen 80;
    server_name yourdomain.com www.yourdomain.com;
    root /var/www/wordpress;
    index index.php index.html;

    # Security headers
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "1; mode=block" always;

    # Max upload size (WordPress media uploads)
    client_max_body_size 64M;

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

    # WordPress permalink support
    location / {
        try_files $uri $uri/ /index.php?$args;
    }

    # PHP-FPM handling
    location ~ \.php$ {
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param PATH_INFO $fastcgi_path_info;
        fastcgi_buffer_size 128k;
        fastcgi_buffers 4 256k;
        fastcgi_busy_buffers_size 256k;
    }

    # Block access to sensitive files
    location ~ /\.ht { deny all; }
    location ~ /wp-config\.php { deny all; }
    location ~ /xmlrpc\.php { deny all; }

    # Gzip compression
    gzip on;
    gzip_vary on;
    gzip_min_length 256;
    gzip_types text/plain text/css application/json
               application/javascript text/xml application/xml
               application/xml+rss text/javascript
               application/vnd.ms-fontobject font/eot
               font/opentype image/svg+xml;
}
# Enable the site
ln -s /etc/nginx/sites-available/wordpress /etc/nginx/sites-enabled/
rm -f /etc/nginx/sites-enabled/default

# Test and reload
nginx -t
systemctl reload nginx

Step 5: SSL with Certbot

# Install Certbot
apt install -y certbot python3-certbot-nginx

# Get SSL certificate
certbot --nginx -d yourdomain.com -d www.yourdomain.com

# Auto-renewal is configured automatically
# Verify with:
certbot renew --dry-run

Now visit https://yourdomain.com to complete the WordPress installation wizard.

Step 6: Performance Optimization

A vanilla WordPress install on bare metal is already fast. With these optimizations, it becomes blazing fast.

PHP OPcache Configuration

OPcache stores precompiled PHP bytecode in memory, eliminating the need to parse PHP files on every request:

# /etc/php/8.3/fpm/conf.d/10-opcache.ini
opcache.enable=1
opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=10000
opcache.revalidate_freq=60
opcache.fast_shutdown=1
opcache.enable_cli=0
opcache.save_comments=1
opcache.jit=1255
opcache.jit_buffer_size=128M

PHP-FPM Tuning

# /etc/php/8.3/fpm/pool.d/www.conf
# Key settings to tune:

pm = dynamic
pm.max_children = 20
pm.start_servers = 5
pm.min_spare_servers = 3
pm.max_spare_servers = 10
pm.max_requests = 500

# For a 4 GB RAM server, these settings handle
# ~200 concurrent PHP requests efficiently

Redis Object Cache

Redis caches WordPress database queries in memory. This eliminates redundant MySQL queries on every page load:

# Install Redis
apt install -y redis-server

# Configure Redis
sed -i 's/^# maxmemory .*/maxmemory 256mb/' /etc/redis/redis.conf
sed -i 's/^# maxmemory-policy .*/maxmemory-policy allkeys-lru/' /etc/redis/redis.conf

# Restart Redis
systemctl restart redis

# Verify
redis-cli ping
# Should return: PONG

Add Redis configuration to wp-config.php:

# Add to wp-config.php (before "That's all, stop editing!")
define('WP_REDIS_HOST', '127.0.0.1');
define('WP_REDIS_PORT', 6379);
define('WP_REDIS_DATABASE', 0);
define('WP_CACHE', true);

Then install the Redis Object Cache plugin via WP-CLI:

# Install WP-CLI
curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
chmod +x wp-cli.phar
mv wp-cli.phar /usr/local/bin/wp

# Install Redis Object Cache plugin
cd /var/www/wordpress
wp plugin install redis-cache --activate --allow-root
wp redis enable --allow-root

# Verify Redis is connected
wp redis status --allow-root

Nginx FastCGI Cache

This is the biggest performance win. FastCGI cache stores fully rendered HTML pages, so Nginx serves them directly without touching PHP or MySQL:

# Add to /etc/nginx/nginx.conf (inside http block)
fastcgi_cache_path /tmp/nginx-cache levels=1:2
    keys_zone=WORDPRESS:100m
    inactive=60m
    max_size=512m;
fastcgi_cache_key "$scheme$request_method$host$request_uri";
fastcgi_cache_use_stale error timeout invalid_header http_500;

Update the WordPress Nginx site config to use the cache:

# Add these inside the server block, before location blocks:

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 or login pages
if ($request_uri ~* "/wp-admin/|/wp-login.php") {
    set $skip_cache 1;
}

# Don't cache for logged-in users
if ($http_cookie ~* "wordpress_logged_in") {
    set $skip_cache 1;
}

# Update the PHP location block to include caching:
# location ~ \.php$ {
#     ... existing fastcgi settings ...
#     fastcgi_cache WORDPRESS;
#     fastcgi_cache_valid 200 60m;
#     fastcgi_cache_bypass $skip_cache;
#     fastcgi_no_cache $skip_cache;
#     add_header X-Cache-Status $upstream_cache_status;
# }
# Restart everything
systemctl restart php8.3-fpm
systemctl restart nginx
systemctl restart redis

Step 7: WP-CLI Management

WP-CLI lets you manage WordPress entirely from the command line. No browser needed:

# Update WordPress core
wp core update --allow-root

# Update all plugins
wp plugin update --all --allow-root

# Update all themes
wp theme update --all --allow-root

# Clear object cache
wp cache flush --allow-root

# Search-replace URLs (useful after migration)
wp search-replace 'http://old-domain.com' 'https://new-domain.com' --allow-root

# Export database backup
wp db export /backups/wordpress-backup.sql --allow-root

# Install a plugin
wp plugin install wordfence --activate --allow-root

# Check site health
wp site health --allow-root

Step 8: Security Hardening

# Set up UFW firewall
ufw allow 22/tcp
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable

# Disable XML-RPC (common attack vector)
# Already blocked in Nginx config above

# Set correct file permissions
find /var/www/wordpress -type d -exec chmod 755 {} \;
find /var/www/wordpress -type f -exec chmod 644 {} \;
chown -R www-data:www-data /var/www/wordpress

# Prevent PHP execution in uploads directory
cat > /var/www/wordpress/wp-content/uploads/.htaccess << 'EOF'
# This is handled by Nginx, but belt and suspenders
EOF

# Add to Nginx (inside server block):
# location ~* /wp-content/uploads/.*\.php$ { deny all; }

# Install fail2ban for brute force protection
apt install -y fail2ban
systemctl enable fail2ban

Automated Backups

# Create backup script
cat > /usr/local/bin/wp-backup.sh << 'SCRIPT'
#!/bin/bash
BACKUP_DIR="/backups/wordpress"
DATE=$(date +%Y%m%d-%H%M%S)
mkdir -p "$BACKUP_DIR"

# Database backup
cd /var/www/wordpress
wp db export "$BACKUP_DIR/db-$DATE.sql" --allow-root

# Files backup
tar -czf "$BACKUP_DIR/files-$DATE.tar.gz" \
  -C /var/www wordpress/wp-content

# Keep only last 7 daily backups
find "$BACKUP_DIR" -name "*.sql" -mtime +7 -delete
find "$BACKUP_DIR" -name "*.tar.gz" -mtime +7 -delete

echo "Backup complete: $DATE"
SCRIPT

chmod +x /usr/local/bin/wp-backup.sh

# Schedule daily backup at 3 AM
echo "0 3 * * * root /usr/local/bin/wp-backup.sh" > /etc/cron.d/wp-backup

Performance Results

After applying all optimizations, here are real benchmarks on a RAW raw-2 ARM server ($6/mo):

MetricShared HostingRAW Bare Metal (Optimized)
TTFB (homepage)1,200ms12ms (cached)
TTFB (uncached)2,400ms85ms
Full page load4.2s0.4s
Requests/second~152,800+ (cached)
Lighthouse score45-6595-100
MySQL query time~120ms<5ms (Redis cached)

Tested with Apache Bench (ab -n 1000 -c 50). RAW raw-2 ARM: 2 vCPU, 4 GB RAM, 40 GB NVMe.

That's a 100x improvement in TTFB and a 10x improvement in full page load. Google notices this. Your visitors notice this. Your conversion rate notices this.

Cost Comparison

ProviderPlanPrice
SiteGroundGrowBig (shared)$25/mo (renewal)
WP EngineStartup (managed)$30/mo
KinstaStarter (managed)$35/mo
Cloudways2 GB (VPS)$28/mo
RAWraw-2 ARM (bare metal)$6/mo

You get better performance than all of them for a fraction of the price. The trade-off: you manage the server yourself. With this guide, that takes about 45 minutes to set up and 10 minutes/month to maintain (run wp core update and check backups).

Deploy WordPress on RAW

Get a bare metal server in 13 seconds. Follow this guide. Have the fastest WordPress site you've ever owned, for $6/mo.

$ npx rawhq deployDeploy Free Server →