Skip to content

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.

Install plan builderexample values
Run it from
Ubuntu
Docker, Node, tools
People reach it through
Browser IDE, first workspace
Slug acme. Lower-case letters, digits and hyphens, 32 at most.
your plan, in order · run one step at a time
  1. Get the code · your machine

    # the repository is shared on request; use the URL you were given
    $ git clone <repository-url> kodela && cd kodela
  2. Look first · your machine

    $ bin/provision-host --host 203.0.113.10 --user ops --remote-dir kodela --check

    Read-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.

  3. 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-prereqs

    It 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.

  4. 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.

  5. 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.

  6. 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.

  7. 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.

  8. 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.

  9. 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

RequirementVerified onWhy
Linux with systemdUbuntu 24.04the serving tier and the firewall run as systemd units
Docker, the system daemon29.xevery 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 33.12, standard library onlythe console and the tools; nothing is pip-installed
Node20 or later (22 installed by provisioning)to rebuild the console bundle
git, curl, jq, openssl, tar, sudocurrent packagesshipping the commit, health checks, token minting
Memory8 GB or moreabout 2 GB per active workspace; about 1 GB more per person using the browser IDE
Disk30 GB or morethe 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.

PortWhat listensReachable from
5000the console process, the one entry pointloopback and Docker bridges; the firewall drops the rest
8080the front door (Caddy)loopback only by default
8071 8072 8074model proxy, key broker, git brokerworkspace containers, on the workspace gateway
3128egress proxyworkspace containers, per organisation
8083portal serviceloopback

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.

your machine
$ bin/provision-host --host 203.0.113.10 --user ops --remote-dir kodela --check

It 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.

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-prereqs
FlagDefaultWhat it does
--host, --userrequiredthe target and a non-root ssh user with sudo
--base-urlrequiredthe 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-emailrequiredthe first sign-in. The password is asked for, never passed.
--install-prereqsoffapt 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-stackwhere the checkout lands on the host
--shareda folder inside the checkoutthe state tree. Use /srv/agents only if you will use the GitHub App broker.
--refHEADthe commit to ship; a dirty checkout is warned about and not shipped
--copy-idoffput your ssh key on the host first; ssh-copy-id prompts you
--sudo-password-fileinteractivea 0600 file outside the repository, for an unattended run
--checkoffthe 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:

  1. 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.

  2. Write three env files, mode 0600, in ~/.config/agent-stack/: door.env (front-door user, password, bind to loopback), admin.env (the first operator) and cluster.env (the state tree).

  3. Build the workspace image and publish the tree. The build context is the repository root. bin/deploy push needs sudo because the published tree is root-owned.

  4. 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.

on the host
$ 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.com

Run 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.

FileCarries
door.envFRONT_DOOR_USER, FRONT_DOOR_PASSWORD, FRONT_DOOR_BIND=127.0.0.1
admin.envOPERATOR_EMAIL, OPERATOR_PASSWORD
cluster.envAGENT_STACK_SHARED; read through a drop-in, so a unit update from the repository keeps it
on the host
# 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

  1. Create the organisation. In the console: ConsoleOrganisationsNew It gets its own network, egress in observe and the Guarded security level. The slug must match ^[a-z0-9][a-z0-9-]{0,30}$.

  2. Invite its owner, or yourself as a member. Owner is an operator grant. See Invitations.

  3. 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.

    on the host
    $ sudo -v
    $ bin/workspace-agent create --tenant acme --workspace dev01 \
        --owner you@example.com --ide --repo owner/name
  4. Open 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

on the host
$ 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 status

Good 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.

on the host
$ 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 .ready

Rolling 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.

  1. Enumerate first; change nothing. Which state tree the unit uses, which units, containers, volumes and networks exist.

    on the host
    $ 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-'
  2. Back up, and copy the archive off the host. bin/backup create --volumes.

  3. Stop and remove the units.

    on the host
    $ 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-failed
  4. Bring the firewall down, then remove containers and networks.

    on the host
    $ 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 -f
  5. The 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.

    on the host
    $ docker volume ls --format '{{{{.Name}}}}' | grep -E '^ws-.*-home$'
    $ docker volume ls --format '{{{{.Name}}}}' | grep -E '^ws-.*-home$' | xargs -r docker volume rm
  6. Remove the state tree, the env files and the checkout. This deletes the store (accounts, organisations, workspaces), the credential store and the audit chain.

    on the host
    $ rm -rf "$AGENT_STACK_SHARED"
    $ rm -rf ~/.config/agent-stack ~/kodela
  7. Clean 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/backup exists; 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.

Verified against the main branch, 4 Oct 2026.