How to Route All Requests to a Single PHP Script in Nginx (Apache .htaccess to Nginx Migration Guide)


3 views

When migrating from Apache to Nginx, one of the most common pain points is replicating Apache's mod_rewrite functionality. The specific case we're examining involves routing all requests (except actual files/directories) to a single PHP script (index.php), which was previously handled by this .htaccess rule:

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

The equivalent Nginx configuration requires proper use of try_files directive combined with location matching. Here's the essential implementation:

server {
    listen 80;
    server_name swingset.serverboy.net;
    root /var/www/swingset;
    
    index index.php index.html;
    
    location / {
        try_files $uri $uri/ /index.php?$args;
    }
    
    location ~ \.php$ {
        include fastcgi_params;
        fastcgi_pass 127.0.0.1:9000;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_intercept_errors on;
    }
}

try_files directive: This attempts to serve the requested URI as a file ($uri), then as a directory ($uri/), and finally falls back to index.php while preserving query parameters (?$args).

PHP handling: The second location block ensures PHP files are processed by FastCGI, matching Apache's PHP module behavior.

For more complex routing scenarios, you might need additional configurations:

# Handle clean URLs without query string
location / {
    try_files $uri $uri/ @rewrite;
}

location @rewrite {
    rewrite ^/(.*)$ /index.php?_url=/$1;
}

# Additional security for PHP files
location ~* \.php$ {
    fastcgi_pass unix:/var/run/php/php7.4-fpm.sock;
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    
    # Security headers
    add_header X-Frame-Options "SAMEORIGIN";
    add_header X-XSS-Protection "1; mode=block";
    add_header X-Content-Type-Options "nosniff";
}

1. Missing fastcgi_param: Forgetting to set SCRIPT_FILENAME will result in blank responses

2. Incorrect root path: Ensure the root directive points to your application's document root

3. Cache issues: During testing, disable browser caching to see immediate changes

For high-traffic sites, consider adding these optimizations:

# Cache file metadata
open_file_cache max=1000 inactive=20s;
open_file_cache_valid 30s;
open_file_cache_min_uses 2;

# Buffer settings
fastcgi_buffers 16 16k;
fastcgi_buffer_size 32k;

# Timeouts
fastcgi_read_timeout 300;

When migrating from Apache to Nginx, one of the most common pain points is replicating Apache's mod_rewrite behavior. The specific case we're addressing here involves routing all requests (except those for existing files/directories) to a single PHP script - typically index.php for front controller patterns.

The original Apache configuration uses .htaccess with these key directives:

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

This does three things:

  1. Checks if the requested file doesn't exist (!-f)
  2. Checks if the requested directory doesn't exist (!-d)
  3. If both conditions are met, routes the request to index.php

Here's the complete Nginx server block that accomplishes the same functionality:

server {
    listen 80;
    server_name yourdomain.com;
    root /var/www/your_project;
    index index.php;

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

    location ~ \.php$ {
        include fastcgi_params;
        fastcgi_pass unix:/var/run/php/php7.4-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param PATH_INFO $fastcgi_path_info;
    }

    location ~ /\.ht {
        deny all;
    }
}

try_files directive: This is Nginx's equivalent to Apache's rewrite conditions. It checks for files in this order:

  • The exact requested URI ($uri)
  • The requested URI as a directory ($uri/)
  • Finally falls back to index.php while preserving query strings

For more complex routing needs, you might want to:

# Handle pretty URLs without query strings
location / {
    try_files $uri $uri/ /index.php$is_args$args;
}

# Alternative for PHP frameworks
location / {
    try_files $uri /index.php$request_uri;
}

# With PATH_INFO support
location ~ ^/index\.php(/|$) {
    fastcgi_split_path_info ^(.+\.php)(/.+)$;
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    fastcgi_param PATH_INFO $fastcgi_path_info;
    fastcgi_pass 127.0.0.1:9000;
}

Unlike Apache's .htaccess files (which are read on every request), Nginx configurations are loaded at startup. This means:

  • Changes require server reload (nginx -s reload)
  • No per-directory configuration overhead
  • Generally better performance for high-traffic sites

Always verify your configuration before applying changes:

nginx -t

Then reload the configuration:

nginx -s reload

Watch out for these issues:

  • Incorrect root directory paths
  • Missing fastcgi_param SCRIPT_FILENAME
  • PHP-FPM not running or misconfigured
  • File permission issues

Here's a production-tested configuration for a Laravel application:

server {
    listen 80;
    server_name laravelapp.com;
    root /var/www/laravel/public;

    add_header X-Frame-Options "SAMEORIGIN";
    add_header X-XSS-Protection "1; mode=block";
    add_header X-Content-Type-Options "nosniff";

    index index.php;

    charset utf-8;

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

    location = /favicon.ico { access_log off; log_not_found off; }
    location = /robots.txt  { access_log off; log_not_found off; }

    error_page 404 /index.php;

    location ~ \.php$ {
        fastcgi_pass unix:/var/run/php/php8.0-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include fastcgi_params;
    }

    location ~ /\.(?!well-known).* {
        deny all;
    }
}