1. The problem I’m having:
I’m building a caddy server (first time) to run behind cloudflare using lets encrypt.
I’m building a custom version of caddy in a docker file (launched using docker compose) using xcaddy (see below).
We are also using cloudflare’s dns.
My problem is that I’m looking to add a trusted proxies section for the cloud flare servers.
I found this module:
Contribute to WeidiDeng/caddy-cloudflare-ip development by creating an account on GitHub.
but I’m getting an error saying trusted_proxies is an unknown directive.
2. Error messages and/or full log output:
2025-04-02T03:37:32.984661000Z {"level":"info","ts":1743565052.9842877,"msg":"using config from file","file":"/etc/caddy/Caddyfile"}
2025-04-02T03:37:32.985783000Z Error: adapting config using caddyfile: /etc/caddy/Caddyfile:95: unrecognized directive: trusted_proxies
3. Caddy version:
2.9.1
4. How I installed and ran Caddy:
Using docker build (see below) with docker-compose.
a. System environment:
ubuntu 24.10
docker --version
Docker version 28.0.1, build 068a01e
b. Command:
PASTE OVER THIS, BETWEEN THE ``` LINES.
Please use the preview pane to ensure it looks nice.
c. Service/unit/compose file:
# we build our own caddy file as we need the cloudflare module.
FROM caddy:2.9.1-builder AS builder
# https://caddyserver.com/docs/modules/dns.providers.cloudflare
RUN xcaddy build \
--with github.com/caddy-dns/cloudflare \
--with github.com/WeidiDeng/caddy-cloudflare-ip
FROM caddy:2.9.1
COPY --from=builder /usr/bin/caddy /usr/bin/caddy
docker-compose.yaml
volumes:
caddy_data:
caddy_config:
services:
caddy:
container_name: caddy
image: onepub/onepub-caddy:${ONEPUB_VERSION}
restart: always
network_mode: "host"
cap_add:
- NET_ADMIN
environment:
ACME_AGREE: "true"
ACME_URL: ${ACME_URL} # staging or production
EMAIL: ${AUTH_PROVIDER_EMAIL_ADDRESS}
CLOUDFLARE_API_TOKEN: ${AUTH_PROVIDER_TOKEN}
DEBUG: "false"
volumes:
# Persist certificates
- caddy_data:/data
- caddy_config:/config
# Mount your Caddyfile (this should contain your complete caddy config)
- /opt/onepub/config/caddy:/etc/caddy
# JSON error files
# - /opt/onepub/config/caddy/json:/etc/caddy/json
# Logs
- /tmp/caddy:/var/log/caddy
logging:
driver: "journald"
d. My complete Caddy config:
{
# Email for Let's Encrypt notifications
email {$EMAIL}
# ACME CA URL allows us to flip between production and staging.
# we default to staging.
acme_ca ${ACME_URL:https://acme-staging-v02.api.letsencrypt.org/directory}
log {
output file /var/log/caddy/access.log
}
}
# Main Domain Configuration
*.onepub.dev, onepub.dev {
tls {
# API Token required for Wild card certs
dns cloudflare {env.CLOUDFLARE_API_TOKEN}
}
# Remove duplicate slashes from the URI
# (e.g., /path//to///resource) but avoids the query string
@multiple_slashes path_regexp multipleSlashes ^(.*)//+(.*)$
redir @multiple_slashes {scheme}://{host}{re.multipleSlashes.1}/{re.multipleSlashes.2}{query} permanent
# Block common AI crawler User-Agents
@block_ai header_regexp User-Agent "(?i)(GPTBot|ChatGPT|Google-Extended|Claude-Web|Anthropic|Amazonbot|FacebookBot|cohere-ai|Bytespider|YouBot)"
respond @block_ai 403
# Block all PHP file requests, returning no response (similar to nginx 444)
@block_php path *.php
abort @block_php
# Redirect all .mp4 requests permanently (HTTP 301) to video subdomain
@mp4_redirect path *.mp4
redir @mp4_redirect https://video.{host}{uri} permanent
handle /mailhog/* {
basic_auth {
bsutton XXXX
}
reverse_proxy 127.0.0.1:8025
}
handle /api/* {
reverse_proxy 127.0.0.1:8080 {
header_up Host {host}
header_up X-Real-IP {remote_host}
transport http {
read_timeout 300s
}
# Required for WebSocket support
header_up Connection {header.Connection}
header_up Upgrade {header.Upgrade}
}
}
# Global error handling, applied only for API endpoints
handle_errors {
@api path /api/*
handle @api {
root * /etc/caddy/json
rewrite 502 /500.json
rewrite 404 /404.json
file_server
}
}
reverse_proxy 127.0.0.1:8080 {
header_up Host {host}
header_up X-Real-IP {remote_host}
transport http {
read_timeout 300s
}
# Required for WebSocket support
header_up Connection {header.Connection}
header_up Upgrade {header.Upgrade}
}
trusted_proxies cloudflare {
interval 12h
timeout 15s
}
encode gzip
}
# Combined Video Domains Configuration
# we serve video on the video subdomain, because it would violate
# cloudflare TOS to proxy video through cloudflare
video.onepub.dev, video.beta.onepub.dev {
tls {
# API Token required for Wild card certs
dns cloudflare {env.CLOUDFLARE_API_TOKEN}
}
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Frame-Options "SAMEORIGIN"
X-Content-Type-Options "nosniff"
}
handle *.mp4 {
reverse_proxy 127.0.0.1:8080 {
header_up Host {host}
header_up X-Real-IP {remote_host}
header_up X-Forwarded-Proto https
transport http {
read_timeout 300s
}
# Web sc
header_up Connection {header.Connection}
header_up Upgrade {header.Upgrade}
}
}
# Domain-based redirection (fallback)
@prod host video.onepub.dev
redir @prod https://onepub.dev{uri} permanent
redir https://beta.onepub.dev{uri} permanent
encode gzip
}
5. Links to relevant resources:
thanks for the help.
However, I’m not certain what is going on.
I can see the build steps running (see below).
And I know that its publishing this build as the timestamps on docker hub line up.
Is the problem with the copy command?
Do I need to copy the resulting build from a different location?
# we build our own caddy file as we need the cloudflare module.
FROM caddy:2.9.1-builder AS builder
# https://caddyserver.com/docs/modules/dns.providers.cloudflare
RUN xcaddy build \
--with github.com/caddy-dns/cloudflare \
--with github.com/WeidiDeng/caddy-cloudflare-ip
FROM caddy:2.9.1
COPY --from=builder /usr/bin/caddy /usr/bin/caddy
CMD ["caddy" "run" "--config" "/etc/caddy/Caddyfile" "--adapter" "caddyfile"]
#0 building with "default" instance using docker driver
#1 [internal] load build definition from caddy.dockerfile
#1 transferring dockerfile: 1.01kB done
#1 DONE 0.0s
#2 [internal] load metadata for docker.io/library/caddy:2.9.1-builder
#2 ...
#3 [auth] library/caddy:pull token for registry-1.docker.io
#3 DONE 0.0s
#4 [internal] load metadata for docker.io/library/caddy:2.9.1
#4 ...
#2 [internal] load metadata for docker.io/library/caddy:2.9.1-builder
#2 DONE 1.7s
#4 [internal] load metadata for docker.io/library/caddy:2.9.1
#4 DONE 1.7s
#5 [internal] load .dockerignore
#5 transferring context: 2B done
#5 DONE 0.0s
#6 [builder 1/2] FROM docker.io/library/caddy:2.9.1-builder@sha256:2223a2b14c52cd3d6054cbb97b14d57e9ae6b06d5fe3f72102bd50be01adca88
#6 DONE 0.0s
#7 [stage-1 1/2] FROM docker.io/library/caddy:2.9.1@sha256:cd261fc62394f1ff0b44f16eb1d202b4e71d5365c9ec866a4f1a9c5a52da9352
#7 DONE 0.0s
#8 [builder 2/2] RUN xcaddy build --with github.com/caddy-dns/cloudflare --with github.com/WeidiDeng/caddy-cloudflare-ip
#8 CACHED
#9 [stage-1 2/2] COPY --from=builder /usr/bin/caddy /usr/bin/caddy
#9 CACHED
#10 exporting to image
#10 exporting layers done
#10 writing image sha256:3aa99a677c5c086a5765e5d02191345311b59c45d35b0366cd0698d3d90176d0 done
#10 naming to docker.io/onepub/onepub-caddy:4.63.117 done
#10 DONE 0.0s
Publishing onepub-caddy docker image: onepub/onepub-caddy:4.63.117...
Mohammed90
(Mohammed Al Sahaf)
April 2, 2025, 10:04pm
4
Sorry, I might’ve misread the Docker Hub layer description.
Anyway, I pulled your image to check, and the custom modules are there. Are you passing the correct version in ONEPUB_VERSION
?
/srv # caddy list-modules
admin.api.load
admin.api.metrics
admin.api.pki
admin.api.reverse_proxy
caddy.adapters.caddyfile
caddy.config_loaders.http
caddy.filesystems
caddy.listeners.http_redirect
caddy.listeners.proxy_protocol
caddy.listeners.tls
caddy.logging.cores.mock
caddy.logging.encoders.append
caddy.logging.encoders.console
caddy.logging.encoders.filter
caddy.logging.encoders.filter.cookie
caddy.logging.encoders.filter.delete
caddy.logging.encoders.filter.hash
caddy.logging.encoders.filter.ip_mask
caddy.logging.encoders.filter.query
caddy.logging.encoders.filter.regexp
caddy.logging.encoders.filter.rename
caddy.logging.encoders.filter.replace
caddy.logging.encoders.json
caddy.logging.writers.discard
caddy.logging.writers.file
caddy.logging.writers.net
caddy.logging.writers.stderr
caddy.logging.writers.stdout
caddy.storage.file_system
events
http
http.authentication.hashes.bcrypt
http.authentication.providers.http_basic
http.encoders.gzip
http.encoders.zstd
http.handlers.acme_server
http.handlers.authentication
http.handlers.copy_response
http.handlers.copy_response_headers
http.handlers.encode
http.handlers.error
http.handlers.file_server
http.handlers.headers
http.handlers.intercept
http.handlers.invoke
http.handlers.log_append
http.handlers.map
http.handlers.metrics
http.handlers.push
http.handlers.request_body
http.handlers.reverse_proxy
http.handlers.rewrite
http.handlers.static_response
http.handlers.subroute
http.handlers.templates
http.handlers.tracing
http.handlers.vars
http.ip_sources.static
http.matchers.client_ip
http.matchers.expression
http.matchers.file
http.matchers.header
http.matchers.header_regexp
http.matchers.host
http.matchers.method
http.matchers.not
http.matchers.path
http.matchers.path_regexp
http.matchers.protocol
http.matchers.query
http.matchers.remote_ip
http.matchers.tls
http.matchers.vars
http.matchers.vars_regexp
http.precompressed.br
http.precompressed.gzip
http.precompressed.zstd
http.reverse_proxy.selection_policies.client_ip_hash
http.reverse_proxy.selection_policies.cookie
http.reverse_proxy.selection_policies.first
http.reverse_proxy.selection_policies.header
http.reverse_proxy.selection_policies.ip_hash
http.reverse_proxy.selection_policies.least_conn
http.reverse_proxy.selection_policies.query
http.reverse_proxy.selection_policies.random
http.reverse_proxy.selection_policies.random_choose
http.reverse_proxy.selection_policies.round_robin
http.reverse_proxy.selection_policies.uri_hash
http.reverse_proxy.selection_policies.weighted_round_robin
http.reverse_proxy.transport.fastcgi
http.reverse_proxy.transport.http
http.reverse_proxy.upstreams.a
http.reverse_proxy.upstreams.multi
http.reverse_proxy.upstreams.srv
pki
tls
tls.ca_pool.source.file
tls.ca_pool.source.http
tls.ca_pool.source.inline
tls.ca_pool.source.pki_intermediate
tls.ca_pool.source.pki_root
tls.ca_pool.source.storage
tls.certificates.automate
tls.certificates.load_files
tls.certificates.load_folders
tls.certificates.load_pem
tls.certificates.load_storage
tls.client_auth.verifier.leaf
tls.get_certificate.http
tls.get_certificate.tailscale
tls.handshake_match.local_ip
tls.handshake_match.remote_ip
tls.handshake_match.sni
tls.handshake_match.sni_regexp
tls.issuance.acme
tls.issuance.internal
tls.issuance.zerossl
tls.leaf_cert_loader.file
tls.leaf_cert_loader.folder
tls.leaf_cert_loader.pem
tls.leaf_cert_loader.storage
tls.permission.http
tls.stek.distributed
tls.stek.standard
Standard modules: 124
dns.providers.cloudflare
http.ip_sources.cloudflare
Non-standard modules: 2
Unknown modules: 0
/srv #
hmoffatt
(Hamish Moffatt)
April 3, 2025, 3:00am
5
Your trusted_proxies
needs to be within the reverse_proxy
block I think. reverse_proxy (Caddyfile directive) — Caddy Documentation
Once again thanks for the help.
You can see below what I ended up with (it now correctly validates).
The key change was as you suggested, putting the trusted_proxies in the global servers section.
{
# Email for Let's Encrypt notifications
email {$EMAIL}
# ACME CA URL allows us to flip between production and staging.
# we default to staging.
acme_ca ${ACME_URL:https://acme-staging-v02.api.letsencrypt.org/directory}
log {
output file /var/log/caddy/access.log
}
servers {
trusted_proxies cloudflare {
interval 12h
timeout 15s
}
}
}
# Main Domain Configuration
*.onepub.dev, onepub.dev {
tls {
# API Token required for Wild card certs
dns cloudflare {env.CLOUDFLARE_API_TOKEN}
}
# Remove duplicate slashes from the URI
# (e.g., /path//to///resource) but avoids the query string
@multiple_slashes path_regexp multipleSlashes ^(.*)//+(.*)$
redir @multiple_slashes {scheme}://{host}{re.multipleSlashes.1}/{re.multipleSlashes.2}{query} permanent
# Block common AI crawler User-Agents
@block_ai header_regexp User-Agent "(?i)(GPTBot|ChatGPT|Google-Extended|Claude-Web|Anthropic|Amazonbot|FacebookBot|cohere-ai|Bytespider|YouBot)"
respond @block_ai 403
# Block all PHP file requests, returning no response (similar to nginx 444)
@block_php path *.php
abort @block_php
# Redirect all .mp4 requests permanently (HTTP 301) to video subdomain
@mp4_redirect path *.mp4
redir @mp4_redirect https://video.{host}{uri} permanent
handle /mailhog/* {
basic_auth {
# password is in lastpass under 'mailhog on production'
# hash is generated by running caddy hash-password.
bsutton }
reverse_proxy 127.0.0.1:8025
}
handle /api/* {
reverse_proxy 127.0.0.1:8080 {
header_up Host {host}
header_up X-Real-IP {remote_host}
transport http {
read_timeout 300s
}
# Required for WebSocket support
header_up Connection {header.Connection}
header_up Upgrade {header.Upgrade}
}
}
# Global error handling, applied only for API endpoints
handle_errors {
@api path /api/*
handle @api {
root * /etc/caddy/json
rewrite 502 /500.json
rewrite 404 /404.json
file_server
}
}
reverse_proxy 127.0.0.1:8080 {
header_up Host {host}
header_up X-Real-IP {remote_host}
transport http {
read_timeout 300s
}
# Required for WebSocket support
header_up Connection {header.Connection}
header_up Upgrade {header.Upgrade}
}
# The install script (op-build.dart) will insert a set of
# trusted_proxies statements. One for each Cloudflaer IP
# listed at https://www.cloudflare.com/ips/
# THE FOLLOWING LINE WILL BE REPLACED BY THE BUILD SCRiPTS
# <!IPS FOR CLOUDFLARE>
encode gzip
}
# Combined Video Domains Configuration
# we serve video on the video subdomain, because it would violate
# cloudflare TOS to proxy video through cloudflare
video.onepub.dev, video.beta.onepub.dev {
tls {
# API Token required for Wild card certs
dns cloudflare {env.CLOUDFLARE_API_TOKEN}
}
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Frame-Options "SAMEORIGIN"
X-Content-Type-Options "nosniff"
}
handle /*.mp4 {
reverse_proxy 127.0.0.1:8080 {
header_up Host {host}
header_up X-Real-IP {remote_host}
header_up X-Forwarded-Proto https
transport http {
read_timeout 300s
}
# Web sc
header_up Connection {header.Connection}
header_up Upgrade {header.Upgrade}
}
}
# Domain-based redirection (fallback)
@prod host video.onepub.dev
redir @prod https://onepub.dev{uri} permanent
redir https://beta.onepub.dev{uri} permanent
encode gzip
}
Mohammed90
(Mohammed Al Sahaf)
April 5, 2025, 12:50pm
7
You mean you never used it in the config before?! I thought your complaint was that it doesn’t exist in the build, thus errors when used in config,
both where issues.
I wasn’t certain it was in the build and I didn’t know where to place it.
Again thanks for the help. I wouldn’t have found the solution without your assistance.