How To Migrate a Site From Apache to Nginx on Ubuntu 22.04
Tricknowtech Team 20 min
Goal
By the end, an existing Apache-hosted site is fully served by Nginx on ports 80 and 443, with its VirtualHost directives, .htaccess/mod_rewrite rules, and PHP handling reproduced in a single Nginx server block, TLS certificates carried over, and Apache stopped but still installed as an immediate rollback option.
Prerequisites
An Ubuntu 22.04 LTS server with a non-root user configured with sudo privileges, reachable over SSH.
An existing Apache2 installation actively serving at least one site with a registered domain (this tutorial uses example.com — substitute your own domain and paths throughout).
DNS for the domain already pointed at the server's public IP address.
If the site is dynamic, awareness that this tutorial covers the common case of a PHP site handled by Apache's mod_php.
Comfort editing files with a terminal text editor such as nano or vim.
A recent full backup or snapshot of the server, taken before you start.
Let an AI agent do this for you
Copy a ready-made prompt for an AI coding assistant with terminal access to your server (Claude Code, Cursor, or similar) — it can carry out the steps below for you. Review what it plans to run before it executes anything.
Apache and Nginx can both serve the same site, but they read configuration in fundamentally different ways: Apache lets each directory override behavior with a local .htaccess file, while Nginx centralizes everything into one server block that is only read at startup or reload. Migrating a live site between them is therefore not a matter of installing a new package — it means auditing every rewrite rule, redirect, and per-directory override Apache has been quietly applying, and rebuilding them explicitly in Nginx's syntax before anything user-facing changes.
This tutorial walks through that migration end to end on Ubuntu 22.04 LTS: inventorying the existing Apache virtual host, installing Nginx alongside it without touching live traffic, translating the configuration (including .htaccess and mod_rewrite rules, which have no direct Nginx equivalent and must be folded into the server block), validating the result on an alternate port, and only then cutting over to ports 80 and 443 — with a rollback plan in place the whole time. It assumes a single primary domain, example.com, served from a document root such as /var/www/example.com/html; substitute your own domain and paths as you go.
Prerequisites
An Ubuntu 22.04 LTS server with a non-root user configured with sudo privileges, reachable over SSH.
An existing Apache2 installation actively serving at least one site with a registered domain (this tutorial uses example.com — substitute your own domain and paths throughout).
DNS for the domain already pointed at the server's public IP address.
If the site is dynamic, awareness that this tutorial covers the common case of a PHP site handled by Apache's mod_php.
Comfort editing files with a terminal text editor such as nano or vim.
A recent full backup or snapshot of the server, taken before you start.
Step 1 — Auditing the Existing Apache Configuration
Tricknowtech VPS Hosting
Dedicated KVM resources and full root access — deployed in under 60 seconds, no ticket required.
Before changing anything, take a full inventory of what Apache is currently doing for the site, and back up its configuration so you have a known-good reference to compare against later. Start with apache2ctl -S, which lists every configured VirtualHost, the address/port it matches, and the file and line number it was defined on. Then read through the enabled site file directly, and check whether any part of the site relies on AllowOverride to honor .htaccess files, since that mechanism has no Nginx equivalent at all.
bash
# back up the current Apache configuration before touching anything
sudo cp -r /etc/apache2 ~/apache2-backup-$(date +%F)
# list configured virtual hosts and where each directive came from
sudo apache2ctl -S
# confirm mod_rewrite is actually loaded (relevant if .htaccess uses RewriteRule)
sudo apache2ctl -M | grep -i rewrite
# read the live site configuration
cat /etc/apache2/sites-enabled/example.com.conf
# find every place AllowOverride is set, and to what
grep -ri "allowoverride" /etc/apache2/apache2.conf /etc/apache2/sites-available/*.conf
# locate and print any .htaccess files under the document root
sudo find /var/www/example.com -iname ".htaccess" -print -exec cat {} \;
DocumentRoot — the path Nginx's root directive must match
Every RewriteCond / RewriteRule, in both the vhost file and any .htaccess files
Redirect / RedirectMatch directives
ErrorDocument directives for custom error pages
Header / RequestHeader directives
SSLCertificateFile / SSLCertificateKeyFile paths, if HTTPS is already configured
CustomLog / ErrorLog paths, for parity when you check logs later
“Anywhere AllowOverride is set to None, .htaccess is already being ignored by Apache — good to confirm now, since it means one less place to hunt for hidden rewrite logic later.”
Step 2 — Installing Nginx Without Disabling Apache
Install Nginx from the default Ubuntu repositories. Because Apache is still bound to port 80, the Nginx service will likely fail to start immediately after installation — that's expected and harmless at this stage, since Apache continues serving all live traffic untouched. Stop Nginx for now and remove its default site, so it doesn't compete for port 80 later; you'll give it its own configuration and a temporary alternate port in the next step.
bash
sudo apt update
sudo apt install nginx
# check status — it's fine if this shows failed, Apache still owns port 80
sudo systemctl status nginx
# stop it for now and remove the default site
sudo systemctl stop nginx
sudo unlink /etc/nginx/sites-enabled/default
Step 3 — Translating the VirtualHost to an Nginx Server Block
Create a new Nginx server block that reproduces the static parts of the Apache VirtualHost you audited in Step 1. Point it at a nonstandard port for now (8080 is used throughout this tutorial) so it can run side by side with Apache on port 80 without conflict. The core directive mapping is straightforward:
Step 4 — Migrating .htaccess and mod_rewrite Rules
Nginx has no per-directory configuration file mechanism, so it never reads .htaccess — every rule you found in Step 1, whether it lived in the vhost file or a .htaccess file, has to be rewritten directly into the server block. The most common pattern is a front-controller rewrite (routing all non-file, non-directory requests to a single PHP script), which maps cleanly onto try_files. Simple redirects and custom error pages have equally direct equivalents.
apache
# Apache: typical front-controller rewrite (e.g. in .htaccess)
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
nginx
# Nginx equivalent — replace the location / block from Step 3 with this
location / {
try_files $uri $uri/ /index.php?$args;
}
apache
# Apache: a plain redirect and a custom error page
Redirect 301 /old-page /new-page
ErrorDocument 404 /404.html
Header set X-Frame-Options "SAMEORIGIN"
“Translate every RewriteRule you found, not just the ones that look important — a rule silently missing in Nginx doesn't error, it just serves a 404 or the wrong file, which is easy to miss until a real request hits it.”
Step 5 — Configuring PHP Processing With PHP-FPM
If the Apache site used mod_php (libapache2-mod-php), that module has no Nginx counterpart — Nginx never executes application code itself, it only proxies requests to a separate process. Install php-fpm and add a location block that hands any .php request off to it over its Unix socket. Skip this entire step if the site is static HTML/CSS/JS with no PHP involved.
bash
sudo apt install php-fpm
# Ubuntu 22.04's default repositories install PHP 8.1; confirm the exact
# version and matching service/socket name on your system with:
apt-cache policy php-fpm
sudo systemctl status php8.1-fpm
nginx
# add inside the same server block, alongside location /
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.1-fpm.sock;
}
Step 6 — Testing Nginx on an Alternate Port
With the server block validated and the port still set to 8080, start Nginx and exercise the site through it before it ever touches real traffic. Use a Host header to hit the right server block without needing DNS to point anywhere yet, and specifically retest every redirect and rewrite rule you translated in Step 4, not just the homepage. If PHP is in play, confirm it executes correctly and then remove the test file immediately — a world-readable phpinfo() page leaks configuration details.
bash
sudo nginx -t && sudo systemctl start nginx
# test locally against the alternate port using a Host header
curl -I -H "Host: example.com" http://127.0.0.1:8080/
# test a path that should trigger a rewrite or redirect rule
curl -I -H "Host: example.com" http://127.0.0.1:8080/old-page
# confirm PHP executes, then remove the test file
echo '<?php phpinfo();' | sudo tee /var/www/example.com/html/info.php
curl -H "Host: example.com" http://127.0.0.1:8080/info.php | head -20
sudo rm /var/www/example.com/html/info.php
# watch the logs while you test
sudo tail -f /var/log/nginx/example.com.access.log /var/log/nginx/example.com.error.log
To test from outside the server, temporarily open the alternate port in the firewall and hit it by public IP with a Host header, then close it again once testing is done — there's no need to leave a second port permanently exposed.
If the site already serves HTTPS via Let's Encrypt, the certificate files live independently of Apache under /etc/letsencrypt and can be pointed to directly from Nginx — there's no need to reissue anything. Add a second, temporary server block on an alternate HTTPS port referencing the existing certificate and key, then validate it the same way you validated the HTTP block.
bash
sudo ls -l /etc/letsencrypt/live/example.com/
nginx
# temporary HTTPS test block — append to the same file, still using an
# alternate port so it doesn't conflict with Apache's port 443
server {
listen 8443 ssl;
listen [::]:8443 ssl;
server_name example.com www.example.com;
root /var/www/example.com/html;
index index.html index.php;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.1-fpm.sock;
}
}
Certbot renews certificate files in place regardless of which web server reads them, but its renewal hook currently expects to reload Apache. Add a deploy hook so Nginx gets reloaded after every future renewal, and update certbot's own record of which installer manages the site.
“Open /etc/letsencrypt/renewal/example.com.conf and change the installer = apache line to installer = nginx so certbot's own bookkeeping matches the new setup.”
Step 8 — Cutting Over: Stopping Apache and Moving Nginx to Ports 80/443
Once every rule has been retested on the alternate ports, fold the two temporary server blocks into their final form: the port 80 block becomes an HTTPS redirect, and the port 8443 block moves to 443. Validate the syntax, stop and disable Apache so it no longer starts on boot, then reload Nginx. Because Apache is only stopped — not removed — its configuration remains on disk as an immediate rollback path.
sudo nginx -t
sudo systemctl stop apache2
sudo systemctl disable apache2
sudo systemctl reload nginx
# confirm only Nginx is bound to 80 and 443 now
sudo ss -tlnp | grep -E ':80|:443'
# update the firewall if Apache's ufw profile was previously allowed
sudo ufw app list
sudo ufw allow 'Nginx Full'
sudo ufw delete allow 'Apache Full'
Finally, verify from outside the server that both the redirect and the HTTPS site behave correctly, and check the response headers to confirm Nginx — not Apache — is now answering.
Because Apache was only stopped and disabled, not uninstalled, and its configuration files were never modified, reverting is a matter of starting it again and stopping Nginx. Keep this option available for at least a few days of normal traffic before considering Apache removal, since low-traffic paths and edge cases can take time to surface.
bash
# roll back to Apache
sudo systemctl stop nginx
sudo systemctl start apache2
sudo systemctl enable apache2
# revert the firewall change
sudo ufw allow 'Apache Full'
sudo ufw delete allow 'Nginx Full'
“Keep Apache installed (don't apt purge apache2) and hold on to the ~/apache2-backup-* directory from Step 1 until you're confident the migration is stable — both cost nothing to leave in place.”
Conclusion
The site is now served entirely by Nginx: its VirtualHost directives were audited and translated into a single server block, every .htaccess and mod_rewrite rule was rebuilt using try_files, rewrite, and return, PHP is handled through php-fpm instead of mod_php, and the existing TLS certificate was carried over rather than reissued. The whole change was validated on alternate ports before touching ports 80 or 443, and Apache remains installed and untouched, stopped rather than removed, as a working rollback path.