Skip to content
Self-hosted AI Agents

selfhostedaiagents.com

Self-host open-source AI agents as one Docker Compose stack

A self-hosted AI agent setup on Docker Compose is one Compose project and the order you start it in. Kortix is the open-source AI Operating System, and its stack moves from the three DNS records to five commands, then to the four failures that arrive once it is up.

Five commands, in the order the stack expects

Preflight reads DNS, ports, disk and memory before anything is written; the rest assume it passed.

preflight.sh
$ ./bin/preflight.sh kortix.example.compreflight passed

docs/checks.md ↗

terminal
$ curl -fsSL https://kortix.com/install | bash

Read the docs ↗

terminal
$ kortix self-host init --domain kortix.example.com --admin-email you@example.com

docs/install.md ↗

What one Compose project holds

A self-hosted instance runs the whole platform on one server you control. kortix self-host init writes docker-compose.yml, .env, a Caddyfile and updater.sh into ~/.config/kortix/self-host/default/, and one Compose project, kortix-<instance>, holds every service. The stack has no Redis and no separate worker.

Four services carry the request path. frontend is the web app. kortix-api is the REST API at /v1. llm-gateway is the standalone model gateway, and Caddy sends api.<domain>/v1/llm* to it. kortix-migrate is a one-shot job that migrates the database before kortix-api starts. Behind them sits supabase-*: Postgres as supabase-db, plus auth, REST, storage, realtime and the pooler, every image pinned by digest.

Two more services decide whether the stack is reachable at all. caddy terminates TLS on ports 80 and 443 with ACME HTTP-01 and routes traffic in domain mode. kortix-updater pulls new images once a day, migrates, and rolls the app services, and it mounts the Docker socket, which is why the update window gets a section of its own.

Three DNS records, before the first start

All three point at the server: an A or AAAA for <domain>, the same for api.<domain>, and a wildcard *.p.<domain> for sandbox previews. Caddy asks for a certificate on the first request it receives, so the apex record has to resolve before start, or the first visitor meets a TLS error instead of a sign-in page.

The second record matters more than it looks. No session runs on your server. kortix-api creates each one on a Platinum sandbox with PLATINUM_API_KEY, and that sandbox calls back to the instance's public KORTIX_URL at api.<domain>. With no record there, the stack looks healthy and no session ever starts. The wildcard carries previews. Without it, previews fall back to a path proxy that breaks root-absolute links, and the doctor fails preview-origins.

./bin/preflight.sh kortix.example.com resolves all three, checks ports 80 and 443, disk, memory and tooling, and stops on a missing record. It is safe before the stack exists, and every step after it assumes that check passed. Run it again after any DNS or proxy change.

The install writes its own compose file

bash
./bin/preflight.sh kortix.example.comcurl -fsSL https://kortix.com/install | bashkortix self-host init --domain kortix.example.com --admin-email you@example.comkortix self-host configurekortix self-host start

init asks the setup questions on a terminal and writes the files. configure asks them again and writes files only, so a run with no start after it changes nothing. Three answers carry the weight: the admin address, PLATINUM_API_KEY, and the preview domain p.kortix.example.com. Without a Platinum key the stack still starts and sessions do not, and init says so in a warning rather than stopping.

The admin address is a list, not a single value. Only addresses in KORTIX_PLATFORM_ADMIN_EMAILS reach /admin, and correcting a typo means repeating every address in the list.

The memory floor is arithmetic

The published limits sum to kortix-api 2048m, llm-gateway 2048m, frontend 512m and Postgres fixed at 1280m. That is 5888MB before host memory, so preflight.sh fails under 6000MB, warns up to 7500 and passes at anything higher. Disk has a floor too: kortix-updater stops below KORTIX_MIN_FREE_DISK_MB, 2048 by default.

A 4GB server does not fail somewhere mysterious later. It fails in the preflight, before anything is written.

What stays on your server

Self-hosted here means the control plane, the Postgres database and the stored files. Those live on the machine you rent, under your backups and your access rules. Sessions run on Platinum sandboxes that kortix-api creates with your key, and each sandbox calls back to the instance's public URL. Model calls go to the providers your projects connect, on keys you add in the web app after the first sign-in; each project keeps its own key.

So the split is worth stating plainly before anyone sizes a server around it. The box runs the platform, the database and the files. The sandboxes run the agents. The model providers see the prompts.

The four failures that show up after the stack is up

An open sign-up

A domain-mode instance is reachable by design, and a new one confirms sign-ups without sending mail. Until you set EMAIL_URL or DISABLE_SIGNUP=true, the auth service will confirm an account for any address that asks, including the address you made admin, and whoever claims an admin-listed address first becomes platform admin. ./bin/check-signup.sh https://kortix.example.com reads the auth settings and exits 0 for closed or email-verified, 1 for open, 2 when it cannot tell. Run it after the first admin sign-in and again after every update, because an update is when a setting tends to return.

A callback that does not resolve

This is the failure with the quietest symptom. Every service reports healthy, the web app loads, and each session a person starts sits at nothing. The cause is usually the api.<domain> record, or a proxy that answers on the apex and not the API subdomain. preflight.sh prints that failure by name, and the doctor catches the same missing record on its next run.

A backup without its key

The instance directory is the unit to copy: .env holds the secrets and signing keys, volumes/db/data is Postgres, volumes/storage keeps files. A database dump captures one of those three and leaves the key behind. API_KEY_SECRET decrypts every stored secret, so an archive without it restores a stack that serves pages and cannot read its own credentials. backup.sh refuses to run without the key, stops the stack so the Postgres files are consistent, archives the directory, and writes a sha256 plus a 16-hex key fingerprint. restore.sh fingerprints the archived key and refuses a swap that would break every stored credential. Copy the .tgz, its .sha256 and its .manifest.json off the server.

A doctor nobody reads

./bin/doctor.sh default runs seven checks: docker, docker-compose, compose-config, instance-dir, email-url, preview-origins and open-sign-up. Six of seven is the usual first result, and the one that fails is the failure that would have surfaced after an update at the worst hour. The script prints a fix line per failure and keeps a timestamped report under reports/. Exit 0 means all seven passed.

The update window, and the way back

kortix-updater runs at 02:00 in America/New_York unless KORTIX_UPDATE_TIME or KORTIX_UPDATE_TZ says otherwise. It pulls the channel, runs kortix-migrate, then rolls the API, the gateway and the front end. A failed migration swaps nothing, and the old version keeps answering. kortix self-host status shows the last outcome, including drift and the next scheduled run. When a release goes wrong anyway, rollback --release <version> returns to the version that worked.

None of this is the compose file, and the compose file is written for you. What separates a stack that runs in a demo from one a team depends on is whether those four readings get taken and whether a person reads the answer. The operator kit built around the Kortix CLI exists for that, and verify-all.sh reruns sign-up, doctor, update status and backup age in one pass, exiting 1 when a person is needed. Every environment variable and the full command list are in Read the docs, and what self-hosting costs is worth reading before you pick a server size.

Docker, self-hosting and local runs

Can I use Docker to create AI agents?
Yes. Kortix installs as one Docker Compose stack, where Docker supplies the web app, the API, the model gateway and Postgres, and the agents live in the project’s git repo rather than in an image. kortix self-host start brings the stack up.
Can you self host an AI agent?
Yes. Kortix is the open-source AI Operating System, and a self-hosted instance is the full platform on a server you control, with the control plane, database and files on your disk. Sessions run on Platinum sandboxes that call back to your instance, so plan for a public domain.
Can I run an AI agent locally?
Yes, for evaluation. Kortix runs in local mode with every port on 127.0.0.1, and no session starts there because the sandbox cannot reach a private API. Use domain mode for anything a team depends on.

Point DNS, run five commands, then read the four checks.

Self-host is free, and the preflight names what is missing before anything is written.