Migrating Apache .htaccess to NGINX with AI
Translate Apache mod_rewrite, RedirectMatch, and AuthType Basic into NGINX with AI, then verify every redirect and run nginx -t before you cut over traffic.
- #nginx
- #ai
- #apache
- #migration
- #mod_rewrite
The last Apache-to-NGINX migration I did landed on my desk because the old box was running PHP behind a forest of .htaccess files nobody had touched in six years. There were nine of them scattered through the docroot, each one a little archaeological layer of redirects, auth rules, and mod_rewrite incantations added by people who’d long since left. My job was to make all of that behave identically on NGINX — same URLs, same redirects, same 401 prompts — without breaking a single inbound link or SEO redirect chain. That is exactly the kind of tedious, error-prone translation work AI is genuinely good at drafting. It is also exactly the kind of work where a wrong translation silently 200s a page that should 301, and you don’t find out until traffic craters. So AI drafts, I verify, and nginx -t gets the final say.
The first thing to unlearn: there is no .htaccess
The biggest conceptual trap isn’t syntax — it’s architecture. Apache reads .htaccess files per-directory, at request time, walking down the tree. NGINX does none of that. Configuration is centralized in nginx.conf and its includes, loaded once at startup. There is no per-directory override, no runtime config scan, and that’s a deliberate performance decision, not a missing feature.
This matters because the naive migration is “find every .htaccess and translate it in place.” Wrong instinct. The right move is to collapse all of those scattered rules into location blocks inside one server context, and to rethink them rather than transliterate them. A RewriteRule that made sense as a directory-local patch often becomes a clean try_files or return once you can see the whole server block at once. If you’re newer to NGINX’s request model, my NGINX category has more on how location matching actually resolves.
When I prime the AI, I’m explicit about this so it doesn’t just hand me line-by-line rewrite directives:
You are migrating Apache
.htaccessconfig to NGINX. NGINX has no per-directory config, so consolidate everything intolocationblocks in a single server context. Prefertry_filesandreturn 301overrewritewherever the behavior allows it. For each rule, tell me the original intent in one sentence, then the NGINX equivalent, and flag anything where the behavior might differ.
Translating mod_rewrite
Here’s the classic front-controller pattern — route everything that isn’t a real file to index.php. Nearly every PHP app ships this in .htaccess:
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php [QSA,L]
The literal translation reaches for if (!-f) blocks, which is the well-known NGINX footgun — if inside location is evaluated per-request and interacts badly with other directives. The idiomatic NGINX answer throws the whole RewriteCond/RewriteRule dance away and uses try_files, which does exactly this “serve the file if it exists, otherwise fall through” logic natively:
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
include fastcgi_params;
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
That’s the lesson in miniature: the two RewriteCond lines testing -f and -d collapse into the $uri $uri/ arguments of try_files, and [QSA] becomes $query_string. Translating the rule literally would have worked, but it would have been slower, uglier, and full of if statements you’d regret. AI reaches for the literal version unless you tell it not to — which is why the prompt above explicitly asks for try_files.
For a rule that genuinely needs a rewrite — say, a versioned API prefix — the mapping is more direct:
RewriteRule ^api/v1/(.*)$ /api/$1 [L]
location /api/v1/ {
rewrite ^/api/v1/(.*)$ /api/$1 last;
}
Note [L] becomes last, and the leading ^/ — Apache .htaccess matches without the leading slash, NGINX matches with it. That off-by-one slash is the single most common bug the AI introduces, so it’s the first thing I check.
RedirectMatch becomes return 301
External redirects are where mistakes cost you real traffic, so I treat these with the most suspicion. Apache:
RedirectMatch 301 ^/old-blog/(.*)$ https://example.com/blog/$1
Redirect 301 /pricing /plans
location ~ ^/old-blog/(.*)$ {
return 301 https://example.com/blog/$1;
}
location = /pricing {
return 301 /plans;
}
return 301 is faster and clearer than rewrite ... permanent — there’s no regex rewrite engine spun up, NGINX just emits the status and Location header. Use a regex location ~ when you’re capturing groups, and an exact location = for a single fixed path. Resist the urge to cram every redirect into one giant regex; separate location blocks are easier to read and easier to verify one at a time.
Directory, Files, and Auth blocks
<Directory> and <Files> map onto location blocks. The deny-all pattern protecting dotfiles and includes:
<FilesMatch "^\.ht">
Require all denied
</FilesMatch>
location ~ /\.ht {
deny all;
}
And HTTP Basic auth — AuthType Basic translates almost one-to-one to auth_basic. The htpasswd file format is even identical, so you can reuse the existing file:
<Directory "/var/www/admin">
AuthType Basic
AuthName "Restricted"
AuthUserFile /etc/apache2/.htpasswd
Require valid-user
</Directory>
location /admin {
auth_basic "Restricted";
auth_basic_user_file /etc/apache2/.htpasswd;
}
One real difference to flag: Apache’s Require valid-user is implicit in NGINX — pointing at the user file means any valid user gets in. If the original config used Require user alice to restrict to specific accounts, NGINX auth_basic can’t express that, and you need a map or a separate user file. The AI will happily gloss over that nuance, so it’s exactly the kind of thing I make it flag rather than trust it to handle.
Verify, then validate — never the other way around
This is the part you cannot delegate. AI produces config that looks right and parses clean, and “parses clean” is not “behaves correctly.” A redirect that should be 301 but emits 302 passes nginx -t every time. So I separate two checks: behavior verification (do the redirects do the right thing?) and syntax validation (does NGINX accept the file?).
Syntax first, because it’s cheap:
nginx -t
# nginx: configuration file /etc/nginx/nginx.conf test is successful
nginx -s reload
Then behavior. I curl every redirect I migrated and assert on the status code and the Location header — this is the step that catches the off-by-one slash and the 301-vs-302 mistakes:
# Confirm the old blog path 301s to the new host, preserving the slug
curl -sI https://example.com/old-blog/my-post | grep -iE '^(HTTP|location)'
# HTTP/2 301
# location: https://example.com/blog/my-post
# Confirm the front controller serves a missing path through index.php, not a 404
curl -sI https://example.com/some/clean/url | grep -i '^HTTP'
# HTTP/2 200
For a large migration I’ll have the AI generate this curl checklist straight from the original .htaccess — every redirect rule becomes one assertion of expected status and target. That turns “did I get all nine files right?” into a script I can run before and after cutover and diff. The AI is fast at producing the matrix; I’m the one who reads the diff and decides whether to flip DNS.
That division of labor is the whole point. AI collapses a day of careful, boring translation into an hour of drafting, and it’s better than I am at remembering that [L] is last and [QSA] is $query_string. But it does not know your traffic, it does not know which redirect feeds your highest-value backlinks, and it will confidently transliterate a rule that should have been rethought. You stay in control of nginx -t, the curl checklist, and the reload. If you want a second pass once the config is live, I run the result through the same workflow in reviewing NGINX security configuration with AI, and the migration prompts I lean on live in my prompts library.
Download the Free 500-Prompt DevOps AI Toolkit
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.