Colon in default env var

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

https://hub.docker.com/repository/docker/onepub/onepub-caddy/tags/4.63.118/sha256-c13efa82a71577aa5085b99b4fdcd906413475978ab342a47438f9584559254e

4. How I installed and ran Caddy:

docker
https://hub.docker.com/repository/docker/onepub/onepub-caddy/tags/4.63.118/sha256-c13efa82a71577aa5085b99b4fdcd906413475978ab342a47438f9584559254e

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
}

5. Links to relevant resources:

That seems to be incorrect. You probably want:

{	acme_ca {$ACME_URL:"https://acme-staging-v02.api.letsencrypt.org/directory"}

}

Notice the dollar sign position. See this for more details:

1 Like

Yes that fixed it.

docker uses ${xx} and caddy {$xx}…