Directory Browse #

Directory Browsing is a web server feature that dynamically serves a list of file and subdirectory names to visitors when they access a URL path that’s a folder without an index file (like index.html or index.php). By default, Caddy returns an HTTP 404 Not Found response for folders without an index file for security reasons. This behavior is appropriate for protecting your application’s data structure from unauthorized scanning. However, for specific use cases like file sharing servers, public download repositories, or team documentation portals, enabling directory listing becomes essential.

In Caddy, you can explicitly enable this feature using the browse option on the file_server directive. Besides providing a clean, functional built-in layout, Caddy fully supports custom HTML templates using Go template syntax (text/template). This lets you design an interactive, premium-looking file browsing interface tailored to your own design identity.


Enabling Directory Browse #

To enable directory listing in the Caddyfile, just add the browse parameter inside the file_server directive’s configuration block:

# Enable basic directory listing
files.example.com {
    root * /var/www/shared-files
    
    file_server {
        # Allow visitors to browse folder contents
        browse
    }
}

With the configuration above, if the /var/www/shared-files/ folder has no index.html file inside, visitors accessing https://files.example.com/ won’t get a 404 error. Instead, they’re served an HTML page listing the files and folders in that directory.


The Default Layout #

Caddy provides a built-in default template compiled directly into its binary. The layout is very clean, responsive, mobile-friendly, and supports dark mode automatically based on the visitor’s OS preference.

Visualization of Caddy's Standard Directory Listing Layout:

  ┌─────────────────────────────────────────────────────┐
  │  Index of /downloads/                               │
  ├─────────────────────────────────────────────────────┤
  │  📁 important_docs/   -           2026-06-16 10:00 │
  │  📁 photo_gallery/    -           2026-06-15 09:30 │
  │  📄 README.md          2.4 KB      2026-06-14 14:25 │
  │  📄 guide.pdf          8.7 MB      2026-06-12 16:40 │
  │  📄 data_archive.zip  45.1 MB      2026-06-11 11:15 │
  └─────────────────────────────────────────────────────┘

Features of the default layout include:

  • Up Navigation: A link to return to the parent directory.
  • Dynamic Sorting: Visitors can click column headers to sort files by Name, Size, or Modification Date.
  • Human-Readable Sizes: Automatic file size conversion to an easy-to-read format (KB, MB, GB).

Building Custom Templates with Go Templates #

If you want to completely change the look of Caddy’s directory listing to feel premium and blend with your company’s web design, you can create a custom HTML template file and register it on the file_server directive:

# Using a custom template for directory browse
files.example.com {
    root * /var/www/files
    
    file_server {
        # Point to the location of your custom HTML template file
        browse /etc/caddy/templates/modern-browse.html
    }
}

Here’s a complete HTML code example for a modern custom template (modern-browse.html) that’s responsive, has built-in CSS with an elegant color scheme, and smooth visual interactions:

<!-- /etc/caddy/templates/modern-browse.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Index of {{.Name}} — File Portal</title>
    <style>
        :root {
            --bg-color: #f8fafc;
            --text-color: #0f172a;
            --card-bg: #ffffff;
            --border-color: #e2e8f0;
            --accent-color: #4f46e5;
            --accent-hover: #4338ca;
            --text-muted: #64748b;
        }
        @media (prefers-color-scheme: dark) {
            :root {
                --bg-color: #0f172a;
                --text-color: #f8fafc;
                --card-bg: #1e293b;
                --border-color: #334155;
                --accent-color: #6366f1;
                --accent-hover: #4f46e5;
                --text-muted: #94a3b8;
            }
        }
        * { box-sizing: border-box; margin: 0; padding: 0; }
        body {
            font-family: system-ui, -apple-system, sans-serif;
            background-color: var(--bg-color);
            color: var(--text-color);
            line-height: 1.5;
            padding: 2rem 1rem;
        }
        .container {
            max-width: 1024px;
            margin: 0 auto;
        }
        header {
            margin-bottom: 2rem;
        }
        h1 {
            font-size: 1.75rem;
            font-weight: 700;
            margin-bottom: 0.5rem;
        }
        .breadcrumbs {
            font-size: 0.875rem;
            color: var(--text-muted);
        }
        .breadcrumbs a {
            color: var(--accent-color);
            text-decoration: none;
        }
        .breadcrumbs a:hover { text-decoration: underline; }
        .card {
            background-color: var(--card-bg);
            border: 1px solid var(--border-color);
            border-radius: 12px;
            box-shadow: 0 4px 6px -1px rgba(0,0,0,0.05);
            overflow: hidden;
        }
        table {
            width: 100%;
            border-collapse: collapse;
            text-align: left;
        }
        th, td {
            padding: 1rem;
            border-bottom: 1px solid var(--border-color);
        }
        th {
            background-color: rgba(0,0,0,0.02);
            font-weight: 600;
            font-size: 0.875rem;
            text-transform: uppercase;
            letter-spacing: 0.05em;
            color: var(--text-muted);
        }
        tr:last-child td { border-bottom: none; }
        tr:hover td {
            background-color: rgba(79, 70, 229, 0.03);
        }
        a {
            color: var(--text-color);
            text-decoration: none;
            font-weight: 500;
        }
        a:hover {
            color: var(--accent-color);
        }
        .icon {
            margin-right: 0.75rem;
            display: inline-block;
            font-size: 1.1rem;
        }
        .size, .time {
            font-size: 0.875rem;
            color: var(--text-muted);
        }
        .back-link {
            font-style: italic;
            color: var(--accent-color);
        }
    </style>
</head>
<body>
<div class="container">
    <header>
        <h1>📁 Index of {{.Name}}</h1>
        <div class="breadcrumbs">
            <a href="/">Root</a> /
            {{range .Breadcrumbs}}
                <a href="{{.Link}}">{{.Text}}</a> /
            {{end}}
        </div>
    </header>
    
    <div class="card">
        <table>
            <thead>
                <tr>
                    <th>File Name</th>
                    <th>Size</th>
                    <th>Last Modified</th>
                </tr>
            </thead>
            <tbody>
                <!-- Link Back to the Parent Directory -->
                {{if ne .Path "/"}}
                <tr>
                    <td colspan="3">
                        <span class="icon">⬆</span>
                        <a href="../" class="back-link">Back to the previous folder</a>
                    </td>
                </tr>
                {{end}}
                
                <!-- Display the Folder List First -->
                {{range .Items}}
                {{if .IsDir}}
                <tr>
                    <td>
                        <span class="icon">📁</span>
                        <a href="{{.URL}}">{{.Name}}/</a>
                    </td>
                    <td class="size">—</td>
                    <td class="time">{{.ModTime.Format "02 Jan 2006, 15:04"}}</td>
                </tr>
                {{end}}
                {{end}}
                
                <!-- Display the File List -->
                {{range .Items}}
                {{if not .IsDir}}
                <tr>
                    <td>
                        <span class="icon">📄</span>
                        <a href="{{.URL}}">{{.Name}}</a>
                    </td>
                    <td class="size">{{.HumanSize}}</td>
                    <td class="time">{{.ModTime.Format "02 Jan 2006, 15:04"}}</td>
                </tr>
                {{end}}
                {{end}}
            </tbody>
        </table>
    </div>
</div>
</body>
</html>

Template Data Objects and Variables #

When writing Go template code, you have access to the structured data object sent by Caddy. Here’s a list of the main variables you can use:

VariableData TypeDescription
.NamestringThe current directory name being opened (only the last folder name, not the full path).
.PathstringThe directory’s absolute path from the web root level (e.g., /downloads/pdf/).
.URLstringThe safe relative URL path for accessing the current directory.
.Items[]FileInfoAn array of objects representing all files and folders in the current directory.
.Breadcrumbs[]CrumbAn array of breadcrumbs for building folder hierarchy navigation links.

Object Attributes Inside .Items: #

  • .Name (string): The file or folder name.
  • .IsDir (bool): true if the item is a directory.
  • .Size (int64): The file size in bytes.
  • .HumanSize (string): The formatted file size (e.g., "12.4 MB").
  • .ModTime (time.Time): The file’s last modification time.
  • .URL (string): The escaped URL for downloading the file.

Securing Directory Browse (Layered Defense) #

Enabling directory listing without protection on a production server is a major security risk. Attackers can map your entire asset structure, look for backup files (.bak), or steal sensitive data. You must apply the following security methods:

1. Basic Authentication (Password Protection) #

Restrict directory listing access to only users with login accounts. You must hash passwords using the bcrypt algorithm:

files.example.com {
    root * /var/www/html
    
    # Set basicauth authentication for the entire site
    basicauth {
        # Username: admin, Password: admin_secret_password (Bcrypt Hash)
        admin $2a$14$V0Z7iE8xYp6b1N2w3h4m5uKjLqMrNsOtPuQvRwSxTyUzVwWxXyYz.
    }
    
    file_server {
        browse
    }
}

[!TIP] You can generate a bcrypt password hash using Caddy’s built-in CLI command: caddy hash-password --plaintext "our_password"

2. Restricting Access to Only Internal IPs (CIDR Whitelisting) #

If directory browse is for internal office needs, you must block all connections from external public IPs:

share.company.internal {
    root * /var/www/internal-files
    
    # 1. Define a Matcher for External IPs
    # (Only allow RFC 1918 local IPs)
    @external {
        not remote_ip 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16
    }
    
    # 2. Block with HTTP 403 if accessed from outside the office
    respond @external "Access denied: You must be connected to the office VPN!" 403
    
    file_server {
        browse
    }
}

Selectively Hiding Files #

Caddy provides the hide option to prevent certain files from appearing in the browse listing while also absolutely blocking direct download access to those files:

files.example.com {
    root * /var/www/html
    
    file_server {
        browse
        
        # Hide files from the listing and block downloads
        hide {
            .git          # Git repository
            .env          # Secret credentials
            .htaccess     # Old apache configuration
            *.secret      # Files with a special extension
            .DS_Store     # macOS metadata file
            Thumbs.db     # Windows thumbnail cache
        }
    }
}

In directories containing hundreds of files, finding one file manually is very difficult. You can add a dynamic JavaScript-based search feature to your custom HTML template:

<!-- Add a search input above the table in your custom HTML template -->
<input type="search" id="fileSearch" onkeyup="searchFiles()" placeholder="Search files here...">

<script>
function searchFiles() {
    // 1. Get the search input value
    let input = document.getElementById('fileSearch');
    let filter = input.value.toLowerCase();
    
    // 2. Get all file data rows (add the class "file-row" to tr in the template)
    let rows = document.querySelectorAll('tbody tr.file-row');
    
    // 3. Filter rows by file name
    rows.forEach(row => {
        let fileName = row.getAttribute('data-name').toLowerCase();
        if (fileName.includes(filter)) {
            row.style.display = ""; // Show
        } else {
            row.style.display = "none"; // Hide
        }
    });
}
</script>

Limiting the Browse Feature to Specific Subdirectories #

Often you want to serve a normal marketing site on the front page but enable directory browse only on the /downloads/* folder. You can limit this scope using the handle block in the Caddyfile:

example.com {
    root * /var/www/html
    
    # 1. Enable directory browse ONLY for the /downloads/ folder
    handle /downloads/* {
        file_server {
            browse
        }
    }
    
    # 2. The main site runs without directory browse (returns 404 if the index is empty)
    handle {
        file_server
    }
}

Summary #

  • Default Status — The Directory Browse feature is disabled by default in Caddy for security reasons.
  • Explicit Activation — You enable it deliberately using the browse parameter inside the file_server directive configuration.
  • Custom Design — Caddy supports external HTML files using the Go Templates system to completely transform the directory interface into a premium look.
  • Layered Security — Protect public directories in production using a combination of basicauth (encrypted password) and network IP restrictions (remote_ip).
  • Hide Option — The hide option in the Caddyfile prevents secret files (like .env or .git) from appearing in the page listing and blocks direct download access.
  • Subdirectory Isolation — Leverage handle routing blocks to limit the browse feature to only certain folders (like /downloads/) so the main domain stays safe.

← Previous: File Server   Next: Error Pages →

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