dokku-google-auth
Put Google OAuth SSO in front of any dokku app with one command. Configure Google once, then:
dokku google-auth:enable my-app
Every request to my-app now requires a signed-in Google account from your
organization. The app itself never touches OAuth — it just reads the
signed-in user from request headers. Paths that should stay open (webhooks,
API-key-protected endpoints, health checks) can be excluded per app.
How it works
┌──────────────────────── dokku host ────────────────────────┐
│ │
browser ── https ──▶ │ nginx (per-app vhost, unchanged dokku routing) │
│ │ │
│ ├─ auth_request GET /_google-auth/verify ──┐ │
│ │ 201? no: 302 to Google sign-in │ │
│ │ ▼ │
│ │ ┌────────────────────────┐ │
│ │ │ google-auth-proxy │ │
│ │ │ (one shared container, │ │
│ │ │ 127.0.0.1:2999) │ │
│ │ └────────────────────────┘ │
│ ▼ │
│ app container ◀── X-Forwarded-Email / X-Forwarded-User │
└────────────────────────────────────────────────────────────┘
- One shared Go service (
google-auth-proxy) runs in a single Docker container on the host, bound to127.0.0.1. It holds the Google client credentials and handles the entire OAuth dance. - Per-app nginx config is injected through dokku's standard
~dokku/<app>/nginx.conf.d/include. It uses nginx'sauth_requestmodule: every request triggers a fast subrequest to the auth service, which checks an encrypted session cookie. No valid session → the browser is redirected through Google sign-in and back. - Only one redirect URI is ever registered with Google (the "auth
host"). When a user signs in to
app-a.example.com, Google redirects tohttps://<auth-host>/_google-auth/callback; the service verifies the identity, then bounces the browser toapp-awith a single-use, 60-second, encrypted hand-off token, which becomes a session cookie scoped toapp-a's own domain. Adding app #47 therefore requires zero changes in the Google Console — apps don't even need to share a parent domain. - Sessions, OAuth state, and hand-off tokens are all AES-256-GCM encrypted
and authenticated with a secret generated at configure time. Session
cookies are
Secure,HttpOnly,SameSite=Lax, and pinned to the host they were minted on.
Headers your apps receive
On every authenticated request, nginx injects (and strips anything the client tried to spoof):
| Header | Value |
|---|---|
X-Forwarded-Email / X-Auth-Request-Email |
signed-in Google email (lowercased) |
X-Forwarded-User / X-Auth-Request-User |
stable Google account id (sub claim) |
X-Auth-Request-Name |
display name |
On excluded paths these headers are set to the empty string, so your app
can trust that a non-empty X-Forwarded-Email always came from the plugin.
How dokku plugins work (background)
A dokku plugin is just a git repo of executable shell scripts. Dokku clones
it into /var/lib/dokku/plugins/available/<name> and:
plugin.toml— metadata (description, version).install— runs as root atplugin:install/plugin:updatetime. This plugin uses it to create its data directory anddocker buildthe Go auth service image (multi-stage build, so the host only needs Docker, not Go).commands+subcommands/<name>— dokku dispatchesdokku google-auth:enable footosubcommands/enable, passing the full command line.commandsprovidesdokku google-auth:help. This is why the plugin must be installed under the namegoogle-auth.- Triggers — executables named after lifecycle hooks that dokku (via
plugn) calls with arguments. This plugin
implements:
nginx-pre-reload/core-post-deploy— regenerate the app'snginx.conf.d/google-auth.confon every deploy, so it always references the app's current upstream (whose name changes if you remap ports).post-delete,post-app-rename,post-app-clone— keep plugin state in sync with app lifecycle.
- State lives under
/var/lib/dokku/data/google-auth/(global config + one directory per enabled app). Secrets are0600.
The nginx integration relies on a stable, documented dokku feature: the
generated vhost for every app contains
include /home/dokku/<app>/nginx.conf.d/*.conf; inside the server block,
which is exactly where this plugin drops its location blocks.
Installation
On the dokku host:
sudo dokku plugin:install https://github.com/<you>/dokku-google-auth.git --name google-auth
(The --name google-auth matters — subcommand dispatch is keyed off the
plugin directory name.)
Requirements: dokku with the default nginx proxy (not traefik/caddy),
Docker (always present on a dokku host), and nginx built with
http_auth_request_module (true for stock Debian/Ubuntu nginx).
1. Create the Google OAuth client (one time, ever)
-
Configure the OAuth consent screen if you haven't: Internal user type (recommended for a Workspace org — it automatically limits sign-in to your organization and skips app verification).
-
Create Credentials → OAuth client ID → Web application.
-
Add exactly one Authorized redirect URI:
https://<auth-host>/_google-auth/callback<auth-host>is any HTTPS domain that routes to an app you will enable the plugin on. Pick your most permanent app's domain, or add a dedicated domain (e.g.auth.example.com) to one of your apps withdokku domains:add <app> auth.example.com(plus DNS + letsencrypt as usual). All other apps piggyback on it. -
Note the client ID and client secret.
2. Configure the plugin
configure is re-runnable: every flag it takes is persisted and can be
changed later by passing it again.
dokku google-auth:configure \
--client-id 1234567890-abc.apps.googleusercontent.com \
--client-secret GOCSPX-xxxxxxxxxxxx \
--auth-host auth.example.com \
--allow-domain signal.org
This starts the shared auth service container and prints the callback URL to double-check against the Google Console.
Other flags (all optional, all persisted):
| Flag | Meaning | Default |
|---|---|---|
--allow-domain <d> |
allow any verified *@d account (repeatable; replaces the stored list) |
— |
--allow-email <e> |
allow a specific address, e.g. an outside collaborator (repeatable) | — |
--deny-email <e> |
block a specific address even if the allow lists cover it (repeatable; replaces the stored list) | — |
--clear-deny-emails |
empty the deny list | — |
--session-ttl <dur> |
how long a sign-in lasts (24h, 72h, 30m, …) |
24h |
--cookie-name <n> |
session cookie name | _google_auth |
--port <p> |
host port (127.0.0.1 only) for the auth service | 2999 |
--regenerate-cookie-secret |
rotate the session encryption key (signs everyone out) | — |
These flags replace the list they name, which suits initial setup. For one-at-a-time changes afterwards, see the next section.
3. Decide who is allowed in
--allow-domain and --allow-email together form the allowlist, and at
least one entry is required. Any account matching neither is rejected — there
is no "allow everyone" mode. So to limit access to a specific list of people,
use only addresses and no domain:
dokku google-auth:allow greyson@signal.org alice@signal.org
The deny list is checked first and wins over both allow rules, which is how you cut off one person without narrowing the whole domain:
dokku google-auth:allow signal.org # everyone at signal.org…
dokku google-auth:deny former@signal.org # …except this account
These four commands each change one entry at a time and restart the auth service for you:
dokku google-auth:allow # show the allow list
dokku google-auth:allow signal.org # a domain (any verified account there)
dokku google-auth:allow guest@partner.com # one address
dokku google-auth:unallow guest@partner.com # remove either kind
dokku google-auth:deny # show the deny list
dokku google-auth:deny former@signal.org
dokku google-auth:undeny former@signal.org
Changes take effect on the affected user's next request: session cookies
are re-checked against the current lists rather than trusted until they
expire, so denying (or unallowing) someone with a live session ends it. To
sign out everyone at once instead, use
configure --regenerate-cookie-secret.
unallow refuses to remove the last allow entry, since an empty allowlist
locks everyone out of every enabled app. dokku google-auth:report shows
both lists as they currently stand.
4. Protect apps
dokku google-auth:enable my-app
dokku google-auth:enable other-app
...
That's it. Visit the app in a browser — you'll be bounced through Google and back.
5. Exclude paths (optional, per app)
# Path prefix — everything under it is open:
dokku google-auth:exclude my-app /api/webhooks
# Regex (prefix with re:):
dokku google-auth:exclude my-app 're:^/(healthz|metrics)$'
# List / remove:
dokku google-auth:exclude my-app
dokku google-auth:unexclude my-app /api/webhooks
Excluded paths proxy straight to your app with identity headers blanked — protect them yourself (API key, HMAC signature, etc.).
Day-to-day commands
dokku google-auth:report # global + per-app status, incl. both access lists
dokku google-auth:report my-app # one app
dokku google-auth:allow alice@signal.org # let someone in
dokku google-auth:deny former@signal.org # cut someone off
dokku google-auth:disable my-app # turn SSO off for an app
dokku google-auth:logs -t # follow auth service logs (sign-ins, denials)
dokku google-auth:restart # restart the auth service
Users can check who they're signed in as at https://<any-app>/_google-auth/
and sign out at https://<any-app>/_google-auth/logout.
Things worth knowing
- HTTPS is required in practice. Google refuses plain-HTTP redirect URIs
and the session cookie is marked
Secure. Use dokku-letsencrypt as usual. (--insecure-allow-httpexists for local experiments only.) - The auth host must stay enabled. Google's callback lands on
<auth-host>, so the app serving that domain must keep google-auth enabled.google-auth:disablewarns you if you disable that app. - Sessions are per-domain. Signing in to
app-athen visitingapp-btriggers another round-trip through Google, but it's silent (already signed in) — the user just sees a quick redirect. - In-flight POSTs across an expired session get redirected to sign-in and
are replayed as GETs — the POST body is lost. That's inherent to
redirect-based SSO; keep session TTLs comfortable (e.g.
72h) if it bites. - Custom nginx location blocks in your own
nginx.conf.dfiles: plainlocation /fooprefix blocks will be shadowed by this plugin's catch-all regex location — uselocation ^~ /fooin your own snippets if you need them to win. - Non-browser clients (curl, fetch without
Accept: text/html) get a JSON401instead of a redirect chain. - Websockets work — upgrade headers are forwarded exactly like dokku's stock nginx template.
- Plugin updates:
sudo dokku plugin:update google-authrebuilds the service image and restarts the container automatically.
Uninstall
dokku google-auth:disable <each-app>
dokku google-auth:stop
sudo dokku plugin:uninstall google-auth
sudo rm -rf /var/lib/dokku/data/google-auth
docker image rm dokku-google-auth:latest
Development
Tasks are defined in mise.toml (mise pins Go and
shellcheck):
mise install # install pinned toolchain
mise run test # Go unit tests (full OAuth flow against a fake Google)
mise run test-nginx-conf # generate nginx config + validate with real nginx in docker
mise run shellcheck # lint all plugin scripts
mise run check # everything CI would run
mise run docker-build # build the service image locally
Layout:
cmd/google-auth-proxy/ Go entrypoint
internal/authproxy/ OAuth flow, session crypto, HTTP handlers (+ tests)
functions shared bash for the plugin (config store, nginx generation)
subcommands/ dokku google-auth:* commands
install, nginx-pre-reload, core-post-deploy, post-* dokku lifecycle triggers
test/nginx-conf-test.sh bash integration test for the generated nginx config
Dockerfile multi-stage build → static binary in a scratch image