
This article provides a guide demonstrating how to deploy Hanko on Debian VPS.
What is Hanko?
Hanko is an open-source authentication and user management platform for web and mobile apps. In practical terms, it gives developers a ready-made login system instead of making them build authentication from scratch.
It can handle things like:
- Email/password login
- Passwordless login
- Passkeys / WebAuthn
- MFA / 2FA
- Social login / OAuth
- SSO, including SAML in supported setups
- User registration and user management
- Session handling
- Authentication APIs
- Embeddable frontend login components called Hanko Elements
Hanko is designed to be framework-agnostic, meaning it can be used with many frontend and backend stacks. It is a fully open-source authentication and user management system built around a flexible API, with support for passwords, passkeys, 2FA, SSO, and Hanko Elements.
A simple way to think about it:
Hanko is an open-source alternative to services like Auth0, Clerk, Firebase Auth, AWS Cognito, WorkOS, or Stytch, with a strong focus on passkeys and privacy-first authentication.
It can be used in two main ways:
- Self-hosted, where you run Hanko on your own VPS or infrastructure.
- Hanko Cloud, where Hanko hosts and manages it for you.
For VPS/self-hosted users, Hanko is useful when you want to add modern authentication to an app while keeping more control over your deployment, data, and infrastructure.
Hanko is an open-source authentication and user-management platform for modern web and mobile apps. It supports email/password, passcodes, passkeys, MFA, OAuth SSO, SAML SSO, webhooks, Hanko Elements, and API-first authentication flows.
This guide installs Hanko on a Debian VPS using:
- Debian 12
- Docker + Docker Compose
- PostgreSQL
- Nginx reverse proxy
- Let’s Encrypt SSL
- systemd service management
- Production-ready config structure
The official Hanko quickstart uses Docker Compose and builds services locally from the Hanko repository. The upstream quickstart runs with:
docker compose -f deploy/docker-compose/quickstart.yaml -p "hanko-quickstart" up --build
and exposes the example login app on localhost:8888.
For a VPS, we will adapt that pattern into a cleaner production-style deployment.
Example Deployment Layout
In this guide, we will use the following example domains:
auth.example.com Hanko public API auth-admin.example.com Hanko admin API, restricted app.example.com Your application using Hanko
You can use a single hostname for the public API, but separating the admin API is safer.
Replace these values throughout the guide:
auth.example.com auth-admin.example.com app.example.com admin@example.com
Minimum VPS Requirements
Recommended baseline:
- OS: Debian 12
- CPU: 2 vCPU
- RAM: 2 GB minimum, 4 GB preferred
- Disk: 20 GB+
- Access: root or sudo user
- Ports: 22, 80, 443 open
Passkeys/WebAuthn require HTTPS in production, so do not attempt to run production Hanko over plain HTTP.
Compare Debian VPS Plans
How to Deploy Hanko on Debian VPS
To deploy Hanko on Debian VPS, follow the steps outlined below:
-
Update Debian
Log in via SSH to the VPS:
ssh root@your_server_ip
Update the system:
apt update apt upgrade -y apt install -y ca-certificates curl gnupg lsb-release git ufw nano openssl
Set the hostname:
hostnamectl set-hostname hanko01.example.com
Optional but recommended:
reboot
Reconnect after reboot.
-
Create a Non-Root Admin User
adduser deploy usermod -aG sudo deploy
Copy your SSH key to the new user if needed:
rsync --archive --chown=deploy:deploy ~/.ssh /home/deploy
Then log in as the new user:
ssh deploy@your_server_ip
-
Configure Firewall
Allow SSH, HTTP, and HTTPS:
sudo ufw allow OpenSSH sudo ufw allow 80/tcp sudo ufw allow 443/tcp sudo ufw enable sudo ufw status
Do not expose PostgreSQL, Hanko public port
8000, or Hanko admin port8001directly to the internet. -
Install Docker Engine
Install Docker’s official repository:
sudo install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/debian/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg sudo chmod a+r /etc/apt/keyrings/docker.gpg
Add the Docker repo:
echo \ "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \ https://download.docker.com/linux/debian \ $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \ sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
Install Docker:
sudo apt update sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
Allow your user to run Docker:
sudo usermod -aG docker $USER
Log out and back in, then verify:
docker --version docker compose version
-
Install Nginx and Certbot
sudo apt install -y nginx certbot python3-certbot-nginx
Enable Nginx:
sudo systemctl enable --now nginx
Check status:
systemctl status nginx
-
Point DNS Records to the VPS
Create DNS records like this:
A auth.example.com your_server_ip A auth-admin.example.com your_server_ip A app.example.com your_server_ip
Wait until DNS resolves:
dig +short auth.example.com dig +short auth-admin.example.com
-
Create the Hanko Directory
sudo mkdir -p /opt/hanko sudo chown -R deploy:deploy /opt/hanko cd /opt/hanko
Create subdirectories:
mkdir -p config data/postgres logs backups
-
Generate Strong Secrets
Generate a PostgreSQL password:
openssl rand -base64 32
Generate a Hanko secret key:
openssl rand -hex 32
Save both values somewhere secure.
Example placeholders used below:
POSTGRES_PASSWORD=replace_with_strong_postgres_password HANKO_SECRET_KEY=replace_with_64_character_hex_secret
The official sample config uses a simple example secret, but production should use a strong random value. The quickstart config also includes database settings, SMTP settings, secret keys, WebAuthn relying-party origins, session cookie security, CORS origins, and MFA options.
-
Clone the Hanko Repository
Because the official quickstart builds Hanko services locally and notes that services are built before startup, we will clone the source and use Docker Compose locally.
cd /opt git clone https://github.com/teamhanko/hanko.git hanko-src
Check the latest release tags:
cd /opt/hanko-src git tag --sort=-v:refname | head
At the time of review, GitHub listed Hanko
v2.7as the latest release, published June 5, 2026.Checkout a stable tag instead of tracking
main:git checkout v2.7
If you want the latest development code instead:
git checkout main git pull
For production, a pinned release tag is safer.
-
Create the Hanko Config File
Create:
nano /opt/hanko/config/config.yaml
Add the following:
database: user: hanko password: "replace_with_strong_postgres_password" host: postgres port: 5432 database: hanko dialect: postgres email_delivery: smtp: host: "smtp.example.com" port: "587" username: "smtp_user@example.com" password: "replace_with_smtp_password" from_address: "admin@example.com" from_name: "Hanko Authentication" secrets: keys: - "replace_with_64_character_hex_secret" service: name: "Hanko Authentication" webauthn: relying_party: id: "example.com" display_name: "Example App" origins: - "https://app.example.com" - "https://auth.example.com" session: cookie: secure: true same_site: lax domain: ".example.com" mfa: enabled: true optional: true acquire_on_login: false acquire_on_registration: true device_trust_policy: "prompt" device_trust_duration: "720h" device_trust_cookie_name: "hanko-device-token" device_trust_max_users_per_device: 20 totp: enabled: true security_keys: enabled: true server: public: address: "0.0.0.0:8000" cors: allow_origins: - "https://app.example.com" - "https://auth.example.com" admin: address: "0.0.0.0:8001" security_notifications: notifications: email_create: enabled: true email_delete: enabled: true password_update: enabled: true passkey_create: enabled: true primary_email_update: enabled: true mfa_create: enabled: true mfa_delete: enabled: trueImportant notes:
webauthn.relying_party.idshould usually be the registrable domain, such asexample.com.webauthn.relying_party.originsmust include the frontend origins that use Hanko.server.public.cors.allow_originsmust include your application URL.- Use HTTPS origins in production.
session.cookie.securemust betruebehind HTTPS.- Hanko’s troubleshooting docs note that CORS errors usually happen when the app URL is not configured correctly.
Secure the config file:
chmod 600 /opt/hanko/config/config.yaml
-
Create the Docker Compose File
Create:
nano /opt/hanko/docker-compose.yml
Add:
services: postgres: image: postgres:16-alpine container_name: hanko-postgres restart: unless-stopped environment: POSTGRES_USER: hanko POSTGRES_PASSWORD: replace_with_strong_postgres_password POSTGRES_DB: hanko volumes: - ./data/postgres:/var/lib/postgresql/data networks: - hanko-internal healthcheck: test: ["CMD-SHELL", "pg_isready -U hanko -d hanko"] interval: 10s timeout: 5s retries: 5 hanko-migrate: build: context: /opt/hanko-src/backend container_name: hanko-migrate command: --config /etc/config/config.yaml migrate up restart: "no" depends_on: postgres: condition: service_healthy volumes: - ./config/config.yaml:/etc/config/config.yaml:ro networks: - hanko-internal hanko: build: context: /opt/hanko-src/backend container_name: hanko-api command: serve --config /etc/config/config.yaml all restart: unless-stopped depends_on: hanko-migrate: condition: service_completed_successfully volumes: - ./config/config.yaml:/etc/config/config.yaml:ro ports: - "127.0.0.1:8000:8000" - "127.0.0.1:8001:8001" networks: - hanko-internal networks: hanko-internal: driver: bridgeReplace the PostgreSQL password with the same value used in
config.yaml.This Compose file intentionally binds Hanko only to
127.0.0.1, so only Nginx on the VPS can proxy to it. -
Start Hanko
From
/opt/hanko:cd /opt/hanko docker compose build docker compose up -d
Check containers:
docker compose ps
Check logs:
docker compose logs -f hanko
Test locally from the VPS:
curl -i http://127.0.0.1:8000/.well-known/config
You should receive a JSON response. Hanko documents the public configuration endpoint at
/.well-known/config, which is useful for checking available frontend auth options. -
Configure Nginx for the Public Hanko API
Create:
sudo nano /etc/nginx/sites-available/hanko-public.conf
Add:
server { listen 80; server_name auth.example.com; location / { proxy_pass http://127.0.0.1:8000; proxy_http_version 1.1; 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_read_timeout 300; proxy_connect_timeout 300; proxy_send_timeout 300; } }Enable it:
sudo ln -s /etc/nginx/sites-available/hanko-public.conf /etc/nginx/sites-enabled/hanko-public.conf
Test Nginx:
sudo nginx -t sudo systemctl reload nginx
-
Configure Nginx for the Admin API
The admin API is sensitive. Do not leave it publicly open.
Create a basic auth password file:
sudo apt install -y apache2-utils sudo htpasswd -c /etc/nginx/.hanko-admin.htpasswd admin
Create the admin Nginx config:
sudo nano /etc/nginx/sites-available/hanko-admin.conf
Add:
server { listen 80; server_name auth-admin.example.com; auth_basic "Restricted Hanko Admin API"; auth_basic_user_file /etc/nginx/.hanko-admin.htpasswd; location / { proxy_pass http://127.0.0.1:8001; proxy_http_version 1.1; 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_read_timeout 300; proxy_connect_timeout 300; proxy_send_timeout 300; } }Enable it:
sudo ln -s /etc/nginx/sites-available/hanko-admin.conf /etc/nginx/sites-enabled/hanko-admin.conf sudo nginx -t sudo systemctl reload nginx
For higher security, restrict by IP as well:
allow your_office_ip; deny all;
Place those lines inside the
serverblock or relevantlocationblock. -
Issue Let’s Encrypt SSL Certificates
Run:
sudo certbot --nginx -d auth.example.com
Then:
sudo certbot --nginx -d auth-admin.example.com
Choose the redirect-to-HTTPS option when prompted.
Test renewal:
sudo certbot renew --dry-run
-
Verify Public API Over HTTPS
Run:
curl -i https://auth.example.com/.well-known/config
Expected result:
HTTP/2 200 content-type: application/json
You can also test from your local workstation:
curl https://auth.example.com/.well-known/config
-
Add Hanko Elements to a Frontend App
Hanko Elements are Web Components that provide registration, login, and profile settings functionality.
A simple HTML test page can look like this:
<html> <head> <meta charset="utf-8"> <title>Hanko Login</title> <script type="module"> import { register } from "https://cdn.jsdelivr.net/npm/@teamhanko/hanko-elements/dist/elements.js"; await register("https://auth.example.com"); </script> </head> <body> <h1>Login</h1> <hanko-auth></hanko-auth> </body> </html>In a real app, use the official quickstart for your framework. Hanko provides quickstarts for Next.js, Nuxt, Remix, SvelteKit, Angular, JavaScript, React, Solid, Svelte, Vue, and several backend languages.
-
Validate a Hanko Session in Your Backend
After authentication, Hanko issues a session token. Hanko’s backend quickstart documentation explains that apps should retrieve the Hanko session token and validate it against the Hanko backend.
Conceptually:
Browser logs in through Hanko Hanko sets session cookie Your app receives authenticated request Your backend validates the session token with Hanko Your backend allows or denies the protected action
Do not trust frontend-only login state for protected actions.
-
Create a systemd Service for Hanko
Docker Compose already restarts containers, but a systemd wrapper makes boot behavior cleaner.
Create:
sudo nano /etc/systemd/system/hanko.service
Add:
[Unit] Description=Hanko Authentication Service Requires=docker.service After=docker.service network-online.target Wants=network-online.target [Service] Type=oneshot RemainAfterExit=yes WorkingDirectory=/opt/hanko ExecStart=/usr/bin/docker compose up -d ExecStop=/usr/bin/docker compose down TimeoutStartSec=0 [Install] WantedBy=multi-user.target
Enable it:
sudo systemctl daemon-reload sudo systemctl enable hanko sudo systemctl start hanko
Check:
systemctl status hanko docker compose -f /opt/hanko/docker-compose.yml ps
-
Configure SMTP Properly
Hanko needs email delivery for passcodes, verification, and security notifications.
Update this block in
/opt/hanko/config/config.yaml:email_delivery: smtp: host: "smtp.example.com" port: "587" username: "smtp_user@example.com" password: "replace_with_smtp_password" from_address: "admin@example.com" from_name: "Hanko Authentication"Then restart:
cd /opt/hanko docker compose up -d --force-recreate hanko
Check logs:
docker compose logs -f hanko
Make sure the sending domain has SPF, DKIM, and DMARC configured.
-
Run a Registration Test
Open your app using Hanko Elements:
https://app.example.com/login
Test:
- Register a new email address.
- Confirm that the email/passcode arrives.
- Complete login.
- Add a passkey.
- Log out.
- Log back in with the passkey.
- Confirm your app backend validates the Hanko session token.
If you receive CORS errors, verify these values:
webauthn: relying_party: origins: - "https://app.example.com" server: public: cors: allow_origins: - "https://app.example.com"Also confirm your frontend is using:
https://auth.example.com
not:
http://auth.example.com
and not:
http://127.0.0.1:8000
-
Back Up Hanko
Back up:
/opt/hanko/config/config.yaml /opt/hanko/data/postgres
Create a database dump script:
nano /opt/hanko/backups/backup-hanko.sh
Add:
#!/bin/bash set -euo pipefail BACKUP_DIR="/opt/hanko/backups" DATE="$(date +%F-%H%M%S)" cd /opt/hanko docker compose exec -T postgres pg_dump -U hanko hanko > "$BACKUP_DIR/hanko-$DATE.sql" find "$BACKUP_DIR" -type f -name "hanko-*.sql" -mtime +14 -delete
Make it executable:
chmod +x /opt/hanko/backups/backup-hanko.sh
Test:
/opt/hanko/backups/backup-hanko.sh ls -lh /opt/hanko/backups
Add a daily cron job:
crontab -e
Add:
15 2 * * * /opt/hanko/backups/backup-hanko.sh >/opt/hanko/logs/backup.log 2>&1
-
Updating Hanko
Go to the source directory:
cd /opt/hanko-src git fetch --tags git tag --sort=-v:refname | head
Checkout the desired release:
git checkout vX.Y.Z
Rebuild and restart:
cd /opt/hanko docker compose build --no-cache docker compose up -d
Watch logs:
docker compose logs -f hanko-migrate hanko
Check the public config endpoint again:
curl https://auth.example.com/.well-known/config
-
Useful Management Commands
View containers:
cd /opt/hanko docker compose ps
View logs:
docker compose logs -f
Restart Hanko:
docker compose restart hanko
Restart everything:
docker compose down docker compose up -d
Re-run migrations:
docker compose run --rm hanko-migrate
Check PostgreSQL:
docker compose exec postgres psql -U hanko -d hanko
Stop service:
sudo systemctl stop hanko
Start service:
sudo systemctl start hanko
-
Recommended Production Hardening
Before using Hanko for a live application:
- Use a pinned Hanko release tag, not
main. - Keep
/opt/hanko/config/config.yamlreadable only by the deployment user. - Do not expose PostgreSQL publicly.
- Do not expose port
8001publicly without IP restrictions and authentication. - Use HTTPS everywhere.
- Configure valid SMTP with SPF, DKIM, and DMARC.
- Keep
webauthn.relying_party.originslimited to known frontend URLs. - Keep
server.public.cors.allow_originslimited to known frontend URLs. - Back up PostgreSQL daily.
- Test restore procedures.
- Monitor logs for login, email, SMTP, and migration errors.
- Use a firewall.
- Consider fail2ban or upstream WAF/rate limiting.
- Keep Debian and Docker patched.
- Use a pinned Hanko release tag, not
-
Troubleshooting
-
Hanko API returns 502 through Nginx
Check whether Hanko is running:
cd /opt/hanko docker compose ps docker compose logs hanko
Check local API:
curl -i http://127.0.0.1:8000/.well-known/config
If local curl fails, the issue is Hanko. If local curl works, the issue is Nginx.
-
CORS errors in browser
Confirm:
server: public: cors: allow_origins: - "https://app.example.com"Then restart:
cd /opt/hanko docker compose restart hanko
Hanko’s own troubleshooting docs specifically call out incorrect app/API URL configuration as a common cause of CORS and 404 issues.
-
Passkey registration fails
Check:
webauthn: relying_party: id: "example.com" origins: - "https://app.example.com" - "https://auth.example.com"Also verify:
- You are using HTTPS.
- The browser supports passkeys/WebAuthn.
- The frontend origin exactly matches the configured origin.
- There is no mismatch between
wwwand non-www.
-
Emails do not arrive
Check logs:
cd /opt/hanko docker compose logs -f hanko
Verify SMTP credentials:
openssl s_client -starttls smtp -connect smtp.example.com:587
Also check SPF, DKIM, DMARC, and provider rate limits.
-
Database migration fails
View migration logs:
cd /opt/hanko docker compose logs hanko-migrate
Check PostgreSQL health:
docker compose logs postgres docker compose exec postgres pg_isready -U hanko -d hanko
-
-
Final Verification Checklist
Run these checks before production use:
curl -I https://auth.example.com/.well-known/config
Confirm:
HTTP/2 200
Check running containers:
cd /opt/hanko docker compose ps
Confirm Nginx:
sudo nginx -t systemctl status nginx
Confirm Certbot renewal:
sudo certbot renew --dry-run
Confirm firewall:
sudo ufw status
Confirm no public Hanko ports are exposed:
ss -tulpn | grep -E '8000|8001|5432'
Expected bindings should be local-only for Hanko ports:
127.0.0.1:8000 127.0.0.1:8001
Summary
You now know how to deploy Hanko on Debian VPS and have Hanko running with PostgreSQL, Docker Compose, Nginx, SSL, and a production-oriented configuration. The public Hanko API is available through:
https://auth.example.com
Your frontend app should use that URL as the Hanko API URL, while your backend should validate Hanko-issued session tokens before allowing protected actions. Hanko’s official documentation provides framework-specific quickstarts for frontend and backend integration once the self-hosted API is live.









