Why Convert from .htaccess to Nginx?
Migrating from Apache to Nginx is one of the most common web server transitions. Nginx uses significantly less memory under load, handles more concurrent connections, and serves static files faster than Apache. But there's a fundamental difference that trips up many administrators: Nginx has no .htaccess equivalent.
In Apache, .htaccess files provide per-directory, per-request configuration that's parsed on every request. Nginx takes a different approach: all configuration lives in centralized config files that are parsed once when the server starts or reloads. This design is one of the reasons Nginx is faster, but it means every .htaccess rule must be converted to Nginx syntax and placed in the server or location block of your Nginx configuration.
.htaccess files on every request (dynamic). Nginx reads its config files once at startup (static). This means Nginx configuration changes require a reload (nginx -s reload), but the payoff is zero filesystem overhead per request.
Understanding the Mapping
Before diving into specific conversions, it helps to understand how Apache and Nginx concepts map to each other:
| Apache Concept | Nginx Equivalent | Notes |
|---|---|---|
.htaccess | server { } or location { } | Config goes in the vhost file |
RewriteEngine On | Not needed | Nginx rewrite is always available |
RewriteRule | rewrite or location | Different syntax, same concept |
RewriteCond | if or map | Nginx if has limitations |
<Directory> | location | Path-based matching |
ErrorDocument | error_page | Similar functionality |
mod_expires | expires directive | Built-in, no module needed |
mod_deflate | gzip directives | Built-in, no module needed |
mod_headers | add_header | Built-in |
Require all denied | deny all; or return 403; | Access control |
Redirect Conversions
HTTP to HTTPS
Apache .htaccess
RewriteCond %{HTTPS} off
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
Nginx Config
listen 80;
server_name example.com;
return 301 https://$host$request_uri;
}
return 301 in a dedicated server block for port 80. The return directive is also faster than rewrite because it doesn't involve regex processing.
WWW to Non-WWW
Apache .htaccess
RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC]
RewriteRule ^(.*)$ https://%1/$1 [R=301,L]
Nginx Config
listen 443 ssl;
server_name www.example.com;
return 301 https://example.com$request_uri;
}
Specific Page Redirects
Apache .htaccess
RedirectMatch 301 ^/blog/(.*)$ /articles/$1
Nginx Config
return 301 /new-page;
}
rewrite ^/blog/(.*)$ /articles/$1 permanent;
PHP Framework Conversions
The most common .htaccess patterns come from PHP frameworks. Here are the exact conversions for the most popular ones.
WordPress
Apache .htaccess
RewriteEngine On
RewriteBase /
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
# END WordPress
Nginx Config
try_files $uri $uri/ /index.php?$args;
}
try_files $uri $uri/ /index.php?$args means: first try serving the exact file, then try it as a directory, and if neither exists, pass the request to index.php with the query string preserved. This single line replaces WordPress's entire .htaccess block.
Laravel
Apache .htaccess (public/)
RewriteCond %{REQUEST_URI} !(\.[a-zA-Z0-9]{2,4})$
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.php [L]
RewriteCond %{HTTP:Authorization} .
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
Nginx Config
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass unix:/var/run/php-fpm.sock;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
}
CodeIgniter
Apache .htaccess
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php/$1 [L]
Nginx Config
try_files $uri $uri/ /index.php$uri?$args;
}
Security Header Conversions
Apache .htaccess
Header set X-Frame-Options "SAMEORIGIN"
Header set X-Content-Type-Options "nosniff"
Header set X-XSS-Protection "1; mode=block"
Header set Strict-Transport-Security "max-age=31536000"
Header set Referrer-Policy "strict-origin-when-cross-origin"
</IfModule>
Nginx Config
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Strict-Transport-Security "max-age=31536000" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
always, Nginx's add_header only adds headers on successful (2xx) responses. With always, headers are added to all responses including errors (4xx, 5xx). For security headers, always use always.
Caching and Compression
Browser Caching
Apache (mod_expires)
ExpiresActive On
ExpiresByType image/jpeg "access plus 1 year"
ExpiresByType text/css "access plus 1 month"
ExpiresByType application/javascript "access plus 1 month"
</IfModule>
Nginx
expires 1y;
add_header Cache-Control "public, immutable";
}
location ~* \.(css|js)$ {
expires 1M;
add_header Cache-Control "public";
}
Gzip Compression
Apache (mod_deflate)
AddOutputFilterByType DEFLATE text/html
AddOutputFilterByType DEFLATE text/css
AddOutputFilterByType DEFLATE application/javascript
</IfModule>
Nginx
gzip_vary on;
gzip_min_length 1024;
gzip_comp_level 5;
gzip_types
text/plain
text/css
application/javascript
application/json
application/xml
image/svg+xml;
Access Control Conversions
Block Sensitive Files
Apache .htaccess
<FilesMatch "\.(env|log|sql|bak)$">
Require all denied
</FilesMatch>
<FilesMatch "^\.">
Require all denied
</FilesMatch>
Nginx Config
location ~* \.(env|log|sql|bak)$ {
deny all;
return 404;
}
location ~ /\. {
deny all;
return 404;
}
IP-Based Access Control
Apache .htaccess
Require ip 192.168.1.0/24
</RequireAll>
Nginx Config
allow 192.168.1.0/24;
deny all;
}
Custom Error Pages
Apache .htaccess
ErrorDocument 500 /errors/500.html
ErrorDocument 503 /errors/maintenance.html
Nginx Config
error_page 500 502 503 504 /errors/500.html;
location = /errors/404.html {
internal;
}
Reverse Proxy
Apache .htaccess
RewriteRule ^api/(.*)$ http://localhost:3000/$1 [P,L]
ProxyPassReverse /api/ http://localhost:3000/
Nginx Config
proxy_pass http://localhost:3000/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
Rate Limiting
Apache doesn't have a built-in rate limiter (it requires mod_evasive or mod_ratelimit), but Nginx includes powerful rate limiting natively:
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
# Apply to specific location
location /wp-login.php {
limit_req zone=login burst=3 nodelay;
fastcgi_pass unix:/var/run/php-fpm.sock;
include fastcgi_params;
}
The "if" Directive: Handle with Care
Nginx's if directive is one of the most misunderstood features. Unlike Apache's RewriteCond, Nginx's if has significant limitations and should be used sparingly.
location block, if creates an implicit nested location. This can cause unexpected behavior with other directives. Safe uses of if are: return and rewrite inside if. Avoid if for anything else. Use map or multiple location blocks instead.
if ($request_uri ~* "\.php$") {
return 403;
}
# BETTER: Use map for complex conditions
map $http_user_agent $bad_bot {
default 0;
"~*crawl" 1;
"~*bot" 1;
"~*spider" 1;
}
server {
if ($bad_bot) {
return 403;
}
}
Testing Your Nginx Configuration
Unlike .htaccess which either works or breaks silently, Nginx provides a configuration testing command that catches syntax errors before you apply changes:
$ nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
# Apply changes (graceful reload, zero downtime)
$ nginx -s reload
# If test fails:
$ nginx -t
nginx: [emerg] unexpected "}" in /etc/nginx/sites-enabled/example.com:42
nginx: configuration file /etc/nginx/nginx.conf test failed
nginx -t before every nginx -s reload. A configuration error in a reload command will prevent Nginx from restarting, potentially taking all your sites offline.
Common Pitfalls
Trailing Slash in proxy_pass
proxy_pass http://localhost:3000; (no trailing slash) passes the full URI including the location prefix. proxy_pass http://localhost:3000/; (with trailing slash) strips the location prefix. This single slash causes more Nginx confusion than any other syntax.
Location Block Ordering
Nginx processes location blocks by specificity, not order: exact (=) first, then longest prefix, then regex (~) in order. Apache processes rules top-to-bottom. This difference frequently causes conversion bugs.
add_header Inheritance
If you add add_header directives inside a location block, ALL headers from the parent server block are lost. You must repeat all headers in every location that defines its own headers.
Regex Differences
Apache uses PCRE with implicit anchoring. Nginx uses PCRE but requires explicit anchoring (^ and $). Apache's RewriteRule ^(.*)$ becomes Nginx's rewrite ^(.*)$ ... but behavior differs in subtle ways.
Nginx on Panelica
Panelica uses Nginx as its primary web server and generates optimized Nginx configurations automatically when you set up domains. The panel supports both Nginx and Apache per domain, so you can use the right web server for each site without managing configuration files manually.
try_files and location blocks are generated automatically. No manual Nginx configuration needed.
Conversion Checklist
Use this checklist when migrating from Apache to Nginx:
- Convert all RewriteRule directives to Nginx rewrite or try_files
- Convert RewriteCond conditions to if/map directives
- Convert mod_expires to Nginx expires directives in location blocks
- Convert mod_deflate to Nginx gzip configuration
- Convert mod_headers to add_header directives (with "always" flag)
- Convert ErrorDocument to error_page directives
- Convert access control (Require) to allow/deny directives
- Test configuration with nginx -t before reloading
- Verify all redirects work with curl -I (check status codes)
- Test PHP processing for all application types
- Verify security headers with securityheaders.com
Conclusion
Converting .htaccess rules to Nginx configuration is not a one-to-one translation exercise. Nginx's architecture is fundamentally different from Apache's, and the best Nginx configurations often look nothing like the Apache rules they replace. The try_files directive alone replaces most of what .htaccess rewrite rules do, but in a more efficient and readable way.
The key to a successful migration is understanding the conceptual mapping between Apache and Nginx, not just the syntax differences. Apache's per-request .htaccess parsing becomes Nginx's static configuration. Apache's RewriteCond + RewriteRule pairs become Nginx's location blocks and try_files directives. Apache's module-based features become Nginx's built-in directives.
Take the time to test each conversion thoroughly with nginx -t and manual HTTP requests. The result will be a faster, more memory-efficient web server that handles more concurrent connections with less overhead. And if you're using a panel like Panelica, much of this configuration is handled automatically, letting you focus on your applications rather than web server syntax.