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:
| Variable | Data Type | Description |
|---|---|---|
.Name | string | The current directory name being opened (only the last folder name, not the full path). |
.Path | string | The directory’s absolute path from the web root level (e.g., /downloads/pdf/). |
.URL | string | The safe relative URL path for accessing the current directory. |
.Items | []FileInfo | An array of objects representing all files and folders in the current directory. |
.Breadcrumbs | []Crumb | An array of breadcrumbs for building folder hierarchy navigation links. |
Object Attributes Inside .Items:
#
.Name(string): The file or folder name..IsDir(bool):trueif 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
}
}
}
Adding Client-Side Search #
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
browseparameter inside thefile_serverdirective 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
hideoption in the Caddyfile prevents secret files (like.envor.git) from appearing in the page listing and blocks direct download access.- Subdirectory Isolation — Leverage
handlerouting blocks to limit the browse feature to only certain folders (like/downloads/) so the main domain stays safe.