Self-hosting Kythene
Run Kythene on your own infrastructure from the published container image - including fully air-gapped, with no external identity provider and no network egress. This guide covers install, configuration, the first-run wizard, SSO, licensing, upgrades and backup.
Kythene self-hosts free for a single user by default (one signed-in person); multi-user self-host is Enterprise, unlocked by a licence. A licence lifts the cap and unlocks your plan's entitlements. See Licensing.
The hosted app lives at
https://kythene.com. Everywhere the other docs say that URL, self-hosters swap in their own.
What you need
- Docker + Docker Compose on a single host, or an equivalent container runtime.
- PostgreSQL with the
vectorextension. The bundled stack uses thepgvectorimage; the app applies all migrations on boot, includingCREATE EXTENSION vector, so it connects as a role that may create the extension. - An S3-compatible object store (MinIO, AWS S3, ...) for artifact and version bytes. Blob storage is disabled when the endpoint is blank, and publishing then refuses - so keep it configured.
- A reverse proxy terminating TLS (Caddy, nginx, Traefik, Cloudflare, ...) in front of the app. The app serves plain HTTP (
tls: none); never expose its port directly.
Install (image + Compose)
The image is published at ghcr.io/kythene/app:latest (and per-version tags). Below is a complete single-node stack - the app plus its own Postgres and MinIO, and a one-shot bucket-creation container. Save it as docker-compose.yml:
services:
app:
image: ${KYTHENE_IMAGE:-ghcr.io/kythene/app:latest}
restart: unless-stopped
env_file: .env
depends_on:
postgres: { condition: service_healthy }
createbucket: { condition: service_completed_successfully }
environment:
KYTHENE_PORT: ${KYTHENE_PORT:-8080}
# Wire the app to the bundled services (these override .env).
KYTHENE_DB: postgres://${POSTGRES_USER:-kythene}:${POSTGRES_PASSWORD:-kythene}@postgres:5432/${POSTGRES_DB:-kythene}?sslmode=disable&search_path=public
KYTHENE_MINIO_ENDPOINT: minio:9000
KYTHENE_MINIO_ACCESS_KEY: ${MINIO_ROOT_USER:-kythene}
KYTHENE_MINIO_SECRET_KEY: ${MINIO_ROOT_PASSWORD:-changeme-minio}
KYTHENE_MINIO_BUCKET: ${KYTHENE_MINIO_BUCKET:-kythene}
KYTHENE_MINIO_USE_SSL: "false"
volumes:
- appdata:/data # writable volume for the auto-refreshed licence key
ports:
- "${KYTHENE_PORT:-8080}:${KYTHENE_PORT:-8080}" # front this with a TLS proxy
postgres:
image: pgvector/pgvector:pg18 # ships the `vector` extension semantic recall needs
restart: unless-stopped
environment:
POSTGRES_USER: ${POSTGRES_USER:-kythene}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-kythene}
POSTGRES_DB: ${POSTGRES_DB:-kythene}
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-kythene} -d ${POSTGRES_DB:-kythene}"]
interval: 10s
timeout: 5s
retries: 10
start_period: 20s
minio:
image: minio/minio
restart: unless-stopped
command: server /data --console-address ":9001"
environment:
MINIO_ROOT_USER: ${MINIO_ROOT_USER:-kythene}
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-changeme-minio}
volumes:
- miniodata:/data
ports:
- "9001:9001" # optional MinIO console; remove if unwanted
createbucket:
image: minio/mc
depends_on:
minio: { condition: service_started }
entrypoint: >
/bin/sh -c "
until mc alias set local http://minio:9000 ${MINIO_ROOT_USER:-kythene} ${MINIO_ROOT_PASSWORD:-changeme-minio}; do
echo 'waiting for minio...'; sleep 2;
done &&
mc mb --ignore-existing local/${KYTHENE_MINIO_BUCKET:-kythene} &&
echo 'bucket ready';
"
restart: "no"
volumes:
appdata:
pgdata:
miniodata:Alongside it, save this as .env and fill it in (every setting is documented in Configuration below):
# Required
KYTHENE_ENV=prod
KYTHENE_URL=https://kythene.example.com # the exact URL you reach the app on, no trailing slash
KYTHENE_PORT=8080
# Bundled Postgres + MinIO - CHANGE THESE PASSWORDS before first run
POSTGRES_USER=kythene
POSTGRES_PASSWORD=changeme-postgres
POSTGRES_DB=kythene
MINIO_ROOT_USER=kythene
MINIO_ROOT_PASSWORD=changeme-minio # min 8 chars
KYTHENE_MINIO_BUCKET=kythene
# Licence (free single-user by default; a licence lifts the seat cap)
KYTHENE_INSTALL_MODE=self_host
KYTHENE_LICENCE_ENFORCE=false
KYTHENE_LICENCE_KEY_FILE=/data/licence.key
KYTHENE_LICENCE_REFRESH_URL=https://www.kythene.com # the storefront/issuer; blank for air-gappedThen bring it up:
docker compose up -dOn boot the app connects to Postgres, applies migrations forward (idempotent), and starts serving. The createbucket service creates the object-store bucket first - the app does not create it itself. Point your TLS proxy at the app's port (8080 by default) and open your KYTHENE_URL.
The image is FROM scratch - a single static binary that listens on KYTHENE_PORT, declares /data as a volume (for the writable licence key), and has a self-probing healthcheck subcommand as its container health check.
Configuration and secrets
Every setting is read from the environment with the prefix KYTHENE_ (viper AutomaticEnv). A value that starts with / is treated as a file path whose contents are the real secret - useful for Docker/Kubernetes secrets.
Required
| Key | Example | Purpose | |---|---|---| | KYTHENE_ENV | prod | dev \| test \| prod | | KYTHENE_URL | https://kythene.example.com | Public base URL; builds links, cookies and OAuth callbacks. No trailing slash. | | KYTHENE_PORT | 8080 | HTTP listen port inside the container | | KYTHENE_DB | postgres://user:pass@host:5432/kythene?sslmode=disable&search_path=public | Postgres DSN | | KYTHENE_MINIO_ENDPOINT | minio:9000 | Object store host:port (blank disables blob storage) | | KYTHENE_MINIO_ACCESS_KEY / KYTHENE_MINIO_SECRET_KEY | ... | Object-store credentials | | KYTHENE_MINIO_BUCKET | kythene | Bucket name (create it; the app does not) | | KYTHENE_MINIO_USE_SSL | false | true for a TLS object-store endpoint | | KYTHENE_MINIO_REGION | (blank) | S3 region, if your provider needs one |
TLS is always terminated by your proxy (tls: none); the app has no TLS setting to enable.
Sign-in
| Key | Default | Purpose | |---|---|---| | KYTHENE_LOCAL_AUTH_ENABLE | true | Email + password sign-in. Bootstraps the first admin with no external IdP; the only sign-in an air-gapped install needs. | | KYTHENE_GITHUB_CLIENTID / KYTHENE_GITHUB_CLIENTSECRET | (unset) | GitHub sign-in (button appears only when both are set) | | KYTHENE_GOOGLE_CLIENTID / KYTHENE_GOOGLE_CLIENTSECRET | (unset) | Google sign-in | | KYTHENE_MICROSOFT_CLIENTID / KYTHENE_MICROSOFT_CLIENTSECRET | (unset) | Microsoft sign-in | | KYTHENE_APPLE_CLIENTID / KYTHENE_APPLE_CLIENTSECRET | (unset) | Apple sign-in |
Licence and seats
| Key | Default | Purpose | |---|---|---| | KYTHENE_INSTALL_MODE | self_host | self_host or hosted. Leave as self_host. | | KYTHENE_LICENCE_ENFORCE | false | true = refuse to start without a valid licence (paid installs). false = run free under the seat cap. | | KYTHENE_LICENCE_KEY | (unset) | The licence token, or a /path to a file holding it. Usually entered in-app instead. | | KYTHENE_LICENCE_KEY_FILE | /data/licence.key | Writable path the key is read from and rewritten to on auto-refresh, so it survives restarts. | | KYTHENE_LICENCE_REFRESH_URL | https://www.kythene.com | Storefront base URL for auto-renew. Blank = offline re-validation only (air-gapped). | | KYTHENE_LICENCE_REFRESH_INTERVAL | 12h | How often to refresh | | KYTHENE_LICENCE_GRACE | 72h | How long past expiry to keep serving before failing closed | | KYTHENE_FREE_SELFHOST_SEATS | 1 | Unlicensed seat cap (signed-in humans, instance-wide). Default 1 = free single user; a licence lifts it for multi-user (Enterprise). 0 = unlimited. |
Optional
| Key | Purpose | |---|---| | KYTHENE_CONSENT_SIGNING_KEY | Cookie/consent signing key (>= 16 bytes). If unset, derived from db + url - fine for a single instance. | | KYTHENE_ANTHROPIC_APIKEY | Enables the AI features (semantic memory, assistants). Blank = off. | | KYTHENE_POSTHOG_APIKEY | Product analytics. Blank = off. | | KYTHENE_OTLP_TRACES_ENDPOINT / KYTHENE_OTLP_METRICS_ENDPOINT / KYTHENE_OTLP_LOGS_ENDPOINT | OpenTelemetry export. Blank = off. | | KYTHENE_ENTITLEMENTS_ENFORCE | Hosted-only master switch. Leave false on self-host. |
First run - the setup wizard
On a fresh instance the first person in becomes the administrator.
- With local auth on (default): opening the app takes you to the first-run wizard at
/setup. Create the first admin with an email and password and, if you have one, paste a licence key there and then. That account owns the instance. - With an OAuth provider configured: the first person to sign in becomes the admin instead.
For an air-gapped install, leave local auth on and use the wizard - no external provider or network access is required.
Add more people afterwards from the admin area:
- Password users: Admin -> Local users (
/admin/local-users). - SSO users: anyone who can sign in through a configured provider joins automatically, subject to the seat cap.
Configuring SSO
To add single sign-on, register an OAuth app with the provider and set both its id and secret. A provider's button appears only when both are present. The callback URL is:
<KYTHENE_URL>/auth/{ref}/callbackwhere {ref} is github, google, microsoft or apple. For example, with KYTHENE_URL=https://kythene.example.com:
- GitHub:
https://kythene.example.com/auth/github/callback - Google:
https://kythene.example.com/auth/google/callback
Register that exact URL in the provider's console, then set the matching KYTHENE_*_CLIENTID / KYTHENE_*_CLIENTSECRET and restart.
Generic OIDC / SAML directories (Okta, Entra ID, Keycloak and the like via a generic connector, plus SCIM provisioning) are a planned enhancement and are not available yet. Today, use the named OAuth providers above, or local email + password auth. This guide will be updated when generic enterprise SSO lands.
Applying and renewing a licence
An unlicensed instance runs free for a single user (KYTHENE_FREE_SELFHOST_SEATS defaults to 1). Once the cap is reached, further sign-ins are refused until a licence is applied. A licence lifts the cap to your plan's seat count (multi-user self-host is Enterprise) and unlocks its entitlements.
Apply or replace a licence at any time:
- In-app: Admin -> Licence (
/admin/licence) - paste the key. It is written toKYTHENE_LICENCE_KEY_FILE(/data/licence.key) so it survives restarts, and auto-refreshes againstKYTHENE_LICENCE_REFRESH_URLso an active subscription never lapses in place. - By config: set
KYTHENE_LICENCE_KEYbefore first run.
Set KYTHENE_LICENCE_ENFORCE=true to make the app refuse to start without a valid, unexpired licence. Validation is offline; only the refresh fetch is a network call, so an air-gapped install leaves KYTHENE_LICENCE_REFRESH_URL blank and re-validates the key it already holds.
Upgrades
docker compose pull app
docker compose up -dNew migrations apply forward automatically on boot. Migrations are forward-only and additive; take a Postgres backup before a major upgrade as normal practice.
Backup and ops
The instance's state lives in three places - back up all three:
- Postgres - all metadata: workspaces, projects, published work, memory, members, audit. Use
pg_dumpor snapshot the volume. - The object store - the artifact and version bytes. Because they are content-addressed, an object-store backup stays consistent with any point-in-time DB backup.
- The licence key (
/data/licence.key) and your.env(secrets and the key-derivation inputs).
The container's health check is the binary's own healthcheck subcommand, so docker compose ps reports health directly. Point KYTHENE_OTLP_* at a collector if you want traces, metrics and logs.