Caddy server recipes

These recipes are for the Caddy web server version 2.

Create a Caddyfile consisting of the following single line:

import Caddyfile.d/*.conf

Create the directory Caddyfile.d/ next to the Caddyfile. Put your site configurations in Caddyfiles with the extension .conf in Caddyfile.d/. Note that with this setup, one broken Caddyfile will prevent your server from starting.

Use the respond or file_server directive in the error handler.

handle_errors {
	file_server {
		status 200
	}

    rewrite * /templates/index.html
}

This will only work with a single query parameter. The example is something useful for the Fossil SCM wiki. It prevents search engines and users from accessing your pages as both /wiki?name=foo and /wiki/foo. (The latter seems more search engine-friendly.)

@wiki_query {
	path_regexp ^/wiki(?:/*|)$
	query name=*
}

route @wiki_query {
	uri replace name= /
	redir * /wiki{query}
}

root * /opt/foo/static

@not_static {
	not {
		path /BingSiteAuth.xml /robots.txt
		path /favicon.ico
	}
}

reverse_proxy @not_static localhost:8100
file_server

@foo path /foo /foo/*

reverse_proxy @foo {
	to localhost:9000
	transport fastcgi
}

You can use the unofficial third-party scgi-transport module.

@foo path /foo /foo/*

reverse_proxy @foo {
	to localhost:9999
	transport scgi
}

See the Glowing Bear wiki page “Proxying WeeChat relay with a web server”.

weechat.example.com {
	reverse_proxy /weechat localhost:9001
}

Caddy can run CGI scripts either with a plugin or with a FastCGI-to-CGI proxy. See my caddy-cgi repository.

You can serve Markdown files as adequately good-looking minimal web pages. See my caddy-markdown-site repository.

This will give your server a human-readable, if not the most user-friendly, error page. It will tell the user an error has occurred and give them something to report to you. The page will contain a custom error message for the most common errors: 403, 404, and 500. Otherwise, the error message will be the description text of the HTTP status code.

handle_errors {
	rewrite * /error.html

	file_server
	templates
}

<!DOCTYPE html>
{{- $code := placeholder "http.error.status_code" -}}
{{- $text := placeholder "http.error.status_text" }}
<html lang="en">

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">

    <title>{{ $code }} {{ $text }}</title>

    <link rel="icon" href="/favicon.ico" type="image/x-icon">
</head>

<body>
    <h1>Error {{ $code }}</h1>

    {{ if eq $code "403" -}}
    <p>You don't have permission to access this resource.</p>
    {{- else if eq $code "404" -}}
    <p>The requested URL was not found on this server.</p>
    {{- else if eq $code "500" -}}
    <p>An internal server error has occurred.</p>
    {{- else -}}
    <p>{{ $text }}.</p>
    {{- end}}

    <ul>
        <li><a href="/">Home</a></li>
        <li><a href="#" onclick="javascript:history.back(); return false;">Back</a></li>
        <li>{{- /* The `href` does not preserve the fragment identifier; only the JavaScript does. */ -}}<a href="{{ .OriginalReq.URL }}" onclick="javascript:window.location.reload(); return false;">Reload</a></li>
    </ul>
</body>

</html>

This website has a dynamic license page feature inspired by mit-license.org. Pages like /mit-license/2025 are generated from a template. This allows you to link to a copy of your chosen software license with specified copyright years while preventing arbitrary text injection. This is useful because you can add a free license to a short code snippet or a script, and it only takes up one line.

The following shows how I have set this up with Caddy templates.

First, the Caddyfile. We match the paths /mit-license and /mit-license/license.txt with an optional year or range of years.

Caddy only evaluates files with a certain Content-Type as templates. You can choose the particular types (text/html and text/plain by default), but Caddy must detect one. On FreeBSD, Caddy doesn’t detect license.txt as text/plain by default. On a typical Debian or Ubuntu installation, it works as expected. See “Content-Type on FreeBSD”.

@mit_license {
	path_regexp license ^/mit-license(?:|/|/(?<years>\d{4}(?:-\d{4})?(?:,\d{4}(?:-\d{4})?){0,100})/?)$
}

handle @mit_license {
	rewrite * /mit-license.html

	file_server
	templates
}

@mit_license_txt {
	path_regexp license ^/mit-license/(?:(?<years>\d{4}(?:-\d{4})?(?:,\d{4}(?:-\d{4})?){0,100})/)?license.txt$
}

handle @mit_license_txt {
	rewrite * /mit-license/license.txt

	file_server
	templates
}

handle * {
	file_server
	try_files {path} {path}.html
}

This is the template part of the license webpage. The fallback year is based on server time. The page links to license.txt for the current range of years.

{{- $years := or (placeholder "http.regexp.license.years") (now | date "2006") -}}

<p><a href="/mit-license/{{ $years | replace "," ", " }}/license.txt">Download</a>.</p>
<pre class="text"><code>Copyright (c) {{ $years }} J. Random Hacker

Permission is hereby granted, free of charge, to any person obtaining a copy
[...]
</code></pre>

This is the downloadable plain-text license file. The fallback year is based on server time, as above.

Copyright (c) {{ or (placeholder "http.regexp.license.years" | replace "," ", ") (now | date "2006") }} J. Random Hacker

Permission is hereby granted, free of charge, to any person obtaining a copy
[...]

When working on dynamic license pages, I found that Caddy on FreeBSD evaluated HTML files as templates as documented but didn’t evaluate plain-text files. I traced this to the files not having a Content-Type: text/plain or any Content-Type header like they had on Linux.

This solution is an alternative to setting the Content-Type header manually. You need to create /usr/local/etc/mime.types on your system. You can download the latest version of the file from Apache’s GitHub mirror:

# As root:
fetch -o /usr/local/etc/mime.types https://raw.githubusercontent.com/apache/httpd/master/docs/conf/mime.types

Alternatively, ports like Apache 2.4 and Python provide the file. Copy or symlink one of the following files to /usr/local/etc/mime.types if you have them installed:

  • /usr/local/etc/apache24/mime.types
  • /usr/local/lib/python3.11/test/mime.types (older version than in Apache; lacks image/avif in Python 3.11)