How to use Docker secrets with Caddy

When running services in Docker, storing secrets (like API keys) directly in environment variables can expose them when inspecting the container. A safer approach is to use Docker Secrets, which keeps sensitive values outside of your runtime environment.

This guide shows how to integrate Docker Secrets with Caddy, making secrets available as environment variables inside the container without exposing them.


Define Your docker-compose.yml

We’ll configure a caddy service that builds from a custom Dockerfile and mounts a secret called api_key.

name: caddy

services:
  caddy:
    build: .
    container_name: caddy
    restart: unless-stopped
    cap_add:
      - NET_ADMIN
    secrets:
      - api_key
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - data:/data
      - config:/config
    networks:
      - caddy

volumes:
  data:
  config:

networks:
  caddy:
    name: caddy
    external: true

secrets:
  api_key:
    environment: API_KEY

This declares the api_key secret, which we’ll inject into the container. It content comes from the host environment variable API_KEY.

Create a Custom Dockerfile

Caddy itself doesn’t automatically load secrets from /run/secrets/, so we need a small helper script to export them as environment variables.

FROM caddy:2-alpine

COPY ./export_secrets /usr/sbin/export_secrets
RUN chmod +x /usr/sbin/export_secrets

ENTRYPOINT ["/usr/sbin/export_secrets"]
CMD ["caddy"]

In our Dockerfile we copy the script, make it executable, and set it as the container entrypoint.

Write the export_secrets Script

This script loops over all files in /run/secrets/, converts their filenames to uppercase, and exports them as environment variables before launching Caddy.

#!/bin/sh
set -e

# Export all secrets in /run/secrets as uppercase env vars
for secret_file in /run/secrets/*; do
  if [ -f "$secret_file" ]; then
    secret_name=$(basename "$secret_file")
    env_var_name=$(echo "$secret_name" | tr '[:lower:]' '[:upper:]')
    export "$env_var_name"="$(cat "$secret_file")"
  fi
done

# Execute the container’s main process
exec "$@"

For example, a file named /run/secrets/api_key becomes the environment variable API_KEY.

Use the Secret in Caddyfile

Now you can reference the secret as an environment variable in your Caddyfile:

{
  ...
  some_module {
    api_key {env.API_KEY}
  }
  ...
}

Run the Container

Start the container by providing the secret, add a space before the secret to not safe this command into your bash/zsh history:

$  API_KEY='super_secret!' docker compose up -d

Or use any Secret Manager with a CLI to get the secret.


Of course, this would be even nicer if implemented as a Caddy module, but this is out of scope right now.

1 Like

Thank you! I’ve move your post to the Wiki category.

1 Like

Can you help me understand please? If you’re converting secrets into the environment variables, how does that differ from just using env_file?

services:
  caddy:
    env_file: "caddy.env"
...
1 Like

By the way, you don’t need to parse the secrets file and load them into env vars. You can use the placeholder {file./path/to/secret} directly.

4 Likes

Sorry, answering my own question after a quick test.

docker-compose.yaml:

services:
  caddy_build:
    build: .
    container_name: caddy_build
    env_file: ./caddy_env_file
    secrets:
      - caddy_secret
    restart: unless-stopped

secrets:
  caddy_secret:
    file: ./caddy_secret_file

Dockerfile:

FROM caddy:2-alpine

COPY ./export_secrets /usr/sbin/export_secrets
RUN chmod +x /usr/sbin/export_secrets

ENTRYPOINT ["/usr/sbin/export_secrets"]
CMD [ "caddy", "run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile" ]

caddy_env_file:

CADDY_ENV=test_value

caddy_secret_file:

super_secret!
$ docker inspect caddy_build | jq '.[] | .Config.Env'
[
  "CADDY_ENV=test_value",
  "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
  "CADDY_VERSION=v2.10.2",
  "XDG_CONFIG_HOME=/config",
  "XDG_DATA_HOME=/data"
]

$ docker exec -ti caddy_build printenv
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOSTNAME=69c78f1d017f
TERM=xterm
CADDY_ENV=test_value
CADDY_VERSION=v2.10.2
XDG_CONFIG_HOME=/config
XDG_DATA_HOME=/data
HOME=/root

$ docker exec -ti caddy_build strings /proc/1/environ
CADDY_ENV=test_value
HOSTNAME=69c78f1d017f
SHLVL=1
HOME=/root
CADDY_VERSION=v2.10.2
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
XDG_CONFIG_HOME=/config
XDG_DATA_HOME=/data
CADDY_SECRET=super_secret!
PWD=/srv

So, while env_file shows up directly in .Config.Env, the environment variables created from secrets need a bit more digging to find. That’s the difference.

My question was kind of silly, but at least it gave me an excuse to poke around a bit :slight_smile:

this is the official and better approach.

Thank you I wasn’t aware of this :+1: