Hanko on Ubuntu 24.04 on Azure User Guide
Overview
This image runs Hanko 2.7.0, the open source authentication backend from teamhanko, on Ubuntu 24.04 LTS. Hanko lets you add modern, passwordless sign in to your applications through a clean REST API: passkeys and security keys (FIDO2 and WebAuthn), email passcodes, passwords, multi factor authentication and social login. It handles registration, sign in, credential management and account recovery, and issues signed session tokens your application verifies against a published JSON Web Key Set.
The pinned upstream backend is run as the official ghcr.io/teamhanko/hanko container under a systemd service that starts it on boot and restarts it on failure, backed by a local PostgreSQL 16 database on the same VM. nginx sits in front as a TLS ready entry point and serves the public authentication API on port 80. The administrative API is bound to loopback only (port 8001) and is never exposed on the network, as the Hanko project intends.
What is included:
- Hanko 2.7.0 (official
ghcr.io/teamhanko/hankobackend image, pinned by digest), run by systemd - The public authentication API behind nginx on
:80 - The administrative API on
127.0.0.1:8001(loopback only) - A per-VM PostgreSQL password and a per-VM session signing key generated on first boot and recorded in a root-only file
- Database migrations applied automatically on first boot
- PostgreSQL 16, bound to loopback, holding users, credentials and sessions
postgresql.service,docker.service,nginx.serviceandhanko.serviceas systemd units, enabled and active- No administrator baked into the image: the first user you register becomes your admin
- 24/7 cloudimg support
Prerequisites
An active Azure subscription, an SSH key pair, and a VNet plus subnet in the target region. Standard_B2s (2 vCPU / 4 GiB RAM) is a sensible starting point; size up for heavier use. NSG inbound: allow 22/tcp from your management network and 80/tcp for the public authentication API. Do not expose port 8001 - the administrative API is intentionally loopback only. Hanko serves plain HTTP on port 80; for production, front it with your own domain and TLS (see Production hardening).
Step 1 - Deploy from the Azure Marketplace
Sign in to the Azure Portal, choose Create a resource, search the Marketplace for Hanko by cloudimg, and select Create. On Basics pick your subscription, resource group, region and size; under Administrator account choose SSH public key and paste your key; under Inbound port rules allow SSH (22) and HTTP (80). Then Review + create then Create.
Step 2 - Deploy from the Azure CLI
az vm create \
--resource-group <your-rg> \
--name hanko \
--image <marketplace-image-urn> \
--size Standard_B2s \
--admin-username azureuser \
--ssh-key-values ~/.ssh/id_ed25519.pub \
--vnet-name <your-vnet> --subnet <your-subnet> \
--public-ip-sku Standard
az vm open-port --resource-group <your-rg> --name hanko --port 80 --priority 1010
Step 3 - Connect to your VM
ssh azureuser@<vm-public-ip>
Step 4 - Confirm the services are running
The backend runs as the hanko systemd service (a host-networked ghcr.io/teamhanko/hanko container) and talks to PostgreSQL on 127.0.0.1:5432. nginx publishes the authentication API on port 80. Confirm all four services are active and check the listeners - only nginx (:80) is on all interfaces; the Hanko public (:8000) and admin (:8001) APIs are on loopback:
systemctl is-active postgresql docker nginx hanko
sudo ss -tlnp | grep -E ':80 |:8000 |:8001 ' | awk '{print $4}' | sort -u
Expected output:
active
active
active
active
0.0.0.0:80
127.0.0.1:8000
127.0.0.1:8001
[::]:80

Step 5 - Retrieve your per-VM secrets
On the first boot of every VM, hanko-firstboot.service generates a unique PostgreSQL password and a unique session signing key, creates the database, applies migrations, and writes the details into a root-only file. Read it with:
sudo cat /root/hanko-credentials.txt
There is no default or shared credential in the image: Hanko ships with no administrator baked in, and the database password and session key are unique to this VM. The first user you register (through your application or the admin API) becomes your organization administrator. Store these secrets somewhere safe.

Step 6 - Check the public authentication API
The public API is served through nginx on port 80. Confirm the health endpoints return 200, look at the enabled sign-in methods, and fetch the JSON Web Key Set your application uses to verify Hanko's session tokens:
curl -s -o /dev/null -w '/health/ready : HTTP %{http_code}\n' http://127.0.0.1/health/ready
curl -s -o /dev/null -w '/health/alive : HTTP %{http_code}\n' http://127.0.0.1/health/alive
curl -s http://127.0.0.1/.well-known/config | python3 -m json.tool | sed -n '1,10p'
curl -s http://127.0.0.1/.well-known/jwks.json | python3 -c 'import sys,json;k=json.load(sys.stdin)["keys"][0];print("jwks kid:",k["kid"],"alg:",k["alg"])'
The /.well-known/config response advertises which sign-in methods are enabled (passwords are on by default; passkeys and email passcodes are configurable), and /.well-known/jwks.json publishes the per-VM public key your backend uses to validate the JWTs Hanko issues.

Step 7 - Create a user through the admin API
The administrative API on 127.0.0.1:8001 manages users directly. It is deliberately bound to loopback with no network authentication, so run these commands on the VM (over SSH) and never expose port 8001. Create a user and read it back:
EMAIL="alice-$(date +%s)@example.com"
RESP=$(curl -s -X POST http://127.0.0.1:8001/users -H 'Content-Type: application/json' \
-d "{\"emails\":[{\"address\":\"${EMAIL}\",\"is_primary\":true,\"is_verified\":true}]}")
echo "$RESP" | python3 -m json.tool | sed -n '1,10p'
NEW_ID=$(echo "$RESP" | python3 -c 'import sys,json;print(json.load(sys.stdin)["id"])')
curl -s -o /dev/null -w 'read back: HTTP %{http_code}\n' http://127.0.0.1:8001/users/${NEW_ID}
The create call returns the new user's id and email, and reading it back returns HTTP 200 - proving the database, migrations and server are all working end to end.

Step 8 - Wire Hanko into your application
Hanko is an authentication backend. To give your users a sign-in screen, embed the official Hanko login web component (@teamhanko/hanko-elements, MIT licensed) in your own frontend and point it at this server's public API. A minimal example:
<script type="module">
import { register } from "https://esm.run/@teamhanko/hanko-elements";
await register("http://<vm-public-ip>"); // your Hanko public API URL
</script>
<hanko-auth></hanko-auth>
The <hanko-auth> element renders registration and sign in (passkeys, passcodes and passwords per your configuration). After a user signs in, your backend validates the session JWT against http://<vm-public-ip>/.well-known/jwks.json. See the Hanko docs for the frontend SDK and framework-specific examples.
Step 9 - Production hardening
The image ships the fully functional authentication server. For production:
- TLS and DNS: front Hanko with your own domain and a TLS-terminating reverse proxy or Azure Application Gateway. nginx on the VM already proxies port 80 to the backend; add your certificate to the nginx site at
/etc/nginx/sites-available/hanko, or terminate TLS upstream. - Relying party domain: passkeys are bound to a domain. Set
webauthn.relying_party.idandwebauthn.relying_party.originsin/etc/hanko/config.yamlto your production domain, thensudo systemctl restart hanko. By default the relying party id islocalhostso the server always starts. - Email passcodes: configure SMTP under
email_deliveryin/etc/hanko/config.yamlto enable email passcode and verification delivery (disabled by default because no mail server is shipped). - Social login and SAML: enable Google, GitHub, Apple, Microsoft and other providers, or SAML, under
third_party/samlin/etc/hanko/config.yaml.
Maintenance
- Back up your data: all authentication state - users, credentials and sessions - lives in the local
hankoPostgreSQL database. Snapshot the OS disk, or usepg_dump hankofor logical backups. - Public origins: on every boot
hanko-configure.servicere-renders/etc/hanko/config.yamlso the CORS and WebAuthn origins track the VM's current public IP. For a stable hostname, set your domain in the config as described above. - Security updates: unattended security upgrades are enabled, so the OS keeps itself patched. Reboot when a new kernel is installed.
- Upgrades: the backend image is pinned for reproducibility; pull a newer
ghcr.io/teamhanko/hankotag and update the tag in/etc/systemd/system/hanko.servicewhen you choose to upgrade.
Source code (AGPL-3.0)
Hanko's backend is free software licensed under the GNU Affero General Public License v3.0 (AGPL-3.0). This image runs the stock upstream backend, unmodified, configured through external files only. In accordance with AGPL section 13, the complete corresponding source for the exact version deployed here is published by the upstream project at github.com/teamhanko/hanko (tag backend/v2.7.0). The Hanko login web components (hanko-elements, hanko-frontend-sdk) are separately licensed under the MIT License.
Support
cloudimg provides 24/7/365 expert technical support for this image. Contact support@cloudimg.co.uk. Hanko's backend is licensed under the AGPL-3.0 License; the corresponding source is linked above. This image is provided by cloudimg; additional charges apply for build, maintenance and 24/7 support.