Ory Kratos is an open-source, API-first identity and user management system handling registration, login, recovery, verification, and session management with a self-service UI. This guide deploys Kratos using Docker Compose with PostgreSQL, the self-service UI Node, and Traefik handling automatic HTTPS for the public API. By the end, you'll have Kratos managing identities and sessions for users registering through your domain over HTTPS.
Prerequisite: SMTP credentials are required for verification and recovery emails. The admin API stays bound to
127.0.0.1on purpose — never expose it publicly.
Set Up the Directory Structure
1. Create the project directories:
$ mkdir -p ~/ory-kratos/{config,data/postgres}
$ cd ~/ory-kratos
2. Create the environment file:
$ nano .env
DOMAIN=kratos.example.com
LETSENCRYPT_EMAIL=admin@example.com
KRATOS_VERSION=v26.2.0
POSTGRES_USER=kratos
POSTGRES_PASSWORD=EXAMPLE_DB_PASSWORD
POSTGRES_DB=kratosdb
LOG_LEVEL=info
3. Create the identity schema — defines the user fields (email + name) and how the email maps to login, recovery, and verification:
$ nano config/identity.schema.json
Use the schema described in the Vultr Docs walkthrough — email is the login identifier with password auth; name is a free-text trait.
4. Create the Kratos configuration — public/admin API URLs, password policy (12-char minimum + HaveIBeenPwned), session lifetimes, self-service flows, SMTP courier:
$ nano config/kratos.yml
Fill in the full configuration from the source article. Key points to keep consistent with the stack below:
- Public API listens on the internal port and is fronted by Traefik on
${DOMAIN}. - Admin API listens on
127.0.0.1:4434only — used by tooling on the host. - The DSN points at the
postgresservice (postgres://kratos:...@postgres:5432/kratosdb?sslmode=disable). - The courier section uses your SMTP provider for verification mail.
Deploy with Docker Compose
1. Create the Compose manifest:
$ nano docker-compose.yml
services:
traefik:
image: traefik:v3.6
container_name: traefik
command:
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
- "--entrypoints.web.http.redirections.entrypoint.to=websecure"
- "--entrypoints.web.http.redirections.entrypoint.scheme=https"
- "--certificatesresolvers.letsencrypt.acme.httpchallenge=true"
- "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web"
- "--certificatesresolvers.letsencrypt.acme.email=${LETSENCRYPT_EMAIL}"
- "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
ports:
- "80:80"
- "443:443"
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro"
- "./letsencrypt:/letsencrypt"
restart: unless-stopped
postgres:
image: postgres:16
container_name: kratos-postgres
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
volumes:
- ./data/postgres:/var/lib/postgresql/data
healthcheck:
test: ["CMD", "pg_isready", "-U", "${POSTGRES_USER}", "-d", "${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
kratos-migrate:
image: oryd/kratos:${KRATOS_VERSION}
container_name: kratos-migrate
depends_on:
postgres:
condition: service_healthy
volumes:
- ./config:/etc/config/kratos:ro
environment:
DSN: "postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}?sslmode=disable"
command: migrate -c /etc/config/kratos/kratos.yml sql -e --yes
kratos:
image: oryd/kratos:${KRATOS_VERSION}
container_name: kratos
depends_on:
kratos-migrate:
condition: service_completed_successfully
volumes:
- ./config:/etc/config/kratos:ro
environment:
DSN: "postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}?sslmode=disable"
LOG_LEVEL: ${LOG_LEVEL}
command: serve -c /etc/config/kratos/kratos.yml --watch-courier
ports:
- "127.0.0.1:4434:4434"
expose:
- "4433"
labels:
- "traefik.enable=true"
- "traefik.http.routers.kratos.rule=Host(`${DOMAIN}`) && PathPrefix(`/.ory/kratos/public`)"
- "traefik.http.routers.kratos.entrypoints=websecure"
- "traefik.http.routers.kratos.tls.certresolver=letsencrypt"
- "traefik.http.services.kratos.loadbalancer.server.port=4433"
restart: unless-stopped
kratos-ui:
image: oryd/kratos-selfservice-ui-node:${KRATOS_VERSION}
container_name: kratos-ui
depends_on:
- kratos
environment:
KRATOS_PUBLIC_URL: https://${DOMAIN}/.ory/kratos/public
KRATOS_BROWSER_URL: https://${DOMAIN}/.ory/kratos/public
expose:
- "3000"
labels:
- "traefik.enable=true"
- "traefik.http.routers.kratos-ui.rule=Host(`${DOMAIN}`)"
- "traefik.http.routers.kratos-ui.entrypoints=websecure"
- "traefik.http.routers.kratos-ui.tls.certresolver=letsencrypt"
- "traefik.http.services.kratos-ui.loadbalancer.server.port=3000"
restart: unless-stopped
2. Start the stack:
$ docker compose up -d
$ docker compose ps -a
$ docker compose logs
Health Checks
1. Probe the admin API on the loopback:
$ docker compose exec kratos wget -qO- http://127.0.0.1:4434/health/alive
2. Probe the public API over HTTPS:
$ curl -s https://kratos.example.com/.ory/kratos/public/health/alive
3. Confirm the self-service UI loads:
$ curl -s -o /dev/null -w "%{http_code}" https://kratos.example.com/
Register and Sign In via the API
1. Open a registration flow:
$ curl -s https://kratos.example.com/.ory/kratos/public/self-service/registration/api
Capture the id from the response — it's the FLOW_ID below.
2. Submit the registration:
$ curl -s -X POST \
"https://kratos.example.com/.ory/kratos/public/self-service/registration?flow=FLOW_ID" \
-H 'Content-Type: application/json' \
-d '{
"method": "password",
"password": "YOUR-PASSWORD",
"traits": {
"email": "YOUR-EMAIL",
"name": { "first": "FIRST-NAME", "last": "LAST-NAME" }
}
}'
3. List identities through the admin API:
$ docker compose exec kratos wget -qO- http://127.0.0.1:4434/admin/identities
4. Sign in:
$ curl -s https://kratos.example.com/.ory/kratos/public/self-service/login/api
$ curl -s -X POST \
"https://kratos.example.com/.ory/kratos/public/self-service/login?flow=FLOW_ID" \
-H 'Content-Type: application/json' \
-d '{
"method": "password",
"identifier": "YOUR-EMAIL",
"password": "YOUR-PASSWORD"
}'
5. Verify the session:
$ curl -s https://kratos.example.com/.ory/kratos/public/sessions/whoami \
-H "X-Session-Token: SESSION_TOKEN"
Next Steps
Kratos is running with PostgreSQL persistence, the self-service UI, and HTTPS in front of the public API. From here you can:
- Add social sign-in (Google, GitHub, Apple) through the OIDC provider list
- Wire Kratos sessions to other services via Ory Oathkeeper or forward-auth
- Use Ory Hydra alongside Kratos to expose OpenID Connect to client apps
For the full guide with additional tips, visit the original article on Vultr Docs.
Top comments (0)