Cookies on Kythene

We use cookies and similar technologies for the things below. You can accept all, reject everything except what's essential, or pick what you're OK with.

Preferences
Theme, language, dismissals. Improves the experience but the site works without.
Improvement
Anonymous usage measurement so we can fix bugs and prioritise work.
Marketing
Lets us measure whether ads we run send people who actually use the site. We don't share personal data with advertisers.

Read our cookies policy and the privacy policy. California residents: Do Not Sell or Share My Personal Information.

Loading…

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 vector extension. The bundled stack uses the pgvector image; the app applies all migrations on boot, including CREATE 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-gapped

Then bring it up:

docker compose up -d

On 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}/callback

where {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 to KYTHENE_LICENCE_KEY_FILE (/data/licence.key) so it survives restarts, and auto-refreshes against KYTHENE_LICENCE_REFRESH_URL so an active subscription never lapses in place.
  • By config: set KYTHENE_LICENCE_KEY before 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 -d

New 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:

  1. Postgres - all metadata: workspaces, projects, published work, memory, members, audit. Use pg_dump or snapshot the volume.
  2. 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.
  3. 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.