1. The problem I’m having:
The documentation on default environment vars in caddy is rather sparse.
so I’m having trouble setting a default var for a tls url.
In my global section I have:
{ acme_ca ${ACME_URL:"https://acme-staging-v02.api.letsencrypt.org/directory"}
}
The intent is that if no ACME_URL environment variable exists then the acme_ca should default to the staging url for lets encrypt.
I’ve tried both with and without quotes around the url.
I’ve also tried escaping the colon after the https http\://acme...'
2. Error messages and/or full log output:
{"level":"error","ts":1743892471.53229,"logger":"tls.obtain","msg":"could not get certificate from issuer","identifier":"*.onepub.dev","issuer":"${ACME_URL:\"https://acme-staging-v02.api.letsencrypt.org/directory\"}","error":"parse \"${ACME_URL:\\\"https://acme-staging-v02.api.letsencrypt.org/directory\\\"}\": first path segment in URL cannot contain colon"}
{"level":"error","ts":1743892471.532569,"logger":"tls.obtain","msg":"will retry","error":"[*.onepub.dev] Obtain: parse \"${ACME_URL:\\\"https://acme-staging-v02.api.letsencrypt.org/directory\\\"}\": first path segment in URL cannot contain colon","attempt":1,"retrying_in":60,"elapsed":0.001460799,"max_duration":2592000}
3. Caddy version:
I building caddy in a docker file.
FROM caddy:2.9.1-builder AS builder
4. How I installed and ran Caddy:
a. System environment:
docker on linux.
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
COPY config/caddy /etc/caddy
# CMD ["tail", "-f", "/dev/null"]
CMD ["caddy", "run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"]
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 stdout
}
# trigger the cloudflare ip module that periodically fetchs
# the list of valid cloud flare proxy IP addresses.
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 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}
}
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
}