Skip to content

Self-host the open source Kortix stack with sign-up closed first

Self-hosting Kortix is closer to a normal deploy than the category suggests: one Docker Compose stack on a Linux server, a domain pointed at it, and three commands. Kortix is the open-source AI Operating System: your agents, their skills, your company memory, and every connector in one git repo you own, with the agents working on real cloud computers.

A reachable instance with sign-up open is not private

The first hour of a self-hosted install decides whether the instance is private at all. The deciding factor is the order of the steps.

Two ways to get it wrong, and both are quiet. An instance that is not reachable runs no sessions, because the sandbox cannot call the API back. Local mode says so in one line: no session runs there, and nothing outside the loopback reaches the API. The other mistake costs more. A new instance confirms every sign-up without sending an email, so a sign-up's address is unproven until you set EMAIL_URL.

The exposure is specific: "anyone who reaches the instance can create a confirmed account for any address, the admin address included" (self-hosting docs). A fresh instance sits in that state until EMAIL_URL is set or DISABLE_SIGNUP is true. The account that matters here is the admin's. KORTIX_PLATFORM_ADMIN_EMAILS decides who is a platform admin, and it matches a password, email-code or social login with an address on the list. While confirmation is off, nobody checks the address, so whoever signs up first with an allowlisted address is platform admin. A box that answers on port 443 with sign-up open hands its console to the first person who finds it.

The security control is therefore a sequence. The admin signs up, then the door closes. What keeps that window short is choosing the lock before DNS points at the box, because the window opens the moment the certificate lands.

What you need before you start

Six things to arrange before the first start.

A Linux host with a public address, on x86_64 or arm64. A bootstrap installs Docker Engine and the Compose plugin where they are missing; where they already run, only the CLI is needed. The CLI itself also runs on macOS.

DNS, before the first start. A or AAAA records for <domain> and api.<domain>, both pointed at the server, because Caddy needs them to get the certificate. Add a wildcard *.p.<domain> for sandbox previews: without it, browser previews fall back to a path proxy that breaks root-absolute links, and doctor fails.

Inbound HTTP and HTTPS, 443 and 80, allowed by the firewall.

A Platinum API key. Without it, sessions do not start and projects have no Drive.

A model provider key, entered inside the app once the admin exists. Each project holds its own, and the self-hosted stack ships no managed model line.

Optional, and safe to defer: a *.apps.<domain> wildcard to host Apps, and COMPOSIO_API_KEY for the connector app catalog.

Two things belong on the checklist before the install, not after it. init writes .env with mode 0600, and API_KEY_SECRET in that file encrypts every stored secret, connector credential and token. A lost API_KEY_SECRET makes them unreadable, so copy the file somewhere safe before you invite anyone. The other is the sign-up lock: a mail server string for EMAIL_URL, a switch that closes the door once your team is in, or init --disable-signup on an install that mints accounts elsewhere.

The install, in the order that closes the window

The CLI installs one Docker Compose stack: every platform service, configured from four files, brought up by a single command and updated the same way.

First, DNS. Records before certificate, and before any service answers on 443.

Second, three commands on the server, in a shell where Docker and the Compose plugin are present. The first installs the CLI from kortix.com. The second writes the stack's files and sets the domain and the admin address. The third runs docker compose up and starts the services. A bootstrap script covers a bare server in one call: it installs Docker and the Compose plugin, installs the CLI, runs init, sets the domain and starts the stack, and re-running it is safe. Both paths end with the same four files on disk.

Third, sign up as the admin. Open https://<domain> and use an address on KORTIX_PLATFORM_ADMIN_EMAILS. A password, email-code or social sign-in matches that list; a SAML sign-in never does. Do this as soon as the stack answers, because everything after it depends on an admin existing.

Fourth, close the door. Set EMAIL_URL to an SMTP connection string: Kortix then confirms new accounts by email and adds magic-link sign-in. Or set DISABLE_SIGNUP=true once your people have signed up, then run kortix self-host start so the auth service reads the value. With sign-up off, nobody signs up, the admin included, which is why this comes fourth and never second.

Then run kortix self-host doctor. Its open-sign-up check fails when a domain or tunnel instance has sign-up open and email confirmation off, so a passing run is the proof that the window is closed.

The rest of the first session is plumbing. Connect managed git at /admin/git, where Create a GitHub App is the recommended route. Create the team account, then a project. Add a model key under Customize, Models, Providers. A personal account cannot hold SAML SSO and a project cannot move between accounts, so the team account is worth creating on day one.

What lands on your disk, and what stays with the provider

kortix self-host init leaves four files in ~/.config/kortix/self-host/<instance>/: the Compose file, .env, the Caddy config and the updater script. kortix self-host start runs docker compose up, and one Compose project, kortix-<instance>, holds every service.

Inside it: the web app, the REST API at /v1, a standalone LLM gateway that Caddy points at, a one-shot migration job that runs before the API starts, and a daily updater that pulls images and rolls the app services. The Supabase distribution brings Postgres, auth, REST, storage, the pooler and Studio, and a capture store holds Kortix Capture recordings. Domain mode adds Caddy for TLS on 80 and 443; tunnel mode adds cloudflared instead. Only Caddy publishes a public port, and every other published port binds 127.0.0.1 (what runs where).

Self-host and the database, the files, the repos, the secrets and the audit record all sit on disk you control, on one Docker Compose stack built from the same images the managed cloud runs. The three app images come from Docker Hub without credentials, so the images a review asks about are the images a customer's instance runs.

The project repository is the company, in the sense that matters to a reviewer: agents, skills, rules and connectors live in it as files, so they can be read in the same place as the code.

Three things are not on that server. Sandboxes: every session runs in its own Platinum sandbox, which kortix-api creates with PLATINUM_API_KEY and which dials back to the API address you published. Volumes: each Drive and each App volume is a Platinum volume, so their data lives at Platinum. Model traffic: the gateway calls the providers your projects connect, on keys your projects hold.

It is not air-gapped, and we will not pretend otherwise: agent sandboxes run on the compute provider you configure, and your instance has to be reachable so they can call back.

Put that split in the architecture note. A network reviewer asks where the agent's machine runs before they ask anything else, and the answer has two halves.

Three modes, and which one to pick

Run it on Kortix Cloud, in your VPC, or on your own on-prem network. Self-host is free.

ModedomainHow you set it--domain <domain>Public URLhttps://<domain> and https://api.<domain>Sessions runyes, 2 replicas behind Caddy
ModetunnelHow you set it--tunnel cloudflarePublic URLAPI only, on a trycloudflare.com addressSessions runyes, 1 replica
ModelocalHow you set itneither flagPublic URLnone, every port binds 127.0.0.1Sessions runno, nothing outside the loopback reaches the API

Domain mode is the production setup, and the extra replica is the reason. The updater rolls kortix-api, llm-gateway and frontend behind Caddy, and with two replicas a new container has to pass its health check before the old one stops.

Tunnel mode is for a five-minute look on your own machine. The web app stays on http://localhost:13737 and only the API is public, at an https://<random>.trycloudflare.com address. That address changes every time cloudflared restarts, and start and update capture the new one. Every OAuth callback you registered against the old address breaks with it. For anything that has to survive a week, create a named tunnel in Cloudflare Zero Trust, point its hostname at http://kortix-api:8008, and set CLOUDFLARE_TUNNEL_TOKEN and CLOUDFLARE_TUNNEL_HOSTNAME with kortix self-host env set.

Local mode is the one to use when you only want to read the files. Nothing is public, and no session runs, so it answers a question about the stack and not about the product.

What to check on day two

kortix self-host doctor exits 0 when every check passes and 1 when one fails, and --json prints the result for a status page. The checks that fail in practice are open-sign-up, preview-origins, email-url and capture-store, and each one has a fix named in the output.

kortix self-host status reports the last update outcome (ok, degraded, failed, skipped or never-run), the version change, a drift check of running images against the configuration, and the schedule. The updater checks once a day at 02:00 in America/New_York by default. The stable channel carries a release Kortix promotes to self-hosted instances after it runs in production, latest carries every release, and a --tag pin wins over the channel.

/admin is the operator console. It shows live instance numbers refreshed every 15 seconds, every account and every project, where new sandboxes are placed, the instance's git identity, and a full-lockdown switch on /admin/utils. An admin. row in an account's audit log means the instance's platform admin acted on that account, so the log answers a question a security review will ask.

The audit record is a route, not a screen: GET /v1/projects/:projectId/audit, filterable by action prefix, and kortix audit ls --action <prefix> reads the same rows from a terminal.

Then close the loop once by hand. Open a session in the project you created, let it produce something small, and merge its change request yourself. That is the first time work reaches main, and it is also the check that the review step is wired the way you meant it.

Last, copy .env again once the model key is in. API_KEY_SECRET is the one value in the stack that cannot be recovered, and everything encrypted in the database depends on it.

Self-hosting hands you the platform, and the platform is the easy half. The recurring work is a page of checks with a name beside each one: the updater, the certificate, the backup, and the person who reads doctor when it goes red.

Three commands, then two checks

Run these on the server, in this order, with DNS already pointed at it.

terminal
curl -fsSL https://kortix.com/install | bash
kortix self-host init --domain kortix.example.com --admin-email you@example.com
kortix self-host start
kortix self-host configure
kortix self-host start
kortix self-host doctor

Source: kortix.com/docs/developers#self-hosting

Four questions an operator asks first

Four short answers, each tied to the part of the stack it depends on.

Install Kortix on a Linux host you run: point DNS at it, run the two kortix self-host commands, sign up as the admin, then close sign-up. The stack and its database sit on your disk, and sessions run on the compute provider you configure.

Yes. Agents are files in a project repository, and the platform that runs them is a Docker Compose stack you install on your own server. The sandbox each session works in stays rented: it runs on Platinum and dials back to the address you published.

No, if completely private means with no network at all. The instance must answer from the internet, because its sandboxes call it back. What you own outright is the record: database, files, repos, secrets and audit log on your disk.

Self-host is free. The bills are the server, the model calls on your own keys, and the sandbox compute at Platinum. Add the hours of whoever owns DNS, certificates and updates, because that is the cost that repeats.

Point one domain at one server and read every file it writes

The install is three commands, and the checks after it are the part worth scheduling.