Nginx uses less memory than Apache with mod_php, and it handles many idle connections well. Moving a site across is mostly translation work: Apache virtual hosts become Nginx server blocks, .htaccess rules move into the server config, and PHP runs through PHP-FPM. This guide walks through the migration on the same server, with a test phase on a spare port and a quick way back.
Before you start
- A Linux VPS running Apache, on Ubuntu 22.04/24.04, Debian 12, or AlmaLinux/Rocky Linux 8 or 9.
- SSH access with
sudo. - A full backup of
/etc/apache2(or/etc/httpd), the web root and the database. - A list of every site Apache serves. Get it with
sudo apachectl -S.
Warning: Both web servers cannot listen on ports 80 and 443 at once. Follow the order below so the live site keeps running until you are ready to switch.
Step 1: Install Nginx without stopping Apache
On Ubuntu and Debian, installing Nginx tries to start it on port 80, which fails while Apache is running. That is expected. Install it and stop it:
sudo apt install nginx
sudo systemctl stop nginxOn AlmaLinux and Rocky:
sudo dnf install nginxDisable the default Nginx site so it does not claim port 80 later:
sudo rm /etc/nginx/sites-enabled/default # Ubuntu/Debian onlyStep 2: Set up PHP-FPM
If Apache uses mod_php, Nginx cannot use that. Install PHP-FPM with the same PHP version and extensions. Check the current version with php -v, then on Ubuntu (example for PHP 8.3):
sudo apt install php8.3-fpm
sudo systemctl enable --now php8.3-fpmList the extensions you already use with php -m and install the matching FPM packages. Copy any custom settings from Apache's PHP config, such as upload_max_filesize and memory_limit, to the FPM php.ini (on Ubuntu, /etc/php/8.3/fpm/php.ini).
Remember to move any php_value lines from .htaccess too. Put them in the FPM php.ini or in the pool file as php_admin_value[...].
Step 3: Convert each virtual host
Here is a typical Apache vhost and its Nginx equivalent. Apache:
<VirtualHost *:80>
ServerName example.com
ServerAlias www.example.com
DocumentRoot /var/www/example.com/public
ErrorLog ${APACHE_LOG_DIR}/example.com-error.log
</VirtualHost>Nginx, in /etc/nginx/sites-available/example.com (or /etc/nginx/conf.d/example.com.conf on AlmaLinux and Rocky). It listens on a test port, 8080, for now:
server {
listen 8080;
server_name example.com www.example.com;
root /var/www/example.com/public;
index index.php index.html;
access_log /var/log/nginx/example.com-access.log;
error_log /var/log/nginx/example.com-error.log;
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 ~ /\.(?!well-known) {
deny all;
}
}On AlmaLinux and Rocky, there is no snippets/fastcgi-php.conf. Use include fastcgi_params; plus fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;, and point fastcgi_pass at unix:/run/php-fpm/www.sock.
The last block blocks dotfiles such as .env and .git, which Apache usually protected for you.
Enable the site on Ubuntu and Debian:
sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/Step 4: Convert.htaccess rules
Nginx ignores .htaccess files. Find every rule you need to carry over:
sudo find /var/www -name .htaccess -exec echo "== {}" \; -exec cat {} \;Common translations:
- Front controller (
RewriteCond %{REQUEST_FILENAME} !-fplusRewriteRule. /index.php): becomestry_files $uri $uri/ /index.php?$args;as shown above. WordPress, Laravel and most PHP apps need only this. - Single redirect (
Redirect 301 /old-page /new-page): becomeslocation = /old-page { return 301 /new-page; }. - Pattern redirect (
RewriteRule ^blog/(.*)$ /news/$1 [R=301,L]): becomesrewrite ^/blog/(.*)$ /news/$1 permanent;. Note the leading slash in Nginx. - Deny a folder (
Require all deniedin a sub-folder.htaccess): becomeslocation ^~ /private/ { deny all; }. - Password protection: reuse the same
.htpasswdfile withauth_basicandauth_basic_user_file.
Test after each change:
sudo nginx -tStep 5: Test on the spare port
Start Nginx while Apache still serves the live site:
sudo systemctl start nginxOpen port 8080 in your firewall temporarily, for example sudo ufw allow 8080/tcp, then test with curl, forcing the host name:
curl -I -H "Host: example.com" http://203.0.113.10:8080/
curl -I -H "Host: example.com" http://203.0.113.10:8080/some-pretty-url/Check pages, logins, form posts, uploads and old redirects. Watch /var/log/nginx/example.com-error.log while you test.
Step 6: Move SSL and switch over
Your existing certificates in /etc/letsencrypt/live/ work with Nginx as they are. Add a 443 server block that uses them, change listen 8080; to listen 80;, and add the redirect to HTTPS.
Then switch in one quick step:
sudo nginx -t && sudo systemctl stop apache2 && sudo systemctl start nginxDowntime is only a second or two. Then disable Apache so it does not start on reboot:
sudo systemctl disable apache2
sudo systemctl enable nginxFinally, switch Certbot renewals to Nginx: run sudo certbot --nginx -d example.com -d www.example.com and choose to reinstall the existing certificate. Remove the temporary 8080 firewall rule.
Tip: Keep Apache installed for a week. To roll back, run sudo systemctl stop nginx && sudo systemctl start apache2.
Check it works
curl -I https://example.com/returns200withserver: nginx.sudo certbot renew --dry-runsucceeds.- Pretty URLs, admin areas and uploads work.
- The Nginx error log stays quiet under normal use.
Troubleshooting
- PHP files download instead of running: the
location ~ \.php$block is missing orfastcgi_passpoints at the wrong socket. Checkls /run/php/. - 502 Bad Gateway: PHP-FPM is stopped or the socket path is wrong. Start FPM and match the path.
- Pretty URLs return 404: the
try_filesline is missing fromlocation /. - Redirect loop after switching: an app or CDN forces HTTPS while Nginx redirects as well. Make sure only one layer handles the redirect.
Next steps
- Learn Nginx server blocks in depth: Host multiple websites with Nginx server blocks.
- Fix gateway errors: Fix Nginx 502 Bad Gateway.
- Compare the options first: Nginx vs Apache vs OpenLiteSpeed for WordPress.
