Tutorial

How to Convert .htaccess Rules to Nginx Configuration

May 09, 2026

Back to Blog

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.

Key difference: Apache reads .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 ConceptNginx EquivalentNotes
.htaccessserver { } or location { }Config goes in the vhost file
RewriteEngine OnNot neededNginx rewrite is always available
RewriteRulerewrite or locationDifferent syntax, same concept
RewriteCondif or mapNginx if has limitations
<Directory>locationPath-based matching
ErrorDocumenterror_pageSimilar functionality
mod_expiresexpires directiveBuilt-in, no module needed
mod_deflategzip directivesBuilt-in, no module needed
mod_headersadd_headerBuilt-in
Require all denieddeny all; or return 403;Access control

Redirect Conversions

HTTP to HTTPS

Apache .htaccess

RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]

Nginx Config

server {
  listen 80;
  server_name example.com;
  return 301 https://$host$request_uri;
}
Nginx advantage: Notice how much cleaner the Nginx version is. Instead of a conditional rewrite rule, Nginx uses a simple 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

RewriteEngine On
RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC]
RewriteRule ^(.*)$ https://%1/$1 [R=301,L]

Nginx Config

server {
  listen 443 ssl;
  server_name www.example.com;
  return 301 https://example.com$request_uri;
}

Specific Page Redirects

Apache .htaccess

Redirect 301 /old-page /new-page
RedirectMatch 301 ^/blog/(.*)$ /articles/$1

Nginx Config

location = /old-page {
  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

# BEGIN WordPress
RewriteEngine On
RewriteBase /
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
# END WordPress

Nginx Config

location / {
  try_files $uri $uri/ /index.php?$args;
}
The try_files directive: This is Nginx's most powerful tool for replacing Apache's rewrite rules. 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/)

RewriteEngine On
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

root /var/www/app/public;

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

RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php/$1 [L]

Nginx Config

location / {
  try_files $uri $uri/ /index.php$uri?$args;
}

Security Header Conversions

Apache .htaccess

<IfModule mod_headers.c>
  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-Frame-Options "SAMEORIGIN" always;
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;
The "always" parameter: Without 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)

<IfModule mod_expires.c>
  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

location ~* \.(jpg|jpeg|png|gif|webp|ico)$ {
  expires 1y;
  add_header Cache-Control "public, immutable";
}

location ~* \.(css|js)$ {
  expires 1M;
  add_header Cache-Control "public";
}

Gzip Compression

Apache (mod_deflate)

<IfModule mod_deflate.c>
  AddOutputFilterByType DEFLATE text/html
  AddOutputFilterByType DEFLATE text/css
  AddOutputFilterByType DEFLATE application/javascript
</IfModule>

Nginx

gzip on;
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

Options -Indexes

<FilesMatch "\.(env|log|sql|bak)$">
  Require all denied
</FilesMatch>

<FilesMatch "^\.">
  Require all denied
</FilesMatch>

Nginx Config

autoindex off;

location ~* \.(env|log|sql|bak)$ {
  deny all;
  return 404;
}

location ~ /\. {
  deny all;
  return 404;
}

IP-Based Access Control

Apache .htaccess

<RequireAll>
  Require ip 192.168.1.0/24
</RequireAll>

Nginx Config

location /admin {
  allow 192.168.1.0/24;
  deny all;
}

Custom Error Pages

Apache .htaccess

ErrorDocument 404 /errors/404.html
ErrorDocument 500 /errors/500.html
ErrorDocument 503 /errors/maintenance.html

Nginx Config

error_page 404 /errors/404.html;
error_page 500 502 503 504 /errors/500.html;

location = /errors/404.html {
  internal;
}

Reverse Proxy

Apache .htaccess

RewriteEngine On
RewriteRule ^api/(.*)$ http://localhost:3000/$1 [P,L]
ProxyPassReverse /api/ http://localhost:3000/

Nginx Config

location /api/ {
  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:

# Define rate limit zone (in http block)
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.

The Nginx "if is evil" rule: Inside a 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.
# SAFE: Using if with return/rewrite
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:

# Test configuration syntax
$ 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
Always test before reload: Make it a habit to run 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.

Zero-config Nginx with Panelica: When you create a domain in Panelica, the panel automatically generates an optimized Nginx server block with proper PHP-FPM upstream, SSL configuration, security headers, static file caching, and gzip compression. For WordPress, Laravel, and other PHP frameworks, the correct 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.

Share:
Pay once. Host forever.