1. Caddy version (caddy version
):
2.4.6, built with the following modules:
- caddy-maxmind-geolocation
- format-encoder
- duckdns
2. How I run Caddy:
docker-compose (non-root user) on a VPS, proxying requests to my home server over a Wireguard tunnel, and also to some docker containers running on-premise.
a. System environment:
Debian 11, docker-compose version 1.25.0
b. Command:
docker-compose up -d --build
c. Service/unit/compose file:
docker-compose.yml:
version: "3"
networks:
default:
external:
name: caddy_net
services:
caddy:
# image: "caddy:latest"
build: .
container_name: "caddy"
hostname: "caddy"
restart: unless-stopped
user: 1000:1000
ports:
- "443:8443"
environment:
- ALLOWED_COUNTRIES=$allowed_countries
- ALT_DOMAIN=$alt_domain
- EMAIL=$email
- DUCKDNS_TOKEN=$duckdns_token
- DOMAIN=$domain
- INVIDIOUS_USER=$invidious_user
- INVIDIOUS_PASSWORD=$invidious_password
- IP_HOME=$ip_home
- IP_SELF=$ip_self
- LIBREDDIT_USER=$libreddit_user
- LIBREDDIT_PASSWORD=$libreddit_password
- NITTER_USER=$nitter_user
- NITTER_PASSWORD=$nitter_password
- TM_USER=$tm_user
- TM_PASSWORD=$tm_password
- WHOOGLE_USER=$whoogle_user
- WHOOGLE_PASSWORD=$whoogle_password
- WIREGUARD_TUNNEL=$wireguard_tunnel
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- ./GeoLite2-Country.mmdb:/etc/caddy/GeoLite2-Country.mmdb
- ./data:/data
- ./config:/config
- ./log:/var/log
Dockerfile:
FROM caddy:builder AS builder
RUN xcaddy build \
--with github.com/porech/caddy-maxmind-geolocation \
--with github.com/caddyserver/format-encoder \
--with github.com/caddy-dns/duckdns
FROM caddy:latest
COPY --from=builder /usr/bin/caddy /usr/bin/caddy
d. My complete Caddyfile or JSON config:
{
email {$EMAIL}
skip_install_trust
https_port 8443
acme_dns duckdns {$DUCKDNS_TOKEN} {
override_domain {$ALT_DOMAIN}
}
log {
# level debug
format console {
time_format wall
}
}
}
(auth_untrusted) {
@untrusted_ips {
not remote_ip {$IP_HOME}
}
basicauth @untrusted_ips {
{args.0} {args.1}
}
}
(logging) {
log {
format formatted
output file /var/log/access.log {
roll_keep 1
roll_keep_for 7d
}
}
}
(geofilter) {
@geofilter {
maxmind_geolocation {
db_path "/etc/caddy/GeoLite2-Country.mmdb"
deny_countries {$DENIED_COUNTRIES}
}
not remote_ip {$IP_SELF} {$IP_HOME}
}
respond @geofilter 403
}
(security) {
header {
Permissions-Policy interest-cohort=()
Strict-Transport-Security "max-age=31536000;"
X-Content-Type-Options "nosniff"
X-Frame-Options "SAMEORIGIN"
X-Robots-Tag "none"
X-Permitted-Cross-Domain-Policies "none"
X-XSS-Protection "1; mode=block"
Referrer-Policy "no-referrer-when-downgrade"
}
}
*.{$DOMAIN} {
map {labels.2} {upstream} {auth_user} {auth_pass} {
collabora {$WIREGUARD_TUNNEL}:80
gotify {$WIREGUARD_TUNNEL}:80
grocy {$WIREGUARD_TUNNEL}:80
jellyfin {$WIREGUARD_TUNNEL}:80
nextcloud {$WIREGUARD_TUNNEL}:80
tm {$WIREGUARD_TUNNEL}:80 {$TM_USER} {$TM_PASSWORD}
git gitea-app:3000
libreddit libreddit:8080 {$LIBREDDIT_USER} {$LIBREDDIT_PASSWORD}
nitter nitter:8080 {$NITTER_USER} {$TM_PASSWORD}
whoogle whoogle:5000 {$WHOOGLE_USER} {$WHOOGLE_PASSWORD}
yt invidious-app:3000 {$INVIDIOUS_USER} {$INVIDIOUS_PASSWORD}
}
encode zstd gzip
import logging
import geofilter
@security not expression `{labels.2} == "nextcloud"`
route @security {
import security
}
@auth expression `{labels.2}.matches("libreddit|nitter|tm|whoogle|yt")`
route @auth {
import auth_untrusted {auth_user} {auth_pass}
}
reverse_proxy {upstream}
}
3. The problem I’m having:
This is more of an aesthetic question, and perhaps a call for a general sanity check on my configuration. I recently redesigned my Caddyfile to use the map directive with a wildcard domain, and I’m very happy with how this works. I do have a (very) minor issue with the handful of subdomains that I want to put behind basic authentication.
Ideally, I would have liked to map the basicauth credentials to placeholders, as in the (non-functional) Caddyfile I included above. This, however, gives me the error message seen in section 4 (“username and password are required” being the relevant part), which I assume is because the {auth_user} and {auth_pass} placeholders are being parsed as empty. The environmental variables are all good and set, so that’s not the issue, but I realize I might be trying to over-optimize here and am straining the possibilities of what placeholders are meant to do.
So, like I said, this is a minor issue, since I’m able to work around it quite easily with a couple of route directives (see section 5). Still, though, I was wondering:
- Is there perhaps some more elegant way of doing what I want without having to import the
auth_untrusted
snippet five times?
And also:
- Am I using the
route
directives correctly here? See, e.g., the way I’m routing thesecurity
snippet for all the subdomains that aren’tnextcloud
. That is, I’m usingroute
more or less only as a non-mutually exclusivehandle
, but I’m not sure that’s quite the intention. It seems to work fine, though.
PS. I really want to take the opportunity to commend you all on what a fantastic piece of software Caddy is. I’m having an amazing time using it!
4. Error messages and/or full log output:
2022/02/11 12:15:43 info admin admin endpoint started {"address": "tcp/localhost:2019", "enforce_origin": false, "origins": ["127.0.0.1:2019", "localhost:2019", "[::1]:2019"]}
2022/02/11 12:15:43 info http server is listening only on the HTTPS port but has no TLS connection policies; adding one to enable TLS {"server_name": "srv0", "https_port": 8443}
2022/02/11 12:15:43 info http enabling automatic HTTP->HTTPS redirects {"server_name": "srv0"}
2022/02/11 12:15:43 info tls.cache.maintenance started background certificate maintenance {"cache": "0xc0004329a0"}
2022/02/11 12:15:44 info tls.cache.maintenance stopped background certificate maintenance {"cache": "0xc0004329a0"}
run: loading initial config: loading new config: loading http app module: provision http: server srv0: setting up route handlers: route 1: loading handler modules: position 0: loading module 'subroute': provision http.handlers.subroute: setting up subroutes: route 2: loading handler modules: position 0: loading module 'subroute': provision http.handlers.subroute: setting up subroutes: route 0: loading handler modules: position 0: loading module 'authentication': provision http.handlers.authentication: loading authentication providers: module name 'http_basic': provision http.authentication.providers.http_basic: account 0: username and password are required
5. What I already tried:
This works, but feels a bit clunky:
*.{$DOMAIN} {
map {labels.2} {upstream} {
collabora {$WIREGUARD_TUNNEL}:80
gotify {$WIREGUARD_TUNNEL}:80
grocy {$WIREGUARD_TUNNEL}:80
jellyfin {$WIREGUARD_TUNNEL}:80
nextcloud {$WIREGUARD_TUNNEL}:80
tm {$WIREGUARD_TUNNEL}:80
git gitea-app:3000
libreddit libreddit:8080
nitter nitter:8080
whoogle whoogle:5000
yt invidious-app:3000
}
encode zstd gzip
import logging
import geofilter
@security not expression `{labels.2} == "nextcloud"`
route @security {
import security
}
@auth expression `{labels.2}.matches("libreddit|nitter|tm|whoogle|yt")`
route @auth {
@libreddit expression `{labels.2} == "libreddit"`
route @libreddit {
import auth_untrusted {$LIBREDDIT_USER} {$LIBREDDIT_PASSWORD}
}
@nitter expression `{labels.2} == "nitter"`
route @nitter {
import auth_untrusted {$NITTER_USER} {$NITTER_PASSWORD}
}
@tm expression `{labels.2} == "tm"`
route @tm {
import auth_untrusted {$TM_USER} {$TM_PASSWORD}
}
@whoogle expression `{labels.2} == "whoogle"`
route @whoogle {
import auth_untrusted {$WHOOGLE_USER} {$WHOOGLE_PASSWORD}
}
@yt expression `{labels.2} == "yt"`
route @yt {
import auth_untrusted {$INVIDIOUS_USER} {$INVIDIOUS_PASSWORD}
}
}
reverse_proxy {upstream}
}