authwall-sidecar-nginx
Several domains behind nginx, with Authwall as a sidecar auth checker. nginx
serves each domain's app directly and consults Authwall only for an auth
decision, using nginx's auth_request module. Authwall is not in the data
path.
client → nginx → { apps, notes, echo }
↑ auth check
authwallflowchart LR
client --> nginx
nginx --> apps
nginx --> notes
nginx --> echo
nginx -.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 nginx attached from Authwall's auth decision.
How it works
nginx.conf is the whole nginx config, mounted over /etc/nginx/nginx.conf.
A single *.mydomain.test server serves two things:
/auth/…is proxied to Authwall — its sign-in UI, OAuth callbacks, and the/auth/sidecarendpoint.- everything else is the protected app. For each request nginx makes an
internal
auth_requestto/auth/sidecar; on 200 it copiesX-Auth-Userand proxies to the app picked from the requestHostby amap; on 401 it redirects the browser to the sign-in page with areturnURL 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 set by nginx from the auth subrequest's response, which overrides any value a client tried to send. The app can trust it.- The apps are reachable only through nginx — 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. - Edit the
map $host $upstreamentries innginx.confto 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, terminate TLS in front of nginx and set
AUTHWALL_PUBLIC_URLaccordingly.
Configuration files
docker-compose.yaml
services:
# nginx is the entrypoint and the only published service. It serves each
# app's domain directly — gating every request with an `auth_request` to
# Authwall's /auth/sidecar endpoint — and serves the Authwall UI under /auth
# on every domain.
nginx:
image: nginx:alpine
restart: unless-stopped
ports:
- 3000:80
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
depends_on:
- authwall
- apps
- notes
- echo
# Authwall runs beside nginx 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-stoppednginx.conf
events {}
http {
# Docker internal DNS
resolver 127.0.0.11 valid=10s;
map $host $upstream {
default "";
apps.mydomain.test http://apps:8080;
echo.mydomain.test http://echo:8080;
notes.mydomain.test http://notes:8080;
}
server {
listen 80;
server_name *.mydomain.test;
# ── Authwall UI (sign-in, OAuth callbacks, sessions, etc.) ──
location /auth/ {
proxy_pass http://authwall:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Connection "";
proxy_http_version 1.1;
}
# ── Protected apps ──
location / {
# ── Auth check ──
auth_request /_auth;
auth_request_set $auth_user $upstream_http_x_auth_user;
# On 401 → redirect to sign-in
error_page 401 = @authwall_signin;
# Unknown host → 404
if ($upstream = "") {
return 404;
}
# ── Proxy to upstream app ──
proxy_pass $upstream;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Auth-User $auth_user;
proxy_set_header Connection "";
proxy_http_version 1.1;
}
# ── Auth subrequest ──
location = /_auth {
internal;
proxy_pass http://authwall:3000/auth/sidecar;
proxy_set_header Host $host;
proxy_set_header Cookie $http_cookie;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Original-URI $scheme://$host$request_uri;
proxy_set_header X-Original-Method $request_method;
proxy_set_header Content-Length "";
proxy_pass_request_body off;
}
# ── Redirect to login ──
location @authwall_signin {
# Relative $uri: sign-in is on this same domain, so no scheme/host
# is needed — and a relative path avoids query-encoding pitfalls.
return 302 /auth/sign-in?return=$uri;
}
}
}