mod_rewrite is one of Apache’s most powerful and most confusing modules. The syntax is dense, the flags are cryptic, and debugging is painful. But it’s also everywhere — almost every HTTPS redirect, URL rewrite, and routing rule in Apache uses it.
I’ve spent a lot of time staring at mod_rewrite configs. Here’s what I actually understand about it and how I use it.
Enabling mod_rewrite
On Debian/Ubuntu:
a2enmod rewrite
systemctl restart apache2
Verify it’s loaded:
apache2ctl -M | grep rewrite
The Basics: RewriteRule Syntax
RewriteRule pattern substitution [flags]
The pattern is a regex matching the request URI. The substitution is what to replace it with. Flags modify behavior.
A simple redirect:
RewriteRule ^/old-page$ /new-page [R=301,L]
^/old-page$— matches exactly/old-page/new-page— redirects to thisR=301— send a 301 (permanent) redirectL— Last rule, stop processing further rules
Enabling Rewrite in Directory Config
If you’re using .htaccess, the directory needs AllowOverride All in the main Apache config:
<Directory /var/www/html>
AllowOverride All
</Directory>
In the server config or .htaccess, start with:
RewriteEngine On
HTTPS Redirect
This is the most common use of mod_rewrite. Redirect all HTTP to HTTPS:
<VirtualHost *:80>
ServerName example.com
ServerAlias www.example.com
RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
</VirtualHost>
RewriteCond %{HTTPS} off — condition: only if the request isn’t already HTTPS. Without this, the HTTPS vhost would also match and you’d get an infinite redirect loop.
https://%{HTTP_HOST}%{REQUEST_URI} — preserves the full URL including path and query string.
A simpler alternative (Apache 2.4+) that doesn’t require mod_rewrite:
<VirtualHost *:80>
ServerName example.com
Redirect permanent / https://example.com/
</VirtualHost>
I prefer the Redirect approach for simple HTTPS redirects — less complexity, easier to read.
www to non-www (and vice versa)
Redirect www to non-www:
<VirtualHost *:443>
ServerName www.example.com
RewriteEngine On
RewriteRule ^(.*)$ https://example.com$1 [R=301,L]
</VirtualHost>
Or with a condition on the hostname:
<VirtualHost *:443>
ServerName example.com
ServerAlias www.example.com
RewriteEngine On
RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC]
RewriteRule ^(.*)$ https://%1$1 [R=301,L]
%1 — back-reference to the first RewriteCond capture group (the domain without www).
Non-www to www:
RewriteCond %{HTTP_HOST} !^www\. [NC]
RewriteRule ^(.*)$ https://www.%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
Clean URLs (removing .php extension)
WordPress and many PHP CMSes do this. Make /about serve about.php:
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ $1.php [L]
!-d — not an existing directory.!-f — not an existing file.
So: if the request doesn’t match an existing file or directory, append .php and try that.
Front Controller Pattern
For MVC frameworks (Laravel, Symfony, CodeIgniter), route all requests through index.php:
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
</IfModule>
This is essentially the default .htaccess for Laravel. If the file doesn’t exist (!-f) and it’s not a directory (!-d), send it to index.php. The framework handles routing from there.
Rewrite Flags Reference
The flags I actually use:
[R=301] — redirect with 301 (permanent). Use R=302 for temporary. Browser caches 301s, so use them carefully.
[L] — last rule, stop processing. Almost always include this.
[NC] — case-insensitive matching.
[QSA] — Query String Append. Appends the original query string to the substitution. Without this, rewriting /old?param=value to /new loses the ?param=value.
[NE] — No Escape. Don’t escape special characters in the output.
[P] — Proxy. Pass the request to a backend via mod_proxy. Requires mod_proxy.
[F] — Forbidden. Returns 403. Useful for blocking specific patterns.
Debugging mod_rewrite
Turn on rewrite logging (in Apache config, not .htaccess):
LogLevel alert rewrite:trace3
This is verbose — trace3 logs every rule evaluation. Use it temporarily and disable when done. The log goes to the Apache error log:
tail -f /var/log/apache2/error.log
Test whether mod_rewrite is doing what you expect:
apachectl configtest # Syntax check
curl -v http://example.com/test # Follow redirects
curl -IL http://example.com/test # Show redirect chain headers only
Common Mistakes
Infinite redirect loop — often caused by HTTPS redirect applied to the HTTPS vhost too. Use RewriteCond %{HTTPS} off or separate HTTP and HTTPS vhosts.
Rules not applying — check AllowOverride All is set for the directory. Check RewriteEngine On is present. Check rule order.
Query string lost — add [QSA] flag or include %{QUERY_STRING} in the substitution explicitly.
Cached redirects — browsers cache 301 redirects. If you change a redirect, clear browser cache or test in incognito mode.
Rules applying in wrong order — mod_rewrite processes rules top to bottom, stopping at [L]. Earlier rules apply first.
ProxyPass as an Alternative
For proxying to a backend, ProxyPass is usually simpler than using RewriteRule [P]:
<VirtualHost *:443>
ServerName api.example.com
ProxyRequests Off
ProxyPreserveHost On
ProxyPass / http://127.0.0.1:3000/
ProxyPassReverse / http://127.0.0.1:3000/
</VirtualHost>
Use mod_rewrite with the [P] flag when you need conditional logic or URL transformation. Use ProxyPass when you just want straightforward proxying.
For the SSL side of Apache configuration, the Apache SSL installation guide covers SSL certificates and virtual host setup in detail.