Skip to content
Let’s plan the right software for your processes. Call us for a demo or a quote: +90 546 737 48 29

TR EN DE

Migrating from Apache and .htaccess to Nginx

With Apache, a site owner can set up redirects, password protection or cache settings by placing a .htaccess file in a folder; the server reads that file on every request. Nginx has no such file: all rules live in the server's own configuration files and are read once, when the server is reloaded.

Moving from Apache to Nginx is therefore not a matter of copying files but of translation: for every rule in .htaccess, its Nginx equivalent is written. This guide gives the most common rules as side-by-side examples and explains the usual pitfalls and what to test after the move. If you are new to Nginx, have a look at the what is Nginx guide first.

In brief

  • Nginx does not read .htaccess files; the rules move into the server configuration.
  • Most RewriteRule lines need no rewrite at all: try_files and return are enough.
  • PHP runs through PHP-FPM instead of mod_php; php_value lines move elsewhere.
  • Every change is tested with nginx -t and then reloaded.

Tip

Do the migration in a test environment or on a separate subdomain first, not on the live site; keep the old Apache configuration and the .htaccess files so that you can roll back.

On this page

The logic and order of a migration

  1. Understand the basic difference

    • Where the rules live: With Apache, rules may be scattered across .htaccess files in various folders. With Nginx they are all in the server configuration: typically one file per site containing a server { … } block. The location of the files varies by distribution (/etc/nginx/conf.d is common; /etc/nginx/sites-available is the Debian/Ubuntu layout).
    • Taking effect: A .htaccess file applies the moment it is saved. In Nginx a change is tested with nginx -t and takes effect on reload. A faulty configuration is caught by the test; the running site is not affected.
    • Permissions: Changing the configuration requires server administrator rights. On shared hosting this access is usually not available; there, the hosting provider or the control panel applies the rules.
    • Approach: In Apache everything is solved with RewriteRule. In Nginx the request is first matched to a location, and then the directives of that location are applied.
    Comparison: Apache reads rules from .htaccess files in folders on every request; Nginx reads a single server configuration on reload : Enlarge
  2. Plan the migration

    1. Take an inventory: Find every .htaccess file on the site; those in subfolders (admin panel, upload folder) are easily forgotten. Also look at the rules in the Apache virtual host (VirtualHost) file.
    2. Classify the rules: redirects, URL rewriting, access restrictions, headers, PHP settings. Do not carry over the ones that are no longer used.
    3. Write the Nginx equivalents (the sections below).
    4. Test: nginx -t, then the test list in the test environment.
    5. Switch over and monitor: watch the error log closely during the first few days.
    Bash
    # List every .htaccess file on the site (replace the path with your own site directory)
    find /var/www/example.com -name ".htaccess" -type f
    
    # Test the Nginx configuration; reload if there are no problems
    sudo nginx -t && sudo systemctl reload nginx
    Flow chart: take an inventory, classify the rules, write the Nginx equivalent, test with nginx -t, test in the test environment, switch over and monitor : Enlarge
  3. Learn how location matching is prioritised

    .htaccess rules run in order from top to bottom. In Nginx, by contrast, a request is handled by a single location, and which one is chosen depends not on the order in which they are written but on this priority:

    1. location = /address: if there is an exact match, it is chosen immediately.
    2. Among the prefix locations, the longest match is found. If that location is written with ^~, it is chosen and regular expressions are not checked.
    3. Regular-expression locations (~ case-sensitive, ~* case-insensitive) are tried in the order they appear in the file; the first match is chosen.
    4. If no regular expression matches, the longest prefix location found in step 2 is used.

    The consequence: password protection written inside location /admin/ is not applied to a request for /admin/index.php, because that request is handled by location ~ \.php$. The solution is to write the location with ^~ and nest the PHP location inside it (see the example in the access restriction section).

    Diagram: location matching priority; first the exact match, then the longest prefix written with ^~, then regular expressions in file order, finally the longest prefix : Enlarge
  4. Connect PHP to PHP-FPM

    In most installations Apache runs PHP inside itself (mod_php). Nginx has no PHP built in; it passes .php requests to the separately running PHP-FPM service with fastcgi_pass.

    Nginx
    location ~ \.php$ {
        # Do not send the request to PHP-FPM if the file does not exist
        try_files $uri =404;
    
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    
        # The socket path varies by distribution and PHP version
        fastcgi_pass unix:/run/php/php-fpm.sock;
    }

    The line try_files $uri =404; prevents a non-existent .php address from being sent to PHP-FPM. For the socket path and pool settings, see the guide on Nginx and PHP-FPM.

    Diagram: in Apache, PHP runs inside the server (mod_php); in Nginx, .php requests are passed with fastcgi_pass to the separately running PHP-FPM service : Enlarge

Table of equivalents

TaskApache / .htaccessNginx
Front controllerRewriteCond !-f, !-d + RewriteRuletry_files $uri $uri/ /index.php?$query_string;
HTTPS and wwwRewriteCond %{HTTPS} + RewriteRule [R=301]separate server block + return 301
Single-address redirectRedirect 301location = … { return 301 …; }
Clean URLRewriteRule … [L]rewrite … last; or a location
Password protectionAuthType Basic, AuthUserFileauth_basic, auth_basic_user_file
IP restrictionRequire ipallow / deny
Directory listingOptions -Indexesautoindex off; (already off by default)
Error pageErrorDocumenterror_page
Cache headersmod_expires (ExpiresByType)expires
Response headerHeader setadd_header
PHPmod_phpPHP-FPM + fastcgi_pass
PHP settingphp_value, php_flagPHP-FPM pool setting or .user.ini

Front controller

Most PHP frameworks and content management systems send every address that does not exist to the index.php file. In Apache this is done with a RewriteRule carrying the conditions "not a file, not a folder":

.htaccess (Apache)
RewriteEngine On
# If the request is not for a real file or folder, send it to index.php
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ index.php [L]

In Nginx the same job takes one line. try_files tries the file, then the folder, in turn; if neither exists, it passes the request to the last value:

Nginx
root /var/www/example.com/public;
index index.php index.html;

# Form 1: the application reads the path from REQUEST_URI (most frameworks)
location / {
    try_files $uri $uri/ /index.php?$query_string;
}

# Form 2: the application reads the path from a parameter (index.php?url=...)
# Use these two instead of the "location /" above:
#
# location / {
#     try_files $uri $uri/ @app;
# }
# location @app {
#     rewrite ^/(.*)$ /index.php?url=$1 last;
# }

If your application reads the path from a query parameter (e.g. index.php?url=…), use the second form in the example. rewrite appends the original query string to the new address automatically (the counterpart of QSA in Apache).

HTTPS and www redirects

.htaccess (Apache)
RewriteEngine On
RewriteCond %{HTTPS} off [OR]
RewriteCond %{HTTP_HOST} !^www\. [NC]
RewriteRule ^ https://www.example.com%{REQUEST_URI} [L,R=301]

In Nginx no condition is written for this. A separate server block is opened for each name to be redirected, containing nothing but return 301; the actual site stays in its own block:

Nginx
# 1) Everything arriving over HTTP -> HTTPS + www
server {
    listen 80;
    server_name example.com www.example.com;
    return 301 https://www.example.com$request_uri;
}

# 2) HTTPS but without www -> www
server {
    listen 443 ssl;
    server_name example.com;
    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    return 301 https://www.example.com$request_uri;
}

# 3) The actual site
server {
    listen 443 ssl;
    server_name www.example.com;
    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    root /var/www/example.com/public;
    index index.php index.html;

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

To be able to redirect a request for example.com that arrives over HTTPS, that block needs a valid certificate as well. For certificate installation, see the guide on HTTPS in Nginx.

Single-address redirects and clean URLs

.htaccess (Apache)
# Single-address redirect
Redirect 301 /old-page.html https://www.example.com/new-page

# Moving a whole section
RedirectMatch 301 ^/blog/(.*)$ https://www.example.com/articles/$1

# Clean URL: /product/42 -> product.php?id=42
RewriteEngine On
RewriteRule ^product/([0-9]+)/?$ product.php?id=$1 [L,QSA]
Nginx
# Single-address redirect
location = /old-page.html {
    return 301 https://www.example.com/new-page;
}

# Moving a whole section (query parameters are preserved)
location ~ ^/blog/(.*)$ {
    return 301 https://www.example.com/articles/$1$is_args$args;
}

# Clean URL: /product/42 -> /product.php?id=42
# The pattern starts with /; the original query string is appended automatically
rewrite ^/product/([0-9]+)/?$ /product.php?id=$1 last;
  • Patterns in .htaccess have no leading slash (^product/…); in Nginx the address always starts with / (^/product/…). This is the detail most often forgotten during translation.
  • If all you need is a redirect, use return 301 rather than rewrite … permanent; it is simpler and easier to read.
  • last restarts the location search with the new address, so that /product.php reaches the PHP location.
  • If you have dozens of old addresses to redirect, building a lookup table with the map directive is tidier than writing a separate location for each one.

Directory protection, IP restriction and hidden files

.htaccess (Apache)
# /admin/.htaccess : password protection
AuthType Basic
AuthName "Panel"
AuthUserFile /home/example/.htpasswd
Require valid-user

# /reports/.htaccess : specific IP addresses only (Apache 2.4)
Require ip 203.0.113.10
Require ip 10.0.0.0/24

# Root .htaccess : switch off directory listing, block addresses starting with a dot
Options -Indexes
RewriteEngine On
RewriteRule (^|/)\.(?!well-known) - [F]
Nginx
# Files and folders starting with a dot (.htaccess, .env, .git ...) are blocked;
# .well-known stays open for certificate validation. Write this before the other regular-expression locations.
location ~ /\.(?!well-known) {
    deny all;
}

# Password protection. ^~ : regular-expression locations (e.g. \.php$) are not checked,
# so the PHP location is defined again inside and the protection covers .php files too.
location ^~ /admin/ {
    auth_basic "Panel";
    auth_basic_user_file /etc/nginx/.htpasswd;

    location ~ \.php$ {
        try_files $uri =404;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_pass unix:/run/php/php-fpm.sock;
    }
}

# Specific IP addresses only
location ^~ /reports/ {
    allow 203.0.113.10;
    allow 10.0.0.0/24;
    deny all;

    location ~ \.php$ {
        try_files $uri =404;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_pass unix:/run/php/php-fpm.sock;
    }
}

# Directory listing: already off by default; writing it explicitly is optional
autoindex off;
  • Password file: A file created with htpasswd can in most cases be used in Nginx as well. Confirm that the password hash type is supported by actually logging in; keep the file outside the web root.
  • IP restriction: allow and deny are evaluated in the order written; the first matching rule applies. If a CDN or another proxy sits in front of the site, Nginx sees the proxy's IP address rather than the visitor's; unless the real address is configured separately, the IP restriction will not work as you expect.
  • Directory listing: In Nginx autoindex is off by default; writing an equivalent for the Options -Indexes line is not required.
  • Hidden files: Apache's default configuration hides files starting with .ht by itself; Nginx has no such default. After the migration, the contents of any leftover .htaccess, .env or .git can be downloaded as plain text. Be sure to add the location that blocks addresses starting with a dot, and write it before the other regular-expression locations.
  • "Deny from all" in subfolders: The .htaccess protections in upload or backup folders no longer work either; write a location for each of them. See Nginx security settings.

Error pages and cache headers

.htaccess (Apache)
# Custom error pages
ErrorDocument 404 /404.html
ErrorDocument 500 /500.html

# Cache headers (mod_expires)
<IfModule mod_expires.c>
    ExpiresActive On
    ExpiresByType text/css "access plus 30 days"
    ExpiresByType application/javascript "access plus 30 days"
    ExpiresByType image/webp "access plus 90 days"
    ExpiresByType image/jpeg "access plus 90 days"
</IfModule>
Nginx
# Custom error pages
error_page 404 /404.html;
error_page 500 502 503 504 /50x.html;

# Cache headers
location ~* \.(?:css|js)$ {
    expires 30d;
}
location ~* \.(?:webp|jpg|jpeg|png|gif|svg|ico)$ {
    expires 90d;
}

For error responses generated by PHP (a 404 returned by the application, for example), error_page does not step in by itself; that requires fastcgi_intercept_errors on; in the relevant location. For cache lifetimes and compression, see the guide on caching and compression.

php_value and php_flag lines

php_value and php_flag lines in .htaccess only work with mod_php. Nginx does not read them, and neither does PHP-FPM. There are two equivalents:

  • PHP-FPM pool setting: php_value[…] / php_flag[…], or php_admin_value[…] / php_admin_flag[…], which the application cannot override. PHP-FPM is reloaded after the change.
  • .user.ini: Placed in the site folder and needing no reload; however, it only applies to settings that can be changed per directory, and because PHP re-reads the file at intervals (300 seconds by default) a change may not show up straight away.
.htaccess (Apache)
# Works with mod_php only; Nginx + PHP-FPM does not read these lines
php_value upload_max_filesize 32M
php_value post_max_size 32M
php_value memory_limit 256M
php_flag display_errors off
php.ini
; Option 1: a .user.ini file in the site's root directory
upload_max_filesize = 32M
post_max_size = 32M
memory_limit = 256M
display_errors = Off

; Option 2: the PHP-FPM pool file (e.g. www.conf; its location varies by distribution)
; A value given with php_admin_* cannot be changed by the application with ini_set()
php_admin_value[upload_max_filesize] = 32M
php_admin_value[post_max_size] = 32M
php_admin_value[memory_limit] = 256M
php_admin_flag[display_errors] = off

If you are carrying over an upload limit, do not forget the Nginx side: client_max_body_size defaults to 1m; unless it is raised, large uploads are rejected with a 413 error before they ever reach PHP.

The "if" directive and other pitfalls

  • if is not a programming-language condition. Inside a location, the directives that can safely be used with if are return and rewrite; combined with other directives it can produce unexpected results. Rather than translating RewriteCond lines one-to-one into if, first ask: can this be done with try_files, return, a separate server or location block, or map? Most of the time it can.
  • The difference in order: .htaccess rules are processed in sequence and affect one another; in Nginx a single location is selected. Writing the rules one below the other in the same order does not give the same result.
  • Inheritance: Directives such as add_header are not inherited from the level above once the same directive is written on the lower level.
  • Ready-made converters: Online converters can provide a good first draft, but their output often contains unnecessary if blocks. Review every line and make sure you understand it.
  • The application's own .htaccess: Off-the-shelf software (content management systems, frameworks) generates a .htaccess on installation and may change it during updates. Take the Nginx equivalent of those rules from the software's documentation and check it again after updates.

Test list after the migration

Bash
# Redirects: status code and Location header
curl -sI http://example.com/ | grep -i -E "^HTTP|^location"
curl -sI https://example.com/ | grep -i -E "^HTTP|^location"
curl -sI https://www.example.com/old-page.html | grep -i -E "^HTTP|^location"

# Hidden files: should return 403 or 404, with no content
curl -s -o /dev/null -w "%{http_code}\n" https://www.example.com/.htaccess
curl -s -o /dev/null -w "%{http_code}\n" https://www.example.com/.env
curl -s -o /dev/null -w "%{http_code}\n" https://www.example.com/.git/config

# Password-protected area: should return 401 (including .php)
curl -s -o /dev/null -w "%{http_code}\n" https://www.example.com/admin/index.php

# Non-existent address: 404
curl -s -o /dev/null -w "%{http_code}\n" https://www.example.com/no-such-page
  • The home page, an inner page and a page with a clean URL (a product or an article, for example) open.
  • http:// and the address without www go to the correct HTTPS address with a 301 in a single step.
  • Redirects from old addresses work; query parameters are preserved.
  • A non-existent address returns 404 and the custom error page; so does a non-existent .php address.
  • The admin panel asks for a password, including for .php files.
  • Addresses such as /.htaccess, /.env and /.git/config are not accessible.
  • PHP does not run in the upload folder; no directory listing appears.
  • Form submission, login and file upload (including a large file) work.
  • Static files arrive with the correct Cache-Control and Content-Type.
  • The security headers are in place (see security headers).
  • There are no unexpected entries in the Nginx and PHP-FPM error logs.

Frequently asked questions

Can I copy my .htaccess file to Nginx as it is?

No. Nginx does not read .htaccess files, and the syntax is different. The equivalent of each rule is written into the server configuration; as far as Nginx is concerned, the file itself is an ordinary text file and access to it must be blocked.

Can I write Nginx rules myself on shared hosting?

Usually not; the server configuration requires administrator rights. Some control panels offer a limited area for this or run Nginx in front of Apache, in which case .htaccess keeps working. Ask your hosting provider.

Does .htaccess work if I use Nginx in front of Apache?

Yes. If Nginx is placed in front as a reverse proxy and passes requests on to Apache, Apache still applies the .htaccess rules. Only static files that Nginx serves directly do not pass through those rules.

Do I have to write an "if" for every RewriteCond?

No; most of the time it is not needed. The file-exists check is covered by try_files, domain and HTTPS conditions by separate server blocks, and address-based conditions by location. "if" should only be used together with return or rewrite, and only when there is no other way.

After the migration the site is slow or returns 502. Where should I look?

Start with the Nginx error log and the PHP-FPM log. A 502 usually means that PHP-FPM is not running or that the socket path is wrong.

BYK Yazılım Support Team
This guide is written and regularly reviewed by the BYK Yazılım support team. Last updated: 4 October 2026.

Related guides

Let us talk about your website infrastructure

BYK Yazılım builds corporate websites. Write to us with any questions about your site.

Contact us Our corporate website service