Learning Caddy - Static File Serving
Episode 5 of 31

Learning Caddy - Static File Serving

This episode dissects static file serving: the file_server and root directives, directory browsing, index files, precompressed files, and try_files for SPA routing. You'll also learn dynamic roots with variables and common file server use cases.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

Serving static files is the most basic job of a web server — and Caddy does it exceptionally well. In episode 5, you'll learn the file_server and root directives in depth, plus advanced features like directory browsing, precompressed files, and try_files for Single Page Applications.

Static files are the foundation of many sites: HTML, CSS, JavaScript, images, and downloadable documents. Even if you focus on reverse proxying, this episode still matters — almost every site needs at least a favicon, robots.txt, or static assets.

After this episode, you'll be able to build production-ready static sites, documentation, or portfolios with a short Caddyfile.

The file_server Directive

Basic Usage

The file_server directive serves files from the document root. Simplest form:

Basic static site
example.com {
    root * /var/www/mysite
    file_server
}

The default index files are index.html (followed by index.txt, and others). So a request to / returns /var/www/mysite/index.html if it exists.

Directory Browsing and Index Files

If you want visitors to be able to browse directories:

Directory browsing
files.example.com {
    root * /srv/downloads
    file_server browse
}

With the browse keyword, Caddy shows a file listing when no index file exists. To control the index filenames:

Custom index files
example.com {
    root * /var/www
    file_server {
        index index.html home.html
    }
}

The file_server block above declares: look for index.html first, then home.html as a fallback. This is useful for non-standard entry pages.

Hidden Files

By default, Caddy refuses to serve files that start with a dot, like .env or .git. This is a wise security decision. Don't disable it unless you fully understand the risk.

Advanced File Serving

Precompressed Files

Caddy can serve pre-compressed files (.gz, .br, .zst) if they sit next to the original file:

Enable precompressed files
example.com {
    encode zstd gzip
    root * /var/www
    file_server {
        precompressed br gzip
    }
}

If /var/www/style.css.br exists, Caddy automatically sends it to browsers that support Brotli — reducing CPU load because compression isn't recomputed per request.

Range Requests and ETags

Caddy supports range requests (great for resuming video downloads) and ETags (cache validation) by default. No extra configuration needed. Just make sure file_server is active.

Info

For large files like videos, range requests let clients download part of a file and resume from the break point. Caddy handles this automatically as long as file_server is active.

The try_files Directive

Fallback to Another File

try_files checks for file existence and picks the first one that exists:

try_files with a fallback
example.com {
    root * /var/www
    try_files {path} {path}.html /index.html
    file_server
}

Meaning: try the exact path file, then the version with a .html extension, then fall back to /index.html. try_files {path} /index.html is the most common pattern for SPA routing.

SPA Routing

React, Vue, and Angular applications need every route to fall back to index.html so the JavaScript router handles it:

SPA routing
spa.example.com {
    root * /app/dist
    try_files {path} /index.html
    file_server
}

With this pattern, a request for /dashboard, whose file doesn't exist, returns index.html, and the SPA router displays the right page. This is the pattern used in nearly every modern frontend deployment.

The root Directive

Setting the Document Root

root determines the directory where files are looked up:

Root per location
example.com {
    root * /var/www/public
    root /api /var/www/api-docs
    file_server
}

The first line, root * /var/www/public, applies to all paths; the second line overrides the root specifically for the /api path. So API docs stored in a separate directory can be served without moving files.

Dynamic Root with Variables

The document root can be dynamic using request placeholders:

Dynamic root per subdomain
*.example.com {
    root * /var/www/sites/{host}
    file_server
}

Combining root * /var/www/sites/{host} with the {host} placeholder serves each subdomain from a different directory — an efficient pattern for static multi-tenancy. We'll go deeper into variables in episode 12.

File Server Use Cases

When to Use These Patterns

  • Static website: HTML, CSS, JS with root and file_server.
  • Single Page Application: add try_files {path} /index.html.
  • File downloads: enable file_server browse.
  • Documentation site: build to a static folder, then serve with Caddy.
  • Portfolio: a two-line configuration is enough.

Conclusion

Episode 5 made you proficient in static file serving: file_server with the browse, index, and precompressed options; try_files for SPA routing and fallbacks; and root with dynamic variables for per-location or per-host configuration.

Key takeaways:

  • file_server + root is the core combination for static files.
  • The browse keyword shows a directory listing.
  • Precompressed files save CPU for large assets.
  • try_files {path} /index.html is the standard SPA routing pattern.
  • root can be set per location and is dynamic.
  • Hidden files are rejected by default for security.

In the next episode, episode 6, we move into reverse proxy basics — the reverse_proxy directive, header manipulation with header_up and header_down, proxying to localhost and containers, WebSocket and gRPC proxying, and transport options like h2c and timeouts. This is the gateway into the world of dynamic applications.

Learning Caddy - Static File Serving | Learning Caddy