An .htaccess file is a per-directory configuration file for Apache. Rules in it apply to that directory and everything below it, take effect immediately without reloading Apache, and can be edited by whoever manages the site — which is why WordPress, Laravel and countless PHP apps ship one.

Make sure .htaccess is enabled

Apache only reads .htaccess where the main config allows it. In the virtual host:

<Directory /var/www/example.com>
    AllowOverride All
    Require all granted
</Directory>

AllowOverride None (the default for /var/www on Debian/Ubuntu) silently ignores .htaccess. You can also allow only certain categories, e.g. AllowOverride FileInfo AuthConfig. Reload Apache after changing it. Rewrite rules also need sudo a2enmod rewrite.

Redirects

# One page
Redirect 301 /old-page/ https://example.com/new-page/

# A whole folder (keeps the rest of the path)
RedirectMatch 301 ^/blog/2019/(.*)$ https://example.com/archive/$1

Force HTTPS and a canonical hostname

RewriteEngine On

# HTTP → HTTPS
RewriteCond %{HTTPS} off
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]

# www → non-www
RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC]
RewriteRule ^ https://%1%{REQUEST_URI} [L,R=301]

Behind a proxy or Cloudflare, %{HTTPS} is off even for HTTPS visitors — check %{HTTP:X-Forwarded-Proto} instead, or you’ll create a redirect loop.

Front controller (pretty URLs)

The classic WordPress block:

<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
</IfModule>

“If the requested file or directory doesn’t exist, hand the request to index.php.” More on rewrite syntax in Apache mod_rewrite.

Caching and compression

<IfModule mod_expires.c>
    ExpiresActive On
    ExpiresByType image/webp "access plus 30 days"
    ExpiresByType image/png "access plus 30 days"
    ExpiresByType text/css "access plus 30 days"
    ExpiresByType application/javascript "access plus 30 days"
    ExpiresByType font/woff2 "access plus 1 year"
</IfModule>

<IfModule mod_deflate.c>
    AddOutputFilterByType DEFLATE text/html text/css application/javascript application/json image/svg+xml
</IfModule>

Enable the modules: sudo a2enmod expires deflate headers.

Security rules

# No directory listings
Options -Indexes

# Block access to sensitive files
<FilesMatch "^(\.env|composer\.(json|lock)|wp-config\.php|readme\.html)$">
    Require all denied
</FilesMatch>

# Block hidden files except .well-known
RewriteRule (^|/)\.(?!well-known/) - [F]

# Security headers
<IfModule mod_headers.c>
    Header always set X-Content-Type-Options "nosniff"
    Header always set X-Frame-Options "SAMEORIGIN"
    Header always set Referrer-Policy "strict-origin-when-cross-origin"
</IfModule>

No PHP execution in uploads — put an .htaccess in the uploads directory:

<FilesMatch "\.(php|phtml|phar)$">
    Require all denied
</FilesMatch>

Password-protect a directory

AuthType Basic
AuthName "Restricted"
AuthUserFile /etc/apache2/.htpasswd
Require valid-user

Create the file with sudo htpasswd -c -B /etc/apache2/.htpasswd alice — outside the web root.

Restrict by IP

<RequireAny>
    Require ip 203.0.113.0/24
    Require ip 198.51.100.25
</RequireAny>

(Apache 2.4 syntax. The old Order allow,deny / Allow from directives are from Apache 2.2.)

PHP settings

With mod_php you can use php_value upload_max_filesize 64M. With PHP-FPM (the modern setup), php_value lines in .htaccess cause a 500 error — use a .user.ini file in the site root, or the FPM pool config.

The performance cost

With AllowOverride All, Apache looks for .htaccess in every directory along each request’s path, on every request. On a busy server that’s measurable overhead. On your own VPS, the faster approach is to move the rules into the virtual host config and set AllowOverride None. Keep .htaccess where applications need to manage rules themselves (WordPress plugins often write to it).

Troubleshooting

  • 500 Internal Server Error right after editing — syntax error or a directive not allowed by AllowOverride; the Apache error log names the line.
  • Rules ignored — AllowOverride None, mod_rewrite disabled, or the file isn’t named exactly .htaccess.
  • Redirect loops — HTTPS detection behind proxies.

Using Nginx? It doesn’t read .htaccess; rules must be translated into the server config — see Nginx rewrite rules and Nginx vs Apache.

  • Apache
  • .htaccess
  • Web Server