Install on your host
One Linux machine you control runs all of Kodela; if you grant it and an organisation turns it on, members' own computers can also host their workspaces. This is the whole install, from an empty host to a first workspace.
Get the code · your machine
# the repository is shared on request; use the URL you were given $ git clone <repository-url> kodela && cd kodela
Look first · your machine
$ bin/provision-host --host 203.0.113.10 --user ops --remote-dir kodela --checkRead-only: ok, miss or bad per prerequisite and port. If key-based ssh fails, add --copy-id; ssh-copy-id asks you for the password, never the script. Ubuntu 24.04 with systemd is what Kodela is verified on.
Provision · your machine
$ bin/provision-host --host 203.0.113.10 --user ops --remote-dir kodela \ --base-url https://kodela.example.com \ --operator-email you@example.com --install-prereqsIt asks for the front-door password and the operator password, 12 characters or more each, and never takes them on a command line. It ships this commit, installs apt packages, Docker and Node 22 (scripts fetched from the internet, run as root), builds the workspace image, installs the serving-tier and firewall units and starts the tier. Unproven end to end on a second machine.
Arm the firewall · on the host
$ ssh ops@203.0.113.10 $ cd ~/kodela && export AGENT_STACK_SHARED="$PWD/.agent-stack-data" $ sudo install -o root -g root -m 0755 bin/egress-firewall /usr/local/sbin/agent-stack-egress-firewall $ sudo systemctl start agent-stack-firewall $ sudo -E bin/egress-firewall verify
Provisioning enables the unit for boot; this runs it now, from a root-owned copy of the script so root never runs an account-editable file. The rules are not saved by iptables, which is why the unit exists.
Put a tunnel in front · on the host
$ read -rs FRONT_DOOR_TUNNEL_TOKEN && export FRONT_DOOR_TUNNEL_TOKEN $ bin/front-door tunnel-up # in your Cloudflare account: point kodela.example.com at http://203.0.113.10:5000
A named Cloudflare Tunnel; the public hostname and any Access policy live in your Cloudflare account. The connector image is not pinned.
Confirm it is ready · on the host
$ curl -s http://127.0.0.1:5000/api/readyz | jq '.ready, .checks' $ bin/deploy verify $ bin/whats-running
readyz answers 503 and names the failing check until everything is in place.
Create the organisation · browser
# https://kodela.example.com/admin/ sign in as you@example.com # Organisations > New > "Acme" (slug acme)
It gets its own network, egress in observe and the Guarded security level.
Create the first workspace · on the host
$ sudo -v $ bin/workspace-agent create --tenant acme --workspace dev01 \ --owner you@example.com --ide
With the browser IDE: plan about 1 GB more memory for each person using it. Add --repo owner/name to let it push.
Open it · browser
# https://kodela.example.com/portal/ sign in, then open acme/dev01
“Tunnel” is a Cloudflare Tunnel, “own TLS proxy” is any reverse proxy you run, and “HTTP trial” reaches the console through an ssh tunnel. Pick how you will run it. The plan re-assembles the exact commands, in order. The host address, user and hostname are examples: replace 203.0.113.10, ops and kodela.example.com with yours.
What this installs
The serving tier: the console process that everyone reaches, the front door in front of workspace terminals and IDEs, the brokers that keep credentials out of containers, the egress proxy and firewall, and the workspace image. State lives on the host in one SQLite store and a state tree beside it. There is no Kubernetes, managed database, cloud account or installer package.
It does not install high availability, a second host, scheduled backups or outbound mail. Each has its own section below, and each is off until you set it up.
What the host needs
| Requirement | Verified on | Why |
|---|---|---|
| Linux with systemd | Ubuntu 24.04 | the serving tier and the firewall run as systemd units |
| Docker, the system daemon | 29.x | every workspace is a container; your user must be able to use it. Full Docker inside a workspace also needs a supported runtime (Sysbox); without one, only managed Docker is available |
| Python 3 | 3.12, standard library only | the console and the tools; nothing is pip-installed |
| Node | 20 or later (22 installed by provisioning) | to rebuild the console bundle |
| git, curl, jq, openssl, tar, sudo | current packages | shipping the commit, health checks, token minting |
| Memory | 8 GB or more | about 2 GB per active workspace; about 1 GB more per person using the browser IDE |
| Disk | 30 GB or more | the workspace image is about 2.6 GB, plus homes and 90 days of audit |
These are the figures Kodela runs on, not a sizing guarantee. No workspaces-per-host figure is stated. Workspaces are capped at 2 GB memory, 2 CPUs and 512 processes each.
Docker in a workspace is off until an organisation allows it (workspace_docker_allowed). docker_full_runtime is a host setting, auto by default: it picks Sysbox when the host has it and never resolves to privileged, which an operator must set explicitly and under which the firewall and no-credentials guarantees no longer hold for that workspace. Each new workspace also gets its own Postgres by default; a host without the Postgres image skips it and says why.
Ports the tier claims on the host. Only 80 and 443 should face people, and those belong to your TLS front end or tunnel, not to Kodela.
| Port | What listens | Reachable from |
|---|---|---|
5000 | the console process, the one entry point | loopback and Docker bridges; the firewall drops the rest |
8080 | the front door (Caddy) | loopback only by default |
8071 8072 8074 | model proxy, key broker, git broker | workspace containers, on the workspace gateway |
3128 | egress proxy | workspace containers, per organisation |
8083 | portal service | loopback |
Get the code
The repository is shared on request with pilot teams. It is not public, there is no installer package, and nothing on the host needs GitHub credentials: provisioning ships the commit you are on from your own checkout.
Check the host first
From your checkout, on your own machine. It reaches the host over key-based ssh and changes nothing. Each line is ok, miss (fixable, or could not be tested) or bad.
$ bin/provision-host --host 203.0.113.10 --user ops --remote-dir kodela --checkIt reports the OS and kernel, systemd, whether your user can use Docker, Python, Node (20 or later), git and the small tools, free disk and memory against the verified figures, whether ports 5000, 8080, 8083, 8071, 8072, 8074 and 3128 are free on any address, and whether an older tier or its containers are already running. The ssh user must not be root, must be allowed sudo, and ends up in the docker group.
A pass here is only a pass for what it tested. The check prints the OS but does not refuse one it was not verified on.
Provision
From another machine, over ssh (recommended)
bin/provision-host ships this commit as a git bundle, writes three env files, builds the workspace image, publishes the tree every workspace mounts, builds the console, installs the serving-tier unit and the firewall unit, starts the tier and checks it. It refuses to run against the machine it runs on.
$ bin/provision-host --host 203.0.113.10 --user ops --remote-dir kodela \
--base-url https://kodela.example.com \
--operator-email you@example.com --install-prereqs| Flag | Default | What it does |
|---|---|---|
--host, --user | required | the target and a non-root ssh user with sudo |
--base-url | required | the exact origin people will open, with its scheme. https keeps the Secure cookie and HSTS on; http turns them off. Mail refuses to send without it. |
--operator-email | required | the first sign-in. The password is asked for, never passed. |
--install-prereqs | off | apt packages, Docker from get.docker.com and Node 22 from NodeSource: scripts fetched from the internet and run as root, which is why it is opt-in |
--remote-dir | ~/agent-stack | where the checkout lands on the host |
--shared | a folder inside the checkout | the state tree. Use /srv/agents only if you will use the GitHub App broker. |
--ref | HEAD | the commit to ship; a dirty checkout is warned about and not shipped |
--copy-id | off | put your ssh key on the host first; ssh-copy-id prompts you |
--sudo-password-file | interactive | a 0600 file outside the repository, for an unattended run |
--check | off | the read-only report above, then exit |
Exit code 0 means every phase that ran passed; 1 is a usage error or a local guard; 2 is a remote phase that failed, and the output names it.
provision-host is new. Its happy path has not yet been recorded end to end against a second machine. If the GitHub CLI is signed in on the host it also runs an extra tooling installer; the serving tier does not need it.
On the host itself
For a single machine you are signed in to. It is the same sequence provision-host runs, by hand: pick The host itself in the builder above for the full list. The parts that matter:
Decide the state tree and export it in every shell.
export AGENT_STACK_SHARED="$PWD/.agent-stack-data"from the checkout. The units read the same value from an env file.Write three env files, mode 0600, in
~/.config/agent-stack/:door.env(front-door user, password, bind to loopback),admin.env(the first operator) andcluster.env(the state tree).Build the workspace image and publish the tree. The build context is the repository root.
bin/deploy pushneeds sudo because the published tree is root-owned.Install the serving-tier unit from
config/systemd/with your user, home, checkout path and base URL, plus the firewall unit, then enable both.
The first operator
A fresh install has no sign-in and no self-service signup. The serving tier creates the first operator from OPERATOR_EMAIL and OPERATOR_PASSWORD in admin.env, once, and never overwrites a password changed later in the console. To add another operator or reset one, run it yourself from the checkout on the host. The password comes from --password or the OPERATOR_PASSWORD variable and must be at least 12 characters; reading it into the variable keeps it out of your shell history.
$ cd ~/kodela && export AGENT_STACK_SHARED="$PWD/.agent-stack-data"
$ read -rs OPERATOR_PASSWORD && export OPERATOR_PASSWORD
$ python3 proxy.py create-operator --email you@example.comRun it with the server's state tree. In a shell whose AGENT_STACK_SHARED differs from the running tier's, it writes to a different, or empty, store, and the console never sees the account.
Then sign in at /admin/ on your origin.
The systemd units
The whole serving tier is one unit whose ExecStart is start-ui.sh: it starts the front door, portal, brokers and egress proxy, verifies each, then runs the console process. systemd owns every child, so stopping the unit stops the tier. Configuration comes from env files, not from edits to the unit.
| File | Carries |
|---|---|
door.env | FRONT_DOOR_USER, FRONT_DOOR_PASSWORD, FRONT_DOOR_BIND=127.0.0.1 |
admin.env | OPERATOR_EMAIL, OPERATOR_PASSWORD |
cluster.env | AGENT_STACK_SHARED; read through a drop-in, so a unit update from the repository keeps it |
# which unit serves the tier, and is it up
$ systemctl list-units --all 'agent-stack-*'
$ UNIT=$(grep -l 'ExecStart=.*start-ui' config/systemd/*.service); N=$(basename "$UNIT")
$ systemctl is-active "$N" agent-stack-firewall
$ systemctl cat "$N"The firewall unit (agent-stack-firewall.service) runs once at boot, as root, after the front door is up. The firewall rules are never saved by iptables, so without this unit a reboot silently drops them while status and verify kept reporting healthy until then.
First organisation and workspace
Create the organisation. In the console: ConsoleOrganisationsNew It gets its own network, egress in
observeand the Guarded security level. The slug must match^[a-z0-9][a-z0-9-]{0,30}$.Invite its owner, or yourself as a member. Owner is an operator grant. See Invitations.
Create a workspace. From the organisation's Workspaces tab, or on the host. Warm sudo first: creation extends the firewall to the organisation's network and refuses if it cannot.
$ sudo -v $ bin/workspace-agent create --tenant acme --workspace dev01 \ --owner you@example.com --ide --repo owner/nameOpen it. The owner signs in at
/portal/: terminal, VS Code in the browser (with--ide), the Git panel and the agent chat. Every outbound connection from this workspace on your host already goes through the egress proxy, in observe.
Verify it works
$ curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:5000/healthz
$ curl -s http://127.0.0.1:5000/api/readyz | jq '.ready, .checks'
$ bin/deploy verify
$ bin/whats-running
$ bin/front-door statusGood looks like: healthz answers 200; readyz says "ready": true with every check ok (it answers 503 and names the failing check otherwise); deploy verify exits 0; the unit's startup banner says BUILT BUNDLE and, on an https origin, does not say PLAIN HTTP. What each readyz check means.
Upgrade
An upgrade is publishing a newer commit. From your machine, re-run provision-host with --ref: it ships, publishes, rebuilds and restarts. On the host, take a backup, publish, verify and restart the unit. Every restart is a short outage. When the store is newer than the code, API calls are refused with a sentence until the code catches up.
$ N=$(basename "$(grep -l 'ExecStart=.*start-ui' config/systemd/*.service)")
$ bin/backup create --volumes && bin/backup verify <name>
$ bin/deploy push --ref <ref> && bin/deploy verify --ref <ref>
$ sudo systemctl restart "$N"
$ curl -s http://127.0.0.1:5000/api/readyz | jq .readyRolling back is bin/deploy rollback --ref <ref>. The full runbook is on Operate.
Uninstall and wipe
There is no uninstall command. This is the procedure, and its order matters: units first (a watchdog respawns processes every ten seconds while the unit runs), the firewall before the networks, the state tree last. Match containers by label and image, never by name, and never use docker system prune, volume prune or network prune on a shared box.
Enumerate first; change nothing. Which state tree the unit uses, which units, containers, volumes and networks exist.
$ cd ~/kodela && export AGENT_STACK_SHARED="$PWD/.agent-stack-data" $ N=$(basename "$(grep -l 'ExecStart=.*start-ui' config/systemd/*.service)") $ systemctl show "$N" -p Environment -p ExecStart -p User $ systemctl list-unit-files 'agent-stack-*' $ docker ps -a --filter label=agent-stack.workspace --format '{{{{.Names}}}}\t{{{{.Status}}}}' $ docker volume ls --format '{{{{.Name}}}}' | grep -E '^ws-.*-home$|^agentstack-' $ docker network ls --format '{{{{.Name}}}}' | grep -E '^agentstack-'Back up, and copy the archive off the host.
bin/backup create --volumes.Stop and remove the units.
$ sudo systemctl disable --now "$N" agent-stack-firewall $ sudo rm -f /etc/systemd/system/agent-stack-*.service $ sudo rm -rf /etc/systemd/system/agent-stack-*.service.d $ sudo systemctl daemon-reload && sudo systemctl reset-failedBring the firewall down, then remove containers and networks.
$ sudo -E bin/egress-firewall down $ docker ps -aq --filter label=agent-stack.workspace | xargs -r docker rm -f $ docker rm -f agentstack-front-door agentstack-access-auth 2>/dev/null $ docker ps -a --format '{{{{.ID}}}}\t{{{{.Image}}}}' | grep 'cloudflare/cloudflared' | cut -f1 | xargs -r docker rm -f $ docker network ls --format '{{{{.Name}}}}' | grep -E '^agentstack-' | xargs -r docker network rm $ docker images --format '{{{{.Repository}}}}:{{{{.Tag}}}}' | grep '^agent-stack/' | xargs -r docker rmi -fThe irreversible step: home volumes. Every developer's work is in
ws-<organisation>-<workspace>-home. Look at the list, confirm the backup is off the host, then remove them.$ docker volume ls --format '{{{{.Name}}}}' | grep -E '^ws-.*-home$' $ docker volume ls --format '{{{{.Name}}}}' | grep -E '^ws-.*-home$' | xargs -r docker volume rmRemove the state tree, the env files and the checkout. This deletes the store (accounts, organisations, workspaces), the credential store and the audit chain.
$ rm -rf "$AGENT_STACK_SHARED" $ rm -rf ~/.config/agent-stack ~/kodelaClean up what lives elsewhere. A Cloudflare tunnel, its Access application and DNS record, a GitHub App and any AI provider sign-ins made through the console still exist on those services. Delete or revoke them there.
Docker, Python, Node and the other prerequisites are left in place, ready for a clean reinstall.
More than one host
Worker hosts (a Postgres placement store and mutual-TLS worker enrolment) are built and off by default. They have been seen working only in development, and their test suite is currently failing. Leave the setting unset: off is the single-host path, byte for byte. A workspace cannot be moved between hosts.
Members' own computers
Separate from worker hosts, and also off by default. You grant it per organisation (personal_hosts_allowed); the organisation then turns it on (personal_hosts_enabled); each member adds their computer on Your computers in the portal and pastes the one command it shows, which runs bin/personal-host-install.sh on Linux or WSL, or bin/personal-host-install.ps1 on Windows. The agent connects out, needs no sudo, and updates itself. A computer that has not connected within its lease (30 days by default) is disabled. The steps are in Workspaces.
Workspaces run on your Kodela host. If a platform operator grants it and your organisation turns it on, a member's own computer can host their workspaces too. There, internet access is observed, not enforced; DNS filtering is not enforced; the source on its disk is readable by whoever uses it; and Docker is not available. Tested end to end only on Windows (Linux laptops up to enrolment; Mac only in a simulation).
Troubleshooting
--host … is THIS machine
provision-host only provisions another machine. On the host itself, use the by-hand path.
key-based ssh to ops@… failed
Your key is not on the host. Re-run with --copy-id; ssh-copy-id asks you for the password.
port 5000 in use, or the unit dies at start
A tier started by hand earlier, possibly from a deleted folder, still holds the ports. --check lists the processes; stop them, then sudo systemctl reset-failed and start the unit.
OPERATOR_PASSWORD must be at least 12 characters
create-operator refuses shorter passwords. So does provision-host, before it changes anything.
Sign-in works from the host, times out from the network
By design. The firewall drops the console port from the network. Put a tunnel or TLS proxy in front, or use an ssh tunnel for a trial.
Sign-in fails over plain http
On an https origin the session cookie is Secure and a browser will not send it over http. Use the https address, or provision with an http base URL for a trial.
egress firewall does not cover … on workspace create
Creation extends the firewall with non-interactive sudo. Run sudo -v and try again.
image agent-stack/workspace:0.1.0 not found
Build it from the repository root: docker build -t agent-stack/workspace:0.1.0 -f docker/workspace/Dockerfile .
Firewall rules gone after a reboot
The firewall unit is not installed or not enabled. systemctl is-enabled agent-stack-firewall.
workspace-agent list prints none
Its Docker queries go through sudo; an authentication failure looks like an empty host. Check sudo -n true.
What it does not do yet
- No installer package, Helm chart or Compose file. The install is a checkout and the steps above.
- No ACME. Kodela serves plain HTTP unless you put a TLS front end or tunnel in front.
- provision-host is unproven end to end. Its own header marks it unproven against a second machine. The by-hand path is the fallback.
- Nothing schedules backups.
bin/backupexists; you add the timer. - No high availability. The control plane is one SQLite file on one host. Worker hosts are built and off.
- Air-gapped install is partial. Everything is version-pinned and images move with
docker save; there is no offline bundle or mirror. - Mail is off. Invitations and password resets are not delivered until you configure SMTP.
- Security starts permissive. Egress starts in
observe, and the shared front-door password is accepted until “require identity” is on.
Related
Verified against the main branch, 4 Oct 2026.