Websites & apps

How to migrate a website from Apache to Nginx

Move a site from Apache to Nginx with low downtime - convert virtual hosts and.htaccess rewrites, set up PHP-FPM, reuse SSL certificates, test on a spare port, then switch.

AdvancedLinux server5 min read

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:

Bash
sudo apt install nginx
sudo systemctl stop nginx

On AlmaLinux and Rocky:

Bash
sudo dnf install nginx

Disable the default Nginx site so it does not claim port 80 later:

Bash
sudo rm /etc/nginx/sites-enabled/default   # Ubuntu/Debian only

Step 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):

Bash
sudo apt install php8.3-fpm
sudo systemctl enable --now php8.3-fpm

List 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:

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:

Nginx
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:

Bash
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:

Bash
sudo find /var/www -name .htaccess -exec echo "== {}" \; -exec cat {} \;

Common translations:

  • Front controller (RewriteCond %{REQUEST_FILENAME} !-f plus RewriteRule. /index.php): becomes try_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): becomes location = /old-page { return 301 /new-page; }.
  • Pattern redirect (RewriteRule ^blog/(.*)$ /news/$1 [R=301,L]): becomes rewrite ^/blog/(.*)$ /news/$1 permanent;. Note the leading slash in Nginx.
  • Deny a folder (Require all denied in a sub-folder .htaccess): becomes location ^~ /private/ { deny all; }.
  • Password protection: reuse the same .htpasswd file with auth_basic and auth_basic_user_file.

Test after each change:

Bash
sudo nginx -t

Step 5: Test on the spare port

Start Nginx while Apache still serves the live site:

Bash
sudo systemctl start nginx

Open port 8080 in your firewall temporarily, for example sudo ufw allow 8080/tcp, then test with curl, forcing the host name:

Bash
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:

Bash
sudo nginx -t && sudo systemctl stop apache2 && sudo systemctl start nginx

Downtime is only a second or two. Then disable Apache so it does not start on reboot:

Bash
sudo systemctl disable apache2
sudo systemctl enable nginx

Finally, 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/ returns 200 with server: nginx.
  • sudo certbot renew --dry-run succeeds.
  • 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 or fastcgi_pass points at the wrong socket. Check ls /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_files line is missing from location /.
  • 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

Was this guide helpful?

Still stuck?

Ask MdmHost AI for a quick answer, or open a ticket and our team will take a look. We are here 24/7.