Dev.to Security 🔐 Cybersecurity 👁 1 📖 4 min read

Deploying Ory Kratos Open-Source Identity and User Management System on Ubuntu 24.04

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 Comp

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.1 on 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=[email protected]
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:4434 only — used by tooling on the host.
  • The DSN points at the postgres service (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.

📰 Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.