Skip to content

How to Deploy Tinyauth on a VPS

Updated Sep 2026

verified on Ubuntu 26.04 · Sep 2026
We earn commissions when you shop through the links below. Full disclosure →

Put Tinyauth's login screen in front of your self-hosted apps on a VPS. It runs as one small container wired into Caddy's forward_auth, with a bcrypt-hashed user, secure cookies and a trusted-proxy setting.

Before you start
  • A small VPS; Tinyauth idles at under 10 MB of RAM
  • A fresh Ubuntu 24.04 or 26.04 server with root/sudo SSH access
  • A domain with subdomains for Tinyauth and each protected app (Tinyauth rejects bare IPs)
  • Docker Engine + Compose installed (see the base guide below)
Need a box for this guide? Kamatera's free tier lets you spin one up now.Start free on Kamatera → (opens in new tab)

What Tinyauth is

Tinyauth adds a login screen in front of apps that don't have one, or whose own login you don't trust. It works as forward-auth middleware: your reverse proxy asks Tinyauth whether a request is signed in, and Tinyauth either lets it through or sends the visitor to its login page. It supports local users (with optional TOTP), OAuth sign-in through Google, GitHub or any generic OIDC provider, LDAP, and per-app access controls. Since v5 it can also act as an OpenID Connect provider, and upstream notes it is OpenID Certified for the Basic OP profile.

It is written in Go, licensed AGPL-3.0, and runs as one container. Upstream warns that Tinyauth is in active development and that configuration can change between releases, so read the release notes before you upgrade.

If you want more policy control (per-path rules, two-factor per app), see Authelia. For a passkey-only OIDC provider, see Pocket ID. If you want a full identity platform with an admin UI instead, see Authentik vs Tinyauth.

Server sizing

Measured idle on our test box, Tinyauth used about 8 MB of RAM and about 60 MB of disk. The catalog's conservative floor is 256 MB RAM. It is almost never the reason to size a server up: put it on the same small VPS as the apps it protects.

Prepare the server

This guide assumes Docker Engine, the Compose plugin and a ufw firewall are set up. If not, see Docker & Compose on Ubuntu.

sudo ufw allow OpenSSH
sudo ufw allow 80
sudo ufw allow 443
sudo ufw status verbose

DNS matters here. Tinyauth sets its session cookie on the parent domain of its own URL. With Tinyauth at auth.example.com, the cookie covers .example.com, so one login works for app.example.com, wiki.example.com and so on. Create A records for auth.example.com and each protected app, all pointing at this server. Tinyauth doesn't accept a bare IP address as its app URL. Upstream also notes that a DDNS name used directly (such as name.duckdns.org) doesn't work because of browser cookie rules. Use subdomains under it instead.

Where to host itaffiliate disclosure
Hetzner Cloudrun it on
2 vCPU · 4 GB RAM · 80 GB SSD · $23.59/mo
Get Hetzner Cloud (opens in new tab)
Kamaterafree trial
1 vCPU · 1 GB RAM · 20 GB SSD · $4.00/mo
Start free on Kamatera → (opens in new tab)
DigitalOceanalso works on
1 vCPU · 1 GB RAM · 25 GB SSD · $6.00/mo
Deploy on DigitalOcean → (opens in new tab)

Paid link — we earn a commission if you shop through it.

Install Tinyauth

mkdir -p ~/tinyauth && cd ~/tinyauth

Create the first user

A Tinyauth user is username:bcrypt-hash, with an optional third part holding a TOTP secret. The Tinyauth image can create the entry. This block generates a random password, hashes it, and writes the user to .env, only on the first run. The command prints the entry in several formats, so the block keeps only the first. The value is single-quoted because Compose would otherwise try to expand every $ in the bcrypt hash as a variable:

if [ ! -f .env ]; then
  TA_PASS=$(openssl rand -base64 18)
  TA_USER=$(docker run --rm ghcr.io/tinyauthapp/tinyauth:v5 user create --username admin --password "$TA_PASS" | grep -o 'admin:\$2[aby]\$[^[:cntrl:]]*' | head -1)
  printf "TINYAUTH_AUTH_USERS='%s'\n" "$TA_USER" > .env
  echo "Tinyauth login: admin / $TA_PASS (save it in your password manager now)"
fi
chmod 600 .env
grep -c 'admin:\$2' .env

To add more users, run docker run -it --rm ghcr.io/tinyauthapp/tinyauth:v5 user create --interactive and append the result to the same variable, separated by commas.

Compose file

This compose file runs Tinyauth and a tiny demo app, traefik/whoami, so you can see the whole flow working before you protect a real service:

cat > docker-compose.yml <<'YAML'
services:
  tinyauth:
    image: ghcr.io/tinyauthapp/tinyauth:v5
    restart: unless-stopped
    env_file: .env
    environment:
      TINYAUTH_APPURL: https://auth.example.com
      TINYAUTH_AUTH_SECURECOOKIE: "true"
      # Caddy on the host reaches the container through Docker's bridge gateway.
      TINYAUTH_AUTH_TRUSTEDPROXIES: 172.16.0.0/12
      TINYAUTH_DATABASE_PATH: /data/tinyauth.db
    volumes:
      - ./data:/data
    ports:
      - "127.0.0.1:3000:3000"

  whoami:
    image: traefik/whoami:latest
    restart: unless-stopped
    ports:
      - "127.0.0.1:8081:80"
YAML

What the settings do:

  • TINYAUTH_APPURL is the public URL of the login page, and the domain its cookie is set for. Replace auth.example.com with your own.
  • TINYAUTH_AUTH_SECURECOOKIE marks the session cookie HTTPS-only. It defaults to false. Turn it on as soon as TLS is in front.
  • TINYAUTH_AUTH_TRUSTEDPROXIES tells Tinyauth which addresses may set X-Forwarded-For and X-Real-IP. Upstream recommends setting it, and it is required for IP-based access rules. Caddy running on the host reaches the container from Docker's bridge network, and 172.16.0.0/12 covers Docker's default address pools. Narrow it if you know your network's subnet.
  • TINYAUTH_DATABASE_PATH keeps Tinyauth's SQLite file on the ./data volume, so it survives container re-creation.

Start it and run Tinyauth's built-in health check:

docker compose up -d
timeout 60 bash -c 'until docker compose exec -T tinyauth tinyauth healthcheck >/dev/null 2>&1; do sleep 2; done' && echo "tinyauth healthy"

Now call the forward-auth endpoint the way Caddy will, as a visitor to app.example.com with no session. Tinyauth answers browsers and scripts differently. It decides by the User-Agent header, so the first request imitates a browser and the second one doesn't:

FWD=(-H 'X-Forwarded-Proto: https' -H 'X-Forwarded-Host: app.example.com' -H 'X-Forwarded-Uri: /')
curl -s -o /dev/null -w 'browser:     %{http_code} -> %{redirect_url}\n' -A 'Mozilla/5.0 (X11; Linux x86_64) Gecko/20100101 Firefox/140.0' "${FWD[@]}" http://127.0.0.1:3000/api/auth/caddy
curl -s -D - -o /dev/null "${FWD[@]}" http://127.0.0.1:3000/api/auth/caddy | grep -iE '^(HTTP|x-tinyauth-location)'

A browser gets a 302 redirect to https://auth.example.com/login, with the original URL carried along so it can return there after sign-in. A non-browser client, such as an API call or curl, gets a 401 with the login URL in an X-Tinyauth-Location header instead of a redirect it can't follow. Either way, nobody without a session reaches the app.

HTTPS + domain via Caddy

Install Caddy as described in Automatic HTTPS with Caddy. Then add a site for Tinyauth itself and one per protected app. Each app's block uses forward_auth with Tinyauth's Caddy endpoint, /api/auth/caddy:

auth.example.com {
    reverse_proxy 127.0.0.1:3000
}

app.example.com {
    forward_auth 127.0.0.1:3000 {
        uri /api/auth/caddy
        copy_headers Remote-User Remote-Name Remote-Email Remote-Groups
    }
    reverse_proxy 127.0.0.1:8081
}

copy_headers is optional. It passes the signed-in user's identity to the app, which lets apps with trusted-header login sign the user in automatically. For other proxies, Tinyauth has endpoints for Traefik (/api/auth/traefik) and Nginx, and upstream's docs cover each of them.

Reload Caddy, open https://app.example.com, and you should land on Tinyauth's login page. After signing in, the whoami page lists the request headers, including the Remote-User header Tinyauth added.

Securing it

  • Add TOTP to your user. tinyauth totp generate adds a TOTP secret to an existing username:hash entry (run it with --interactive from the image). Replace the value in .env and run docker compose up -d.
  • Prefer OAuth for other people. Rather than sharing local accounts, point Tinyauth at Google, GitHub or your own OIDC provider, such as Pocket ID. Then restrict which accounts may sign in with a whitelist.
  • Use access controls per app. Tinyauth can restrict individual apps to specific users or OAuth groups, allow unauthenticated paths such as a health endpoint, and allow or block IP ranges. Upstream's access-controls guide shows the labels and config keys.
  • Keep port 3000 on the loopback interface. Tinyauth trusts forwarded headers from the configured proxy range. Only Caddy should reach it.
  • Failed logins are rate-limited: the default is 3 retries (TINYAUTH_AUTH_LOGINMAXRETRIES) with a 300-second timeout (TINYAUTH_AUTH_LOGINTIMEOUT).

Backups

The configuration is .env (your users) and docker-compose.yml. Runtime state, such as sessions, is in ./data. Back up all three:

cd ~/tinyauth
sudo tar czf tinyauth-backup-$(date +%F).tar.gz .env docker-compose.yml data
sudo chown "$USER": tinyauth-backup-*.tar.gz
ls -lh tinyauth-backup-*.tar.gz

The archive holds password hashes. Store it off the server, and encrypt it if you keep it anywhere shared.

Upgrades

The v5 tag follows Tinyauth's 5.x releases:

cd ~/tinyauth
docker compose pull
docker compose up -d

Because upstream warns that configuration can change between versions, read the release notes for every version you skip. After an upgrade, check docker compose logs tinyauth for startup errors about renamed options.

Troubleshooting

Tinyauth exits on startup with an app URL error. TINYAUTH_APPURL has to be a real domain name. Bare IPs and hostnames with underscores are rejected.

Login succeeds but you land back on the login page. The cookie isn't reaching the app. Either the app isn't a subdomain of the same parent domain, or TINYAUTH_AUTH_SECURECOOKIE=true is set while you're testing over plain HTTP.

The app returns 401 or 502 through Caddy. Check the forward_auth address and uri: the upstream is Tinyauth's port 3000, and the path for Caddy is /api/auth/caddy. Read docker compose logs tinyauth while you reproduce it.

IP-based rules never match. Tinyauth sees the Docker gateway address instead of the client. Make sure TINYAUTH_AUTH_TRUSTEDPROXIES covers the address the requests arrive from.

Verification + next steps

You're done when:

  • https://auth.example.com shows the login page with a valid certificate;
  • opening a protected app while signed out redirects you there;
  • signing in brings you back to the app;
  • the backup archive is stored off the box.

Then replace whoami with the apps you actually want to protect, add TOTP or an OAuth provider, and write access controls for anything that should be limited to specific users.

Next steps

How to self-host Tinyauth →More self-hosted sso & identity tools →Best VPS for Keycloak →Automatic HTTPS with Caddy →Run Claude Code with Ollama on Your Own VPS →Deploy Coolify on a VPS →How to Deploy Actual Budget on a VPS →How to Deploy AnythingLLM on a VPS →How to Deploy Appwrite on a VPS →How to Deploy Audiobookshelf on a VPS →How to Deploy Authelia on a VPS →How to Deploy authentik on a VPS →How to Deploy Baserow on a VPS →How to Deploy Beszel on a VPS →How to Deploy Bitwarden on a VPS →How to Deploy BookStack on a VPS →How to Deploy CapRover on a VPS →How to Deploy Checkmate on a VPS →How to Deploy Directus on a VPS →How to Deploy docker-mailserver on a VPS →How to Deploy Docmost on a VPS →How to Deploy Dokku on a VPS →How to Deploy Dokploy on a VPS →How to Deploy Firefly III on a VPS →How to Deploy Forgejo on a VPS →How to Deploy Gatus on a VPS →How to Deploy Ghostfolio on a VPS →How to Deploy Gitea on a VPS →How to Deploy GitLab on a VPS →How to Deploy GlitchTip on a VPS →How to Deploy Grafana on a VPS →How to Deploy Graylog on a VPS →How to Deploy Headscale on a VPS →How to Deploy Healthchecks on a VPS →How to Deploy Home Assistant on a VPS →How to Deploy Immich on a VPS →How to Deploy Jan on a VPS →How to Deploy Jellyfin on a VPS →How to Deploy Karakeep on a VPS →How to Deploy Keycloak on a VPS →How to Deploy Leantime on a VPS →How to Deploy LibreChat on a VPS →How to Deploy Linkwarden on a VPS →How to Deploy LocalAI on a VPS →How to Deploy Mailcow on a VPS →How to Deploy Mailu on a VPS →How to Deploy Matomo on a VPS →How to Deploy Mattermost on a VPS →How to Deploy Meilisearch on a VPS →How to Deploy Memos on a VPS →How to Deploy n8n on a VPS →How to Deploy Navidrome on a VPS →How to Deploy NetBird on a VPS →How to Deploy Netdata on a VPS →How to Deploy Nextcloud on a VPS →How to Deploy Next.js to a VPS →How to Deploy Nginx Proxy Manager on a VPS →How to Deploy NocoDB on a VPS →How to Deploy ntfy on a VPS →How to Deploy Ollama on a VPS →How to Deploy Open WebUI on a VPS →How to Deploy OpenHands on a VPS →How to Deploy OpenObserve on a VPS →How to Deploy OpenProject on a VPS →How to Deploy Outline on a VPS →How to Deploy Pangolin on a VPS →How to Deploy Paperless-ngx on a VPS →How to Deploy Passbolt on a VPS →How to Deploy Plane on a VPS →How to Deploy Plausible Analytics on a VPS →How to Deploy Pocket ID on a VPS →How to Deploy PocketBase on a VPS →How to Deploy Prometheus on a VPS →How to Deploy Psono on a VPS →How to Deploy Radarr on a VPS →How to Deploy Rocket.Chat on a VPS →How to Deploy SigNoz on a VPS →How to Deploy Sonarr on a VPS →How to Deploy Stalwart on a VPS →How to Deploy Stirling-PDF on a VPS →How to Deploy Supabase on a VPS →How to Deploy Synapse on a VPS →How to Deploy Taiga on a VPS →How to Deploy TeamPass on a VPS →How to Deploy Traefik on a VPS →How to Deploy Trilium on a VPS →How to Deploy Twenty CRM on a VPS →How to Deploy Umami on a VPS →How to Deploy Uptime Kuma on a VPS →How to Deploy Vaultwarden on a VPS →How to Deploy Vikunja on a VPS →How to Deploy wg-easy on a VPS →How to Deploy Wiki.js on a VPS →How to Deploy Zabbix on a VPS →How to Deploy Zitadel on a VPS →How to Deploy Zulip on a VPS →Docker & Compose on Ubuntu 26.04 →Building AI Workflows with n8n →Install Open WebUI with Ollama →Adding AI-Powered Insights to Plausible Analytics →Building AI-Powered Apps with Supabase and pgvector →

We use analytics cookies (Google Analytics, PostHog) to see which guides are useful. No ad networks, no cross-site tracking. See our privacy policy.