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
         authwall
flowchart LR
    client --> nginx
    nginx --> apps
    nginx --> notes
    nginx --> echo
    nginx -.auth check.-> authwall

Use 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.test

Run it

docker compose up

Then 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/sidecar endpoint.
  • everything else is the protected app. For each request nginx makes an internal auth_request to /auth/sidecar; on 200 it copies X-Auth-User and proxies to the app picked from the request Host by a map; on 401 it redirects the browser to the sign-in page with a return URL 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-User is 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 / echo services directly, or requests would bypass the auth check.

What to change for your app

  • Replace the apps / notes / echo services with your own apps.
  • Edit the map $host $upstream entries in nginx.conf to 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_URL accordingly.

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-stopped

nginx.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;
        }
    }
}