Index & Autoindex #

When a browser requests a directory — a URL ending in / or with no file extension — Nginx needs to decide what to return. There are two paths available: show a predefined index file, or show the directory’s contents automatically. The two directives controlling this, index and autoindex, look simple but have important nuances to understand.

How Nginx Handles Directory Requests #

Before discussing the directives specifically, let’s understand the flow that happens when Nginx receives a request to a directory:

flowchart TD
    A["Request: GET /blog/\nNginx needs to determine\nthe file to return"] --> B{"Is try_files used?"}
    B -- Yes --> C["try_files $uri $uri/ =404\nNginx checks $uri as a file first\nthen $uri/ as a directory"]
    B -- No --> D["Nginx checks\nthe directory directly"]
    C --> E["URI is a directory?\nLook for an index file"]
    D --> E
    E --> F{"Does index.html exist\nin the directory?"}
    F -- Yes --> G["Internal redirect to /blog/index.html\nServe that file"]
    F -- No --> H{"Does index.php exist?"}
    H -- Yes --> I["Internal redirect to /blog/index.php\nProcess via FastCGI/PHP-FPM"]
    H -- No --> J{"Is autoindex on?"}
    J -- Yes --> K["Generate HTML listing\nof files in the directory"]
    J -- No --> L["Return 403 Forbidden\n(directory exists but can't be listed)"]

The critical point that often confuses people: 403 Forbidden, not 404, is returned when the directory exists but there’s no index file and autoindex off. This often makes developers wonder why they get a 403 when the directory clearly exists.


The index Directive #

index defines the list of files Nginx looks for when a request points to a directory. Nginx tries the files in the list in order, using the first one found.

Basic Syntax #

server {
    root /var/www/html;

    # Search order: index.html → index.htm → index.php
    index index.html index.htm;

    location / {
        try_files $uri $uri/ =404;
    }
}

When GET / comes in:

  1. Nginx checks whether /var/www/html/index.html exists → if yes, use it
  2. If not, check /var/www/html/index.htm → if yes, use it
  3. If none exist → continue to the next try_files or return an error

Valid Contexts #

index can be placed in the http, server, or location context. Inheritance rules apply: values in deeper contexts override the ones above.

http {
    # Global default — applies to all server blocks unless overridden
    index index.html;
}

server {
    server_name example.com;
    root /var/www/example.com;

    # Override at the server level — applies to all locations here
    index index.html index.htm;

    location / {
        # Inherits from the server: index.html index.htm
        try_files $uri $uri/ =404;
    }

    location /app/ {
        # Override specific to this directory
        index index.php index.html;
        try_files $uri $uri/ =404;
    }

    location /admin/ {
        # Override again — only want index.php
        index index.php;
        try_files $uri $uri/ /admin/index.php;
    }
}

Note: index Triggers an Internal Redirect #

It’s important to understand: when Nginx finds an index file, it doesn’t serve it directly. Nginx performs an internal redirect to that file’s URI, then processes that new URI from scratch (looking for a matching location block again).

Request: GET /
         ↓
Nginx: Directory! Look for an index file.
       Found: /var/www/html/index.html
         ↓
Internal redirect to: GET /index.html
         ↓
Nginx processes again: find a location block matching /index.html
         ↓
Nginx sends the file

Why does this matter? If you have a special location matching index.html, it will be processed:

server {
    root /var/www/html;
    index index.html;

    location / {
        try_files $uri $uri/ =404;
    }

    location = /index.html {
        # This WILL execute on a GET / request
        # because the index directive triggers an internal redirect to /index.html
        add_header Cache-Control "no-cache";
    }
}

The autoindex Directive #

autoindex enables directory listing — Nginx generates an HTML page showing the list of files and folders in a directory when no index file is found.

Basic Configuration #

location /downloads/ {
    root /var/www;
    autoindex on;

    # GET /downloads/ will show a listing:
    # Index of /downloads/
    # ../
    # report-2024.pdf               12-Jan-2024 10:30    2.4M
    # data-export.zip               05-Feb-2024 09:15   45.2M
    # notes.txt                     28-Feb-2024 14:22    1.2K
}

autoindex Display Options #

location /files/ {
    alias /data/shared/;
    autoindex on;

    # Time format — on: server local time, off: UTC/GMT
    autoindex_localtime on;

    # File size format:
    # on: show exact bytes (e.g., 2457600)
    # off: show human-readable units (e.g., 2.4M)
    autoindex_exact_size off;

    # Output format — html (default), xml, json, jsonp
    # json is useful for listings consumed by applications
    autoindex_format html;
}

autoindex with JSON Format #

For file servers accessed by applications (not browsers), the JSON format is more useful:

location /api/files/ {
    alias /data/shared/;
    autoindex on;
    autoindex_format json;

    # GET /api/files/ will return:
    # [
    #   {"name": "report.pdf", "type": "file", "mtime": "Tue, 12 Jan 2024 10:30:00 GMT", "size": 2457600},
    #   {"name": "data/", "type": "directory", "mtime": "Mon, 05 Feb 2024 09:00:00 GMT"}
    # ]

    add_header Content-Type application/json;
    add_header Access-Control-Allow-Origin *;
}

Appropriate Use Cases for autoindex #

autoindex is useful for specific cases, and dangerous if enabled carelessly.

Internal File Server #

# Internal file server — only accessible from the office network
server {
    listen 80;
    server_name files.internal.company.com;

    root /data/shared;

    # Restrict access to internal networks only
    allow 10.0.0.0/8;
    allow 192.168.0.0/16;
    allow 172.16.0.0/12;
    deny all;

    location / {
        autoindex on;
        autoindex_localtime on;
        autoindex_exact_size off;
    }
}

Repository Mirror #

Mirror servers for software distributions often use autoindex:

server {
    listen 80;
    server_name mirror.example.com;

    root /data/mirror;

    location / {
        autoindex on;
        autoindex_localtime on;
        autoindex_exact_size off;

        # Add a header so it can be accessed from a subdomain
        add_header Access-Control-Allow-Origin "https://example.com";
    }

    # But block access to the configuration directory
    location ~ /\.git {
        deny all;
    }
}

Development Server #

# Development server — don't use in production!
server {
    listen 8080;
    server_name localhost;

    root /home/developer/projects;

    location / {
        autoindex on;
        autoindex_localtime on;
        autoindex_exact_size off;
    }
}

Security: When autoindex Must Be Turned Off #

autoindex defaults to off — and for good reason. Exposing a file list can leak information that’s valuable to attackers:

flowchart LR
    A["autoindex on\nin a public directory"] --> B["Attacker sees\nthe file list"]
    B --> C["Finds:\n- backup-2024.sql\n- .env.backup\n- debug.log\n- private-key.pem"]
    C --> D["Download sensitive files\nor use the information\nfor further attacks"]

Directories that MUST NOT have autoindex enabled:

  • Production website root
  • Directories containing configuration files
  • User upload directories
  • Directories containing PHP files or scripts
  • Directories with backup or log files

Correct Security Configuration #

server {
    root /var/www/html;

    # Explicitly disable autoindex across the whole site
    # (default is already off, but explicit is safer)
    autoindex off;

    location / {
        try_files $uri $uri/ =404;
    }

    # Enable only in one subdirectory that actually needs it
    location /public-downloads/ {
        autoindex on;
        autoindex_exact_size off;
        autoindex_localtime on;

        # Restrict which file extensions can be seen (only allow safe files)
        # This implementation requires application-level logic or
        # using a module like ngx_http_addition_module
    }

    # Make sure sensitive directories really can't be browsed
    location /admin/ {
        autoindex off;  # Explicit, even though it's already the default
        # Add authentication
        auth_basic "Admin Area";
        auth_basic_user_file /etc/nginx/.htpasswd;
    }

    location /uploads/ {
        autoindex off;  # Always off for upload directories
        # Only allow image extensions
        location ~* \.(jpg|jpeg|png|gif|webp|svg)$ {
            expires 30d;
        }
        # Block all other file types in uploads
        location ~ {
            deny all;
        }
    }
}

What Happens When There’s No Index and autoindex Is Off #

This is one of the biggest sources of confusion for new Nginx users:

Request: GET /gallery/
Conditions:
  - /var/www/html/gallery/ exists (the directory exists)
  - /var/www/html/gallery/index.html does NOT exist
  - autoindex off (default)

Nginx returns: 403 Forbidden

403, not 404. This means: “The directory exists, but I’m not allowed to tell you what’s inside it (and there’s no default page to show).”

Common Solutions #

location /gallery/ {
    # Option 1: Create a real index file
    # (upload or generate index.html in /var/www/html/gallery/)

    # Option 2: Redirect to a specific page when the directory is accessed
    # Use try_files with a named location
    try_files $uri $uri/ @gallery_fallback;
}

location @gallery_fallback {
    return 302 /gallery.html;  # Redirect to the main gallery page
}

# Option 3: Enable autoindex if it's actually safe
location /gallery/ {
    autoindex on;
    autoindex_exact_size off;
}

# Option 4: Block directory access but allow files
location /gallery/ {
    # Allow direct access to image files
    location ~* \.(jpg|jpeg|png|gif|webp)$ {
        expires 30d;
    }
    # Block direct access to the directory
    return 403;
}

index Configuration for PHP Sites #

For PHP-based websites, the index order is often critical:

server {
    root /var/www/phpapp;

    # Prioritize index.php — processed by PHP-FPM
    # index.html as a fallback for static pages
    index index.php index.html;

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

    # Process PHP files through PHP-FPM
    location ~ \.php$ {
        fastcgi_pass unix:/var/run/php/php8.2-fpm.sock;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }

    # Don't skip this — block access to PHP files in the upload directory!
    location ~ /uploads/.*\.php$ {
        deny all;
    }
}

Combining index and autoindex Correctly #

There’s one pattern that’s often misunderstood: how index and autoindex interact:

location /docs/ {
    alias /opt/docs/;

    # If index.html exists, show that
    index index.html;

    # If index.html does NOT exist, show the listing
    autoindex on;
    autoindex_localtime on;
    autoindex_exact_size off;
}

With this configuration, the behavior is:

  1. GET /docs/ → Nginx checks whether /opt/docs/index.html exists
  2. If it exists → show index.html (autoindex is not invoked)
  3. If it doesn’t → show the directory listing (autoindex is active)

This is useful for documentation trees: folders with an index.html show an intro page, folders without one show a file list.


Debugging index and autoindex Issues #

Checking Why a 403 Appears #

# Check whether an index file exists in the accessed directory
ls -la /var/www/html/gallery/

# Check the active configuration for that path
nginx -T | grep -A 20 "location /gallery"

# Check the error log
tail -f /var/log/nginx/error.log

# 403 output usually shows one of:
# - directory index of "/var/www/html/gallery/" is forbidden → no index, autoindex off
# - "/var/www/html/gallery/" is forbidden → permission problem

Verifying autoindex Is Working #

# Request the directory — there should be an HTML listing
curl http://localhost/downloads/

# Or check in a browser — search for "Index of /downloads/"
curl http://localhost/downloads/ | grep "Index of"

Checking the Listing MIME Type #

Sometimes browsers don’t render the listing correctly because of a MIME type issue:

curl -I http://localhost/downloads/
# Content-Type: text/html; charset=utf-8  ← should be like this

Making autoindex Look More Professional #

Nginx’s built-in autoindex page is very plain — a 90s-era look with monospace text and no styling at all. Although you can’t change the HTML template directly in Nginx Community Edition, there are ways to beautify the appearance using the addition module or by injecting CSS/JS via header injection.

The Easiest Way: Style Injection via Sub-Filter #

If Nginx is compiled with the ngx_http_sub_module (active by default on most distributions), you can inject CSS into the autoindex page:

location /downloads/ {
    alias /data/downloads/;
    autoindex on;
    autoindex_localtime on;
    autoindex_exact_size off;

    # Inject CSS to beautify the display
    sub_filter '</head>' '<style>
        body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; padding: 2rem; max-width: 900px; margin: 0 auto; }
        h1 { font-size: 1.4rem; color: #334155; border-bottom: 2px solid #e2e8f0; padding-bottom: 1rem; margin-bottom: 1.5rem; }
        table { width: 100%; border-collapse: collapse; }
        td, th { padding: 0.5rem 1rem; text-align: left; }
        tr:hover { background: #f8fafc; }
        a { color: #3b82f6; text-decoration: none; }
        a:hover { text-decoration: underline; }
    </style></head>';
    sub_filter_once on;
}

Alternative: Use the Fancyindex Module #

For a truly professional look, consider the ngx_fancyindex module (needs to be compiled or available as a package):

# Ubuntu/Debian
sudo apt install libnginx-mod-http-fancyindex

# Or compile with the module (Nginx from source)
./configure --add-module=/path/to/ngx-fancyindex
location /downloads/ {
    alias /data/downloads/;

    fancyindex on;
    fancyindex_localtime on;
    fancyindex_exact_size off;
    fancyindex_header "/fancyindex-header.html";
    fancyindex_footer "/fancyindex-footer.html";
    fancyindex_name_length 255;
}

Complete Configuration: Production Internal File Server #

Here’s an internal file server configuration you can use right away for file sharing on an office network:

server {
    listen 80;
    server_name files.internal.example.com;

    root /data/shared;

    # ─── Security: internal network only ───────────────────
    # Adjust to your office network subnet
    allow 10.0.0.0/8;
    allow 172.16.0.0/12;
    allow 192.168.0.0/16;
    deny all;

    # ─── Logging ──────────────────────────────────────────────
    access_log /var/log/nginx/files-internal-access.log;
    error_log  /var/log/nginx/files-internal-error.log warn;

    # ─── Directory listing for all folders ─────────────────
    location / {
        autoindex on;
        autoindex_localtime on;
        autoindex_exact_size off;

        # Add useful headers
        add_header X-Robots-Tag "noindex, nofollow" always;
        add_header Cache-Control "no-store" always;
    }

    # ─── Transfer optimization for large files ───────────────────
    sendfile on;
    sendfile_max_chunk 1m;
    tcp_nopush on;

    # ─── Longer timeouts for large files ──────────
    # The default 60s may not be enough for large file downloads
    proxy_read_timeout    300s;
    send_timeout          300s;

    # ─── Limit upload size if any ────────────────────────
    client_max_body_size 0;  # 0 = no limit (for file servers)

    # ─── Block files that shouldn't be visible ────────────────
    location ~ /\. {
        deny all;
        log_not_found off;
    }

    location ~* \.(sh|py|rb|php|pl|exe|bat|cmd)$ {
        # Allow download but don't execute
        default_type application/octet-stream;
        add_header Content-Disposition "attachment";
    }
}

  • index defines the files searched when a request points to a directory — Nginx tries the files in the list in order. A found index file triggers an internal redirect, not direct serving.
  • autoindex on enables directory listing — useful for internal file servers or mirrors, very dangerous for production websites.
  • If there’s no index file and autoindex off: Nginx returns 403 Forbidden (not 404) — the directory exists but there’s nothing to show.
  • The autoindex_format json option is useful when the listing is consumed by an application, not a browser.
  • autoindex_exact_size off + autoindex_localtime on make the listing display more human-readable.
  • Explicitly disable autoindex at the server level, then enable it only in specific locations that actually need it — the principle of least privilege.
  • For upload directories, always add a location block blocking dangerous extensions (.php, .sh, .py) even if autoindex is off.

← Previous: Root & Alias   Next: Custom Error Page →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact