How to Deploy Ory Kratos – Open-Source Identity and User Management System

Ory Kratos is an open-source identity and user management system designed for cloud-native environments. It handles user registration, login, account recovery, email verification, and profile management through a headless Application Programming Interface (API)-first architecture. Unlike traditional identity platforms that bundle a built-in user interface, Kratos exposes RESTful APIs that you connect to your own frontend. This separation gives you full control over the user experience while Kratos manages the underlying identity logic and security.
This article explains how to deploy an Ory Kratos instance on a Linux server using Docker Compose, configure Traefik for automatic HTTPS with Let's Encrypt, and verify the deployment by registering and authenticating a test user.
Prerequisites
Before you begin, you need to:
- Have access to a Linux-based server (with at least 2 CPU cores and 4 GB of RAM) as a non-root user with sudo privileges.
- Install Docker and Docker Compose.
- Create a DNS A record pointing to your server's IP address (for example,
kratos.example.com). - Have SMTP credentials from an email provider for sending verification and recovery emails.
Set Up the Directory Structure, Configuration, and Environment Variables
Ory Kratos requires a project directory containing configuration files for the identity schema and server settings. Environment variables store sensitive credentials separately from configuration files.
Create the project directory with all required subdirectories.
console$ mkdir -p ~/ory-kratos/{config,data/postgres}
This command creates the following directory structure:
config/: Stores Kratos configuration files and identity schemas.data/postgres/: Persists PostgreSQL database files across container restarts.
Navigate to the project directory.
console$ cd ~/ory-kratos
Create the identity schema file. This JSON schema defines which fields are available on user accounts and how those fields map to authentication credentials.
console$ nano config/identity.schema.json
Add the following content.
json{ "$id": "https://schemas.ory.sh/presets/kratos/quickstart/email-password/identity.schema.json", "$schema": "http://json-schema.org/draft-07/schema#", "title": "Person", "type": "object", "properties": { "traits": { "type": "object", "properties": { "email": { "type": "string", "format": "email", "title": "Email", "ory.sh/kratos": { "credentials": { "password": { "identifier": true } }, "recovery": { "via": "email" }, "verification": { "via": "email" } } }, "name": { "type": "object", "properties": { "first": { "title": "First Name", "type": "string" }, "last": { "title": "Last Name", "type": "string" } } } }, "required": ["email"], "additionalProperties": false } } }
Save and close the file.
This schema defines an identity with an email address that serves as the login identifier, along with optional first and last name fields. The
ory.sh/kratosannotations configure the email field for password-based authentication, account recovery, and email verification.Generate two random secrets for cookie and cipher encryption.
console$ openssl rand -hex 16
Run this command twice and copy both output strings. You use these values in the configuration file.
Create the Kratos configuration file.
console$ nano config/kratos.yml
Add the following content. Replace
kratos.example.comwith your domain name,YOUR_GENERATED_SECRETandYOUR_GENERATED_CIPHER_SECRETwith the random strings from the previous command, and update the SMTP section with your email provider's credentials.yamlversion: v0.13.0 serve: public: base_url: https://kratos.example.com/.ory/kratos/public/ cors: enabled: true allowed_origins: - https://kratos.example.com allowed_methods: - POST - GET - PUT - PATCH - DELETE allowed_headers: - Authorization - Cookie - Content-Type exposed_headers: - Content-Type - Set-Cookie allow_credentials: true admin: base_url: http://127.0.0.1:4434/ host: 127.0.0.1 selfservice: default_browser_return_url: https://kratos.example.com/ allowed_return_urls: - https://kratos.example.com methods: password: enabled: true config: min_password_length: 12 haveibeenpwned_enabled: true identifier_similarity_check_enabled: true flows: error: ui_url: https://kratos.example.com/error settings: ui_url: https://kratos.example.com/settings privileged_session_max_age: 15m recovery: enabled: true ui_url: https://kratos.example.com/recovery use: code verification: enabled: true ui_url: https://kratos.example.com/verification use: code after: default_browser_return_url: https://kratos.example.com/ logout: after: default_browser_return_url: https://kratos.example.com/login login: ui_url: https://kratos.example.com/login lifespan: 10m registration: lifespan: 10m ui_url: https://kratos.example.com/registration after: password: hooks: - hook: session - hook: show_verification_ui log: level: info format: text leak_sensitive_values: false secrets: cookie: - YOUR_GENERATED_SECRET cipher: - YOUR_GENERATED_CIPHER_SECRET ciphers: algorithm: xchacha20-poly1305 hashers: algorithm: bcrypt bcrypt: cost: 8 identity: default_schema_id: default schemas: - id: default url: file:///etc/config/kratos/identity.schema.json session: cookie: domain: kratos.example.com same_site: Lax courier: smtp: connection_uri: smtps://username:password@smtp.example.com:465/ from_address: noreply@example.com from_name: Ory Kratos feature_flags: use_continue_with_transitions: true
Save and close the file.
The configuration file controls how Kratos operates. The
serve.publicsection sets the public API base URL and Cross-Origin Resource Sharing (CORS) settings. Theserve.adminsection binds the admin API to the loopback address, restricting it from external access. Theselfservicesection enables password-based authentication with a minimum password length of 12 characters, breach database checking via HaveIBeenPwned, and automatic session creation after registration.The admin APINotebase_urluseshttp://127.0.0.1:4434/intentionally. The admin API provides unrestricted access to all identities and sessions, and should never be exposed through the public reverse proxy.Create the environment variables file.
console$ nano .env
Add the following content. Replace
EXAMPLE_DB_PASSWORDwith a strong, unique database password andadmin@example.comwith your email address for Let's Encrypt notifications.iniDOMAIN=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
Save and close the file.
Deploy with Docker Compose
Docker Compose orchestrates the Kratos server, PostgreSQL database, self-service UI, and Traefik reverse proxy as a single deployment. Traefik automatically provisions TLS certificates from Let's Encrypt.
Create the Docker Compose file.
console$ nano docker-compose.yml
Add the following content. Replace
kratos.example.comwith your domain name, and replaceyourCookieSecret1234andyourCsrfSecret1234with strong random values generated usingopenssl rand -hex 16.yamlservices: traefik: image: traefik:v3.7.0 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-alpine 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-SHELL", "pg_isready -U ${POSTGRES_USER}"] interval: 10s retries: 5 restart: unless-stopped kratos-migrate: image: oryd/kratos:${KRATOS_VERSION} container_name: kratos-migrate environment: - DSN=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}?sslmode=disable command: migrate sql -e --yes depends_on: postgres: condition: service_healthy restart: on-failure kratos: image: oryd/kratos:${KRATOS_VERSION} container_name: kratos ports: - "127.0.0.1:4434:4434" 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 volumes: - ./config:/etc/config/kratos depends_on: - kratos-migrate 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" - "traefik.http.middlewares.kratos-strip.stripprefix.prefixes=/.ory/kratos/public" - "traefik.http.routers.kratos.middlewares=kratos-strip" restart: unless-stopped kratos-selfservice-ui-node: image: oryd/kratos-selfservice-ui-node:${KRATOS_VERSION} container_name: kratos-ui environment: - KRATOS_PUBLIC_URL=http://kratos:4433/ - KRATOS_BROWSER_URL=https://kratos.example.com/.ory/kratos/public/ - COOKIE_SECRET=yourCookieSecret1234 - CSRF_COOKIE_NAME=__HOST-csrf - CSRF_COOKIE_SECRET=yourCsrfSecret1234 - PORT=4455 - SECURITY_MODE= depends_on: - kratos labels: - "traefik.enable=true" - "traefik.http.routers.ui.rule=Host(`${DOMAIN}`)" - "traefik.http.routers.ui.entrypoints=websecure" - "traefik.http.routers.ui.tls.certresolver=letsencrypt" - "traefik.http.services.ui.loadbalancer.server.port=4455" - "traefik.http.routers.ui.priority=1" restart: unless-stopped
Save and close the file.
In the above manifest:
services: Launches five containers managed by Docker Compose:traefik: Serves as the reverse proxy and TLS termination point with automatic Let's Encrypt certificate provisioning.postgres: Stores Kratos identity data in a persistent PostgreSQL 16 database.kratos-migrate: Runs database migrations on startup and exits after completion.kratos: Runs the main Ory Kratos identity server.kratos-selfservice-ui-node: Provides the browser-based UI for registration, login, and account management.
ports (kratos): Binds the admin API to127.0.0.1:4434for local access only, preventing external exposure.labels (kratos): Routes requests matching/.ory/kratos/public/*to port4433via Traefik.labels (kratos-selfservice-ui-node): Routes all other requests to port4455. Thepriority=1ensures this acts as the catch-all route.depends_on: Ensures services start in the correct order with health checks.volumes: Provides persistent storage for TLS certificates.restart: unless-stopped: Enables automatic recovery after failures or server reboots.
The PostgreSQL Data Source Name (DSN) uses
sslmode=disablebecause the database connection travels over the internal Docker network. If you move PostgreSQL to a separate host, changesslmode=disabletosslmode=require.Build and start all services in detached mode.
console$ docker compose up -d
Verify all services are running.
console$ docker compose ps -a
The output displays four running containers and one completed migration container. All containers should show
Upexceptkratos-migrate, which showsExited (0)after completing database migrations.View the logs for the services.
console$ docker compose logs
For more information on managing Docker Compose stack, see the How To Use Docker Compose article.
Access and Configure Ory Kratos
Health check endpoints confirm the Kratos server is running correctly and Traefik routes requests properly.
Test the Kratos admin API health endpoint from the server.
console$ docker compose exec kratos wget -qO- http://127.0.0.1:4434/health/alive
Output:
{"status":"ok"}Test the public API through the Traefik HTTPS reverse proxy.
console$ curl -s https://kratos.example.com/.ory/kratos/public/health/alive
Output:
{"status":"ok"}Verify that the self-service UI is accessible through HTTPS. Replace
kratos.example.comwith your domain.console$ curl -s -o /dev/null -w "%{http_code}" https://kratos.example.com/
A
303status code confirms the UI is accessible. The self-service UI redirects unauthenticated requests to/login. Openhttps://kratos.example.comin a web browser to access the UI.
Register and Authenticate a Test User
The Kratos API provides self-service flows for user registration and authentication. Testing these flows confirms the deployment handles the complete identity lifecycle correctly.
Initiate a registration flow.
console$ curl -s https://kratos.example.com/.ory/kratos/public/self-service/registration/api
The response contains a JSON flow object with an
idfield. Locate and copy theidvalue.Submit the registration form using test user credentials.
console$ 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" } } }'
Replace:
FLOW_ID: The flow ID from the previous step's response.YOUR-EMAIL: Your test email address.YOUR-PASSWORD: A password that meets the minimum 12-character requirement.FIRST-NAMEandLAST-NAME: Your test user's first and last name.
A successful registration returns a JSON response containing a
sessionobject with asession_token.Verify the user was created by querying the admin API.
console$ docker compose exec kratos wget -qO- http://127.0.0.1:4434/admin/identities
The response contains the registered identity with the email address and name provided during registration.
Start a login flow.
console$ curl -s https://kratos.example.com/.ory/kratos/public/self-service/login/api
Locate and copy the flow
idfrom the response. Submit the login credentials by replacingFLOW_IDwith the login flow ID.console$ 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" }'
Replace
YOUR-EMAILandYOUR-PASSWORDwith the credentials used during registration.A successful login returns a JSON response containing a
sessionobject with asession_token.Copy the
session_tokenvalue from the response and verify the session. ReplaceSESSION_TOKENwith the token value.console$ curl -s https://kratos.example.com/.ory/kratos/public/sessions/whoami \ -H "X-Session-Token: SESSION_TOKEN"
The response contains the authenticated user's identity details, confirming the session is valid.
Conclusion
You have deployed Ory Kratos on a Linux server using Docker Compose with PostgreSQL for persistent storage, Traefik for automatic HTTPS, and the official self-service UI for user-facing flows. The configuration enforces production security settings including password policies, CORS rules, and secure session handling. For advanced configuration, social login providers, and API integration, refer to the official Ory Kratos documentation.