authwall-sidecar-caddy
Several domains behind Caddy, with Authwall as a sidecar auth checker. Caddy
serves each domain's app directly and consults Authwall only for an auth
decision, using Caddy's forward_auth directive. Authwall is not in the
data path.
client → caddy → { apps, notes, echo }
↑ auth check
authwallflowchart LR
client --> caddy
caddy --> apps
caddy --> notes
caddy --> echo
caddy -.auth check.-> authwallUse this topology when you want Authwall out of the request path — for performance or isolation — and only need it to answer "is this user signed in?" on each request.
Set up local hostnames
The example uses three domains, so add them to your hosts file — /etc/hosts
on Linux/macOS, C:\Windows\System32\drivers\etc\hosts on Windows:
127.0.0.1 apps.mydomain.test
127.0.0.1 notes.mydomain.test
127.0.0.1 echo.mydomain.testRun it
docker compose upThen open any of the three domains on port 3000:
You are redirected to Authwall's sign-in page, served under /auth on the same
domain; sign up, and you are sent back to the app you started from. Because the
session cookie is scoped to mydomain.test (AUTHWALL_COOKIE_DOMAIN), that one
sign-in is valid across all three domains. Each echo-server response shows the
X-Auth-User header Caddy attached from Authwall's auth decision.
How it works
Caddyfile is mounted over /etc/caddy/Caddyfile. A (gate) snippet holds the
shared logic, and each domain is a site block that imports it with its own
backend app:
/auth/*is proxied to Authwall — its sign-in UI, OAuth callbacks, and the/auth/sidecarendpoint.- everything else is the protected app.
forward_authmakes a subrequest to Authwall's/auth/sidecar; on 200 Caddy copiesX-Auth-Userand proxies to the app; on 401 it redirects the browser to the sign-in page with a relativereturnpath so the user comes back.
AUTHWALL_COOKIE_DOMAIN=mydomain.test scopes the session cookie to every
*.mydomain.test domain, so one sign-in covers all of them.
Security notes
X-Auth-Useris taken from the auth subrequest's trusted response — the app can rely on it.- The apps are reachable only through Caddy — do not publish the
apps/notes/echoservices directly, or requests would bypass the auth check.
What to change for your app
- Replace the
apps/notes/echoservices with your own apps. - Add or edit the per-domain site blocks in the
Caddyfileto match your domains. - The
/auth/path prefix is reserved for Authwall on every domain — if an app has its own routes under/auth/, route them more specifically. - For HTTPS, give the domains real names and drop the
http://prefix — Caddy then obtains certificates automatically; setAUTHWALL_PUBLIC_URLto match.
Configuration files
docker-compose.yaml
services:
# Caddy is the entrypoint and the only published service. It serves each
# app's domain directly — gating every request with a `forward_auth` to
# Authwall's /auth/sidecar endpoint — and serves the Authwall UI under /auth
# on every domain.
caddy:
image: caddy:alpine
restart: unless-stopped
ports:
- 3000:80
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
depends_on:
- authwall
- apps
- notes
- echo
# Authwall runs beside Caddy as an auth checker, reached under /auth on every
# domain. It is not in the data path and never proxies, so it needs no
# AUTHWALL_UPSTREAM_URL. AUTHWALL_COOKIE_DOMAIN shares the session across every
# *.mydomain.test host, so one sign-in covers them all.
authwall:
image: vbarbarosh/authwall
restart: unless-stopped
environment:
AUTHWALL_PUBLIC_URL: http://apps.mydomain.test:3000
AUTHWALL_COOKIE_DOMAIN: mydomain.test
volumes:
- ./data:/app/data
# Stand-in upstream apps, one per domain. echo-server echoes each request,
# so the response shows which Host reached which app.
apps:
image: jmalloc/echo-server
restart: unless-stopped
notes:
image: jmalloc/echo-server
restart: unless-stopped
echo:
image: jmalloc/echo-server
restart: unless-stoppedCaddyfile
# Authwall as a sidecar auth checker, several domains behind Caddy.
#
# Caddy serves each domain's app directly and consults Authwall only for an
# auth decision (forward_auth → /auth/sidecar). Authwall is not in the data
# path; its UI is served under /auth on every domain.
#
# The (gate) snippet holds the shared logic; each domain is a site block that
# imports it with its own backend app.
(gate) {
# Authwall's UI and endpoints.
handle /auth/* {
reverse_proxy authwall:3000
}
# The protected app, gated by an auth subrequest to Authwall.
handle {
forward_auth authwall:3000 {
uri /auth/sidecar
copy_headers X-Auth-User
# No session → send the browser to the sign-in page.
@unauthorized status 401
handle_response @unauthorized {
redir /auth/sign-in?return={http.request.uri.path}
}
}
reverse_proxy {args[0]}
}
}
http://apps.mydomain.test {
import gate apps:8080
}
http://notes.mydomain.test {
import gate notes:8080
}
http://echo.mydomain.test {
import gate echo:8080
}