apache
Migrating Apache to Nginx — the translation patterns and playbook we use in production
Most Apache-to-Nginx migrations get stuck on .htaccess. Here's the translation table, the gotchas, and the playbook that gets a real site cut over without surprises.
17 de mayo de 2026 · 11 min · por Sudhanshu K.
Migrating Apache to Nginx — the translation patterns and playbook we use in production
About a third of the migrations we do at EdgeServers are Apache to Nginx. The reasons vary: customer wants a single web server across their estate to simplify operations, they're moving from a traditional VM stack to a containerised one where Nginx is the obvious choice, or they're shedding mod_php and the rest of the Apache config has been gradually losing value.
Whatever the reason, the migration always looks easier in the proposal than in execution. The 80% case ports in an afternoon. The remaining 20% — .htaccess files scattered through the codebase, ten years of accumulated rewrites, an authentication setup that depends on a module Nginx doesn't have — that's where the project lives.
This post is the playbook and translation reference we actually use, written down. If you're staring at an Apache install and wondering what it would take to move it to Nginx, this is what we'd hand you.
Phase 0: decide whether you should migrate at all
Worth saying first: the migration isn't always the right move. Reasons we've talked customers out of an Apache-to-Nginx project:
- The application is genuinely
.htaccess-driven. WordPress with five caching plugins, each contributing rewrite rules, on shared hosting where editors expect to drop their own redirects. Translating that into Nginx is more pain than the operational benefit is worth. - The customer relies on an Apache-only module —
mod_jkfronting Tomcat,mod_authnz_ldapintegrated with a quirky directory server,mod_perlfor a legacy app. The Nginx equivalents either don't exist or require a separate process. - The Apache install is genuinely stable and the customer's team knows it. Migrating for the sake of having Nginx everywhere is fashion, not engineering.
If none of those apply and the customer wants to consolidate, the migration is worth doing. Below is how.
Phase 1: inventory
The first hour is spent reading. We pull a tarball of /etc/apache2/ (or /etc/httpd/) and the application root, and we catalogue:
- Virtual hosts — how many, which ports, which SSL configurations
- Modules loaded —
apache2ctl -Mgives you the list .htaccessfiles in the document root —find /var/www -name .htaccess- Custom
Includedirectives pointing to files outside the standard config tree - Auth setups — Basic, Digest, LDAP, anything custom
- Rewrite rules — total count, complexity (back-references, lookup tables, conditional logic)
- Headers, redirects, proxy passes
A "small" migration is one VirtualHost, no .htaccess files, no custom modules. A "medium" migration is 5-20 VirtualHosts and a moderate number of rewrite rules. A "large" migration is 50+ VirtualHosts, hundreds of .htaccess files, or any of the Apache-only modules listed above.
For anything larger than medium we run a provisioning workstream in parallel — the new Nginx hosts get built fresh rather than retrofitted in place, and the migration becomes a DNS cutover at the end.
Phase 2: the translation table
The pattern catalogue. Most of what's in any real Apache config is one of these:
VirtualHost → server block
# Apache
<VirtualHost *:443>
ServerName www.example.com
ServerAlias example.com
DocumentRoot /var/www/html
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/www.example.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/www.example.com/privkey.pem
</VirtualHost># Nginx
server {
listen 443 ssl http2;
server_name www.example.com example.com;
root /var/www/html;
ssl_certificate /etc/letsencrypt/live/www.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/www.example.com/privkey.pem;
}.htaccess for index/file routing → try_files
The most common .htaccess pattern is "if the file doesn't exist, route to index.php":
# .htaccess (WordPress, Laravel, etc.)
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]# Nginx equivalent — one line
location / {
try_files $uri $uri/ /index.php?$query_string;
}try_files is the workhorse of Nginx routing. 80% of .htaccess files in the wild are this pattern or a variation; they all collapse into one or two try_files lines. The migration anxiety around .htaccess is usually overblown.
mod_rewrite → rewrite
Simple rewrite rules are 1:1:
RewriteRule ^/old-page$ /new-page [R=301,L]rewrite ^/old-page$ /new-page permanent;Rewrites with conditions get a bit more work. This Apache rule that redirects to HTTPS:
RewriteCond %{HTTPS} off
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]becomes a dedicated server block in Nginx:
server {
listen 80;
server_name www.example.com;
return 301 https://$host$request_uri;
}Two patterns where the translation isn't obvious:
RewriteMap. Apache's lookup-table rewrites have no direct Nginx equivalent. Small maps (under a few hundred entries) translate to Nginx map directives. Larger maps need an external service (typically a small Lua handler with openresty, or a separate redirect service).
Complex back-references with multiple captures. Apache's RewriteRule supports more flexible back-referencing than Nginx's rewrite. We've occasionally rewritten ten-line Apache rules into a single Nginx location block with set directives and a different control flow. Translating is mechanical but not direct.
.htaccess deny rules → location blocks
# .htaccess in /var/www/html/uploads/
<FilesMatch "\.(php|phtml|phar)$">
Require all denied
</FilesMatch>location ~ ^/uploads/.*\.(php|phtml|phar)$ {
deny all;
return 403;
}Basic auth
<Location /admin>
AuthType Basic
AuthName "Admin Area"
AuthUserFile /etc/apache2/htpasswd
Require valid-user
</Location>location /admin {
auth_basic "Admin Area";
auth_basic_user_file /etc/nginx/htpasswd;
}The htpasswd file format is identical between the two; the file copies over unchanged.
mod_php → PHP-FPM
If the source Apache install uses mod_php, this is the part of the migration that takes the most testing. The behaviour difference between in-process PHP and FastCGI PHP is mostly invisible, but a handful of edge cases bite:
- Apache's
php_valueandphp_admin_valuedirectives in.htaccessdon't work via FPM. You move those settings topool.d/www.confasphp_value[setting] = value. $_SERVER['SCRIPT_FILENAME']and a few related variables behave subtly differently. Some applications notice.- File permissions matter more — FPM runs as its own user, not as the Apache user, and uploads/cache directories need their ownership reviewed.
The Nginx side of the FPM wiring:
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}Phase 3: the cutover playbook
We never do an in-place Apache-to-Nginx swap on a live host. Always migration to a separate host (or container, or pod) with a DNS cutover. The playbook:
T-14 days: build the Nginx target.
- Provision fresh hosts with Nginx and the application stack
- Translate every Apache virtualhost into a server block
- Deploy the application code to the new host with the same release process the customer uses today
T-7 days: shadow traffic.
- Configure a small percentage of traffic (we typically use a header-based route on the cloud load balancer, or a weighted DNS record at 1%) to land on the Nginx target
- Watch error rates, latency p50/p95/p99, and any application-level error logs
- This phase catches the things the config diff missed: a header that was being set by some forgotten module, a rewrite rule in an .htaccess file that wasn't in the inventory, an FPM pool sized too small
T-3 days: ramp.
- Increase the shadow traffic to 10%, then 50%
- Monitor the same metrics
- This is where any remaining inconsistencies surface — they almost always do
T-0: cutover.
- Move 100% of traffic to Nginx
- Apache stays running on the old hosts for at least 24 hours, traffic-quiesced, ready to flip back if needed
- A "flip back" is a single LB config change and takes under a minute
T+7 days: decommission.
- Once we're confident, the Apache hosts are torn down
- The Apache config tarball is archived (we keep these for a year — they're occasionally useful when an obscure feature surfaces post-migration)
What goes wrong
Across a decent number of migrations under our management, the failures cluster into a few patterns:
The forgotten .htaccess. A folder somewhere in the document root has a four-line .htaccess that someone added in 2019 to handle one specific URL pattern. It wasn't in the initial inventory. The customer notices a single broken redirect three days post-cutover.
The fix: a comprehensive find /var/www -name .htaccess -exec cat {} \; during inventory, with the output reviewed line by line. Tedious; necessary.
The LimitRequestBody you didn't know was there. Apache's defaults for max request size are often overridden somewhere in the config tree. Nginx defaults to 1MB for client_max_body_size. Users start hitting 413 errors on the new host for file uploads that worked on Apache. Fix: search the Apache config for LimitRequestBody and set client_max_body_size to match.
Mod_security rules that don't port. ModSecurity rules themselves do port (the rules language is the same), but the way they're loaded and per-virtualhost-included is different in Nginx. The whole ModSecurity config goes in http {} block, and per-server rule exclusions become per-server location blocks. We always migrate ModSecurity as a separate workstream after the base Nginx config is working.
The auth backend. mod_authnz_ldap, mod_auth_kerb, mod_authn_dbd — these have no direct Nginx equivalent. The migration usually involves either moving auth to the application layer, deploying a separate auth proxy (Authelia, oauth2-proxy), or — rarely — accepting that we keep one or two Apache hosts specifically as auth-fronting reverse proxies in front of an otherwise Nginx estate.
When we don't migrate
To close the loop honestly: Apache is fine. We run a lot of Apache in production and have no plans to retire it. The migration question is "does Nginx materially serve this workload better?" and the answer is sometimes yes, sometimes no.
The cases where we say yes:
- Static-asset-heavy workloads where Nginx's event model meaningfully outperforms Apache
- Microservices estates where Nginx's smaller footprint matters per-pod
- Customers consolidating onto a single web server for operational simplicity
- Workloads where the Apache config has accumulated cruft and a clean Nginx rebuild is faster than refactoring Apache in place
The cases where we say no:
- Heavy
.htaccess-driven CMS workloads with multiple editing teams - Dependence on Apache-only modules
- Stable, well-understood Apache installs where the operational benefit doesn't justify the risk
If you're staring at an Apache install and wondering which side of that line it falls on, we'll do a free assessment. Get in touch; we'll spend an hour on the config, give you a real opinion, and not push a migration that doesn't pay back.
Sudhanshu K. is a Staff DevOps engineer at EdgeServers (RemotIQ Pty Ltd, ABN 91 682 628 128). He has run more Apache-to-Nginx migrations than is reasonable and quietly considers Apache underrated in 2026.