How to Deploy Tinyauth on a VPS
Updated Sep 2026
verified on Ubuntu 26.04 · Sep 2026Put 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.
- 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)
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.
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_APPURLis the public URL of the login page, and the domain its cookie is set for. Replaceauth.example.comwith your own.TINYAUTH_AUTH_SECURECOOKIEmarks the session cookie HTTPS-only. It defaults tofalse. Turn it on as soon as TLS is in front.TINYAUTH_AUTH_TRUSTEDPROXIEStells Tinyauth which addresses may setX-Forwarded-ForandX-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, and172.16.0.0/12covers Docker's default address pools. Narrow it if you know your network's subnet.TINYAUTH_DATABASE_PATHkeeps Tinyauth's SQLite file on the./datavolume, 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 generateadds a TOTP secret to an existingusername:hashentry (run it with--interactivefrom the image). Replace the value in.envand rundocker 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.comshows 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.