Caddy doesn’t actually interpolate environment variables directly into its config files, which is a good thing for security and predictability.
Let’s say you’re running Caddy and want to dynamically configure it, maybe to point to a backend service whose address isn’t known until runtime. You might be tempted to do something like this in your Caddyfile:
:80 {
reverse_proxy {$BACKEND_HOST}:8080
}
This looks like it should work, right? You set BACKEND_HOST=my-app.internal in your environment, start Caddy, and expect it to proxy to my-app.internal:8080.
But Caddy’s Caddyfile parser is designed to be explicit. It doesn’t perform shell-style variable expansion within the config itself. If you try the above, Caddy will likely error out, complaining about an invalid placeholder like {$BACKEND_HOST}.
The safe and intended way to achieve this is through Caddy’s exec template function. This function allows Caddy to execute an external command (like env or printenv) and use its output within the configuration.
Here’s how you’d do it correctly:
:80 {
reverse_proxy {{$BACKEND_HOST := exec "printenv BACKEND_HOST"}}{{$BACKEND_HOST}}:8080
}
Let’s break this down.
{{$BACKEND_HOST := exec "printenv BACKEND_HOST"}} is the key.
execis the template function that runs a command."printenv BACKEND_HOST"is the command being executed. It specifically asks theprintenvutility to output the value of theBACKEND_HOSTenvironment variable.$BACKEND_HOST := ...assigns the output of that command to a template variable named$BACKEND_HOST.
Now, when Caddy loads this configuration, it first runs printenv BACKEND_HOST. If your environment variable is set to my-app.internal, the output will be my-app.internal. This output is then substituted into the reverse_proxy directive, resulting in:
:80 {
reverse_proxy my-app.internal:8080
}
This is a crucial distinction. Caddy isn’t directly reading your shell’s environment at parse time; it’s executing a command and using its stdout. This is safer because it limits what Caddy can access. It can only grab what the specified command is designed to expose.
Consider another common use case: setting an upstream service name.
:443 {
tls your@email.com
reverse_proxy {{$UPSTREAM_SERVICE_NAME := exec "printenv UPSTREAM_SERVICE_NAME"}}:80
}
If UPSTREAM_SERVICE_NAME=api-gateway, Caddy effectively becomes:
:443 {
tls your@email.com
reverse_proxy api-gateway:80
}
You can also chain these exec calls or use them for more complex logic, though it quickly becomes unreadable. For instance, to get a port number from an environment variable:
:8080 {
reverse_proxy localhost:{{ exec "printenv APP_PORT" }}
}
If APP_PORT=9000, Caddy will configure to localhost:9000.
The exec function is powerful, but it also means that Caddy’s behavior is dependent on the environment at the time Caddy starts. If the environment variable changes after Caddy has started, the configuration will not update automatically. You’ll need to restart Caddy for the exec command to be re-run with the new environment value.
This mechanism is also how Caddy handles dynamic TLS certificate issuers based on environment variables. For example, you might configure a DNS provider like Cloudflare:
:443 {
tls your@email.com {
dns cloudflare {env CLOUDFLARE_API_TOKEN}
}
}
Here, {env CLOUDFLARE_API_TOKEN} is a shorthand template function that is equivalent to {{ exec "printenv CLOUDFLARE_API_TOKEN" }}. Caddy will execute printenv CLOUDFLARE_API_TOKEN and use its output as the API token for Cloudflare.
The primary danger here isn’t Caddy itself, but how you expose sensitive information. If you’re running Caddy in a container and passing environment variables, ensure those variables are managed securely. For instance, using Kubernetes Secrets or Docker Secrets is far better than hardcoding tokens directly into your Caddyfile or exposing them broadly in your container’s environment. The exec function will happily print out anything printenv gives it, including potentially sensitive tokens if they are set in the environment where Caddy runs.
The next thing people often run into is needing to use the same environment variable in multiple places within a Caddyfile and wanting to avoid repeating the exec call.