
Switching web servers on a live site sounds risky — and it can be, if you skip the prep work. But an Apache to Nginx migration on a hosting VPS is one of the highest-leverage changes you can make for a WordPress site under real traffic.
Nginx handles concurrent connections with far less memory overhead than Apache's prefork model. That difference shows up directly in your time-to-first-byte numbers.
This tutorial walks through moving a production WordPress site from Apache to Nginx on Ubuntu 24.04. It covers PHP-FPM tuning, rewrite rule conversion, and SSL certificate reuse. You'll have a rollback plan at every stage, so a bad config never takes your site offline for more than a few seconds.
Why Move From Apache to Nginx
Apache's default prefork MPM spawns a new process per connection. Under moderate WordPress traffic — say 50-100 concurrent visitors — that can push memory usage past 2GB on a 4GB VPS. That leaves little room for MySQL or PHP-FPM workers.
Nginx uses an event-driven model instead. It handles thousands of connections with a handful of worker processes. That typically cuts memory usage by 30-50% on the same workload.
Static asset delivery is also noticeably faster. Nginx serves files directly, without the module overhead Apache carries by default.
None of this makes Apache a bad choice. .htaccess-based configs are genuinely convenient, and some legacy applications depend on Apache-specific modules. But for a standard WordPress or WooCommerce stack, Nginx plus PHP-FPM wins on performance almost every time.
If you haven't tuned that combination yet, our VPS performance optimization tutorial covers the baseline settings this migration builds on.
Before You Start: Inventory and Backup Checklist
Don't touch a live web server config without a full snapshot. Take one now.
- Snapshot the VPS or take a filesystem-level backup (see our VPS snapshot backup tutorial if this isn't automated yet)
- Export your current Apache VirtualHost configs:
cp -r /etc/apache2/sites-available /root/apache-backup-$(date +%F) - Document every .htaccess rule per site — Nginx doesn't read these files, so anything there needs manual conversion
- Confirm your PHP version and installed extensions:
php -v && php -m - Note existing SSL certificate paths, usually under
/etc/letsencrypt/live/yourdomain.com/
If you're moving to a fresh VPS rather than converting an existing one, follow the server rebuild in our WordPress migration tutorial, then apply the Nginx steps below on the new box before DNS cutover.
Step 1: Install Nginx and PHP-FPM Alongside Apache
Run both servers temporarily on different ports so you can test before switching over.
sudo apt update
sudo apt install nginx php8.3-fpm php8.3-mysql php8.3-curl php8.3-gd php8.3-mbstring php8.3-xml php8.3-zipStop Apache from claiming port 80, but don't disable it yet:
sudo systemctl stop apache2
sudo systemctl start nginx
sudo systemctl enable php8.3-fpmCheck that PHP-FPM is listening correctly:
sudo systemctl status php8.3-fpm
ls /run/php/You should see a socket file like php8.3-fpm.sock. That socket is what Nginx will talk to instead of using mod_php.
Step 2: Convert Your Virtual Host Config
A typical Apache VirtualHost for WordPress looks like this:
<VirtualHost *:80>
ServerName example.com
DocumentRoot /var/www/example.com/public_html
<Directory /var/www/example.com/public_html>
AllowOverride All
</Directory>
</VirtualHost>The Nginx equivalent, saved to /etc/nginx/sites-available/example.com, looks like this:
server {
listen 80;
server_name example.com www.example.com;
root /var/www/example.com/public_html;
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;
}
}Enable it and test the syntax before reloading:
sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginxThe try_files directive replaces WordPress's typical .htaccess rewrite block. This one line handles permalinks, pretty URLs, and 404 routing all at once.
Step 3: Reuse Your Existing SSL Certificates
You don't need to reissue Let's Encrypt certificates just because you changed web servers. Point Nginx at the existing cert paths:
server {
listen 443 ssl http2;
server_name example.com www.example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
root /var/www/example.com/public_html;
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;
}
}
server {
listen 80;
server_name example.com www.example.com;
return 301 https://$host$request_uri;
}After switching, run sudo certbot renew --dry-run to confirm renewal still works under Nginx's webroot plugin. If it fails, you likely need to update certbot's plugin from apache to nginx:
sudo apt install python3-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.comFor a from-scratch SSL setup, our Let's Encrypt setup guide tutorial covers both server types in more depth. If you hit renewal errors specifically, check the AutoSSL troubleshooting tutorial for common DCV validation failures.
Step 4: Tune PHP-FPM for Your Traffic Level
Default PHP-FPM settings assume a small site. For anything with real traffic, edit /etc/php/8.3/fpm/pool.d/www.conf:
pm = dynamic
pm.max_children = 20
pm.start_servers = 5
pm.min_spare_servers = 3
pm.max_spare_servers = 8
pm.max_requests = 500A rough sizing rule: divide your available RAM (minus 1-2GB reserved for MySQL and the OS) by your average PHP process memory footprint, usually 40-80MB for WordPress. On a 4GB VPS, 20 max_children is a sane starting point.
Restart to apply:
sudo systemctl restart php8.3-fpmStep 5: Add Caching Before You Go Live
This is the step people skip. It also delivers the biggest speed gain of the whole process.
Nginx's FastCGI microcache can serve WordPress pages in single-digit milliseconds without touching PHP at all for repeat requests.
Our Nginx caching tutorial covers the full microcache setup with purge rules for logged-in users and WooCommerce carts, so I won't duplicate it here. Just don't launch without it.
Step 6: Cut Over and Test
Once the Nginx config is confirmed working on an alternate port or via a hosts file test, disable Apache fully:
sudo systemctl stop apache2
sudo systemctl disable apache2
sudo systemctl restart nginxThen run through this checklist:
- Load the homepage and three inner pages directly by IP, bypassing DNS/cache
- Check wp-admin login and the WordPress dashboard
- Submit a test form or checkout if you run WooCommerce
- Verify SSL with
curl -vI https://example.com— look for a valid certificate chain - Check response headers for
Server: nginxto confirm the switch took effect - Watch the error log for 15 minutes:
sudo tail -f /var/log/nginx/error.log
If something breaks, rolling back is one command away as long as you kept Apache installed:
sudo systemctl stop nginx
sudo systemctl start apache2Common Migration Pitfalls
A few issues show up repeatedly during this switch, and most have quick fixes.
404s on every inner page. This almost always means the try_files directive is missing or misconfigured. Double-check the location block against the example above.
Blank white screen with no error. Usually a PHP-FPM socket mismatch. Confirm the socket path in your Nginx config matches the actual file under /run/php/.
Uploaded images or plugin assets 403 forbidden. Check file ownership. Nginx typically runs as www-data, same as PHP-FPM, but confirm with ps aux | grep nginx and match permissions accordingly.
Mixed content warnings after SSL cutover. WordPress sometimes hardcodes http:// URLs in the database. Run a search-replace with WP-CLI: wp search-replace 'http://example.com' 'https://example.com' --all-tables.
Real visitor IPs showing as the proxy IP. Only relevant if you're also running Cloudflare or a load balancer in front — our Nginx real IP configuration tutorial covers the fix.
What About Security Headers and Firewall Rules?
Apache and Nginx don't share security header configs. Anything you had in .htaccess for HSTS, CSP, or X-Frame-Options needs to be re-added at the server block level. See our Nginx security headers configuration tutorial for copy-paste blocks that work well with WordPress out of the box.
Your UFW or iptables rules generally don't need changes since both servers use ports 80 and 443. But if you're running rate limiting or fail2ban jails tied to Apache log paths, those need repointing to Nginx's log format and file location.
If you're planning this migration on a resource-constrained shared plan, it's worth moving to a proper HostMyCode VPS first — Nginx's efficiency gains matter most when you control the full stack. Our managed VPS hosting plans come with Nginx and PHP-FPM pre-tuned, so you skip the manual config work and get straight to testing.
FAQ
Will switching from Apache to Nginx break my WordPress permalinks?
Only if you skip the try_files directive. Copy the rewrite logic shown in Step 2 and permalinks work exactly as before.
Do I need to reinstall SSL certificates after migrating?
No. Let's Encrypt certificates are server-agnostic — you just point Nginx at the same file paths and update certbot's renewal plugin.
Can I run Apache and Nginx together long-term?
Yes. Using Nginx as a reverse proxy in front of Apache is a valid setup, particularly useful if you rely on .htaccess-heavy legacy apps. See our reverse proxy setup guide tutorial for that specific configuration.
How much downtime should I expect?
With this staged approach — running both servers before cutover — actual downtime is typically under 60 seconds, limited to the service restart itself.
What if my site uses a caching plugin like WP Rocket or W3 Total Cache?
Most caching plugins detect the web server automatically. Still, regenerate the cache config after migration and confirm it's writing rules compatible with Nginx rather than leftover .htaccess rules.