Skip to content

Self-hosted install

Self-hosted Thulu is one binary (thulu) that installs and manages the rest: the daemon with the dashboard, Postgres and the MCP servers the agents use. The binary weighs about 22 MB and carries the deployment files inside, so there is nothing to clone; the daemon runs from the octopus image. thulu init is a terminal wizard that asks questions, writes the configuration, creates the first workspace with a CLI key and brings the stack up. The installer also leaves octopus as an alias of the same command.

If you would rather not install anything, see Get started on the SaaS version: today it is a closed beta by invitation.

You need What for
Linux or macOS, amd64 or arm64 The CLI is built for these.
Docker with Compose v2 (or a Kubernetes cluster with Helm) Runs the daemon, Postgres and the MCPs. On macOS, Colima works.
Optional: an Anthropic API key and a GitHub token for the agents Without the first, runs fail at dispatch. Without the second, agents can only read public repos. You can load them later.

The images are published for linux/amd64 and linux/arm64, so a Mac with Apple Silicon runs them natively.

This is the shortest way to see Thulu running on your computer. The dashboard is published only on 127.0.0.1:8585 and you get in with a link that carries a token.

  1. Install the binary. The script detects your platform, downloads the release, verifies the checksum and, if you have cosign, the signature:

    Terminal window
    curl -fsSL https://github.com/MisterDonnuts/thulu/releases/latest/download/install.sh | sh

    It puts the binary in a writable directory on your PATH, never with sudo. If there is none, it uses ~/.local/bin and prints the line to add it to your PATH. --version 0.3.4 installs a specific release. If there is a terminal, it asks at the end “Configure it now? (thulu init)”.

  2. Run the wizard from any folder:

    Terminal window
    thulu init

    If you aren’t inside a checkout, it writes the deployment files to ~/.octopus (or to OCTOPUS_HOME). Before the first question it checks the machine: whether Docker answers, whether Compose v2 is there, the architecture, whether ports 8585 and 5433 are free and the kubectl context. The questions are forms: arrows to move, space to check and Enter to confirm. With -plain they become text with numbered options.

  3. At “How do you want to install?” keep the default option, “Quick, on this machine”: Docker with the published image, no login and only on 127.0.0.1:8585. It asks three things and everything else takes its default:

    • Engines: check Claude Code and paste an Anthropic API key (it starts with sk-ant-api). Enter skips it and you load it later in the dashboard. Amazon Bedrock and Google Vertex AI are configured later, in Settings → AI engines.
    • Runner: keep “No, agents run in containers” (see Other flows).
    • GitHub: the token so agents can clone and push. You can skip it too.
  4. Review the summary. It shows the installation, the access, the database, the MCP servers, the engines and what was skipped. Nothing is written before this point. Pick “Install”, “Show the deploy/.env changes and the command” (it shows the diff, with secrets trimmed, and asks again) or “Cancel”.

  5. Wait for it to come up. The wizard pulls the image, detects the Docker socket’s group, starts Postgres, creates the workspace and the key, starts the rest of the stack and waits for the daemon to answer. The full log stays in deploy/init.log. At the end it prints the dashboard link, the key and the next steps:

    Thulu is up.
    Dashboard http://localhost:8585/?token=…
    127.0.0.1 only, no logins: this link is the key. `thulu open` opens it again.
    Key for the CLI and the API. It is shown only this once, keep it:
    oct_…
    Use it with: export OCTOPUS_API_KEY=oct_6b77…
    The key expires on …

    Keep the key in a secrets manager: it is the one the CLI uses to talk to the daemon and it can’t be recovered. It expires after 90 days, unless you ran thulu init with OCTOPUS_API_KEY_MAX_TTL=0 in the environment. For another one, run thulu keys mint.

  6. Open the dashboard with thulu open, or by pasting the link from the previous step. The “Setup” button in the top bar shows how many things are missing.

    Freshly installed Thulu Dashboard: zero tasks, the First steps block, the empty Default board and the Setup button with a number in the top bar
    First access to an installation without login. The number on the Setup button is the items still missing.
  7. Complete the pending items from “Setup” (see below). For an agent to work you need the model key, a project, an agent and a column with that agent. No agent comes installed.

At “How do you want to install?” there are three modes:

Mode What it is
“Quick, on this machine” Docker with the published image, no login and only on 127.0.0.1:8585. It asks about engines, runner and GitHub.
“Custom Docker” You choose how it is published, how people get in, the database and the MCP servers. Agents run in containers of this Docker.
“Kubernetes / k3s” Writes the Helm chart’s values. Agents run as isolated pods in microVMs (Kata by default).

Outside quick mode, the wizard moves through sections. Some appear only depending on what you answered before.

Section What it asks Default
How do people log in? “Identity”: OIDC provider, GitHub OAuth app or “No login (local only)”. For OIDC: issuer URL, Client ID, Client secret, button text, allowed email domains, groups claim and mapping of groups to roles. “No login” on the stack without a domain; OIDC on the domain stack with HTTPS, where “No login” isn’t offered
Postgres On Docker, whether to use the Postgres the stack ships. If not, or on Kubernetes, the DSN of an external database, and it tests whether it answers. The stack’s Postgres
Public address Domain (host only), the addresses the dashboard is opened from, the IP of the proxy or ingress in front of it and, on the stack without a domain, the address it listens on. It rejects a domain with a scheme or path and an origin that isn’t http(s)://host. With OIDC or OAuth it gives you the redirect URI to register with the provider. localhost on Docker
Cluster and isolation Kubernetes only: namespace, Helm release name, agent isolation (kata-clh, gvisor or no RuntimeClass) and agent image. It warns if the RuntimeClass isn’t installed. octopus, kata-clh
MCP servers for agents A checklist of the native MCPs (see below). octopus-api, argus-github and argus-browser
Enterprise license (optional) The license key. Without it, the community edition runs. On Kubernetes it is required (see Kubernetes). Skipped
First workspace The workspace name (the slug comes from the name) and the owner’s email. With login, also your email at the identity provider, to run the workspace. Default; with no login, the email from your git config
Engines Which CLI each agent runs and its credential. Claude Code
Runner on this machine Docker only: whether agents can also run on this machine, outside the containers. No
GitHub The GitHub Enterprise Server URL if you use one, and the token so agents can clone and push. Skipped

In Engines the list also offers Codex (with an OpenAI key) and OpenCode (OpenAI-compatible APIs). Both run agents and auditors; messages to a running agent and the runner need Claude Code. See Credentials and engines.

Two secrets are generated on their own and never asked for: the one that signs sessions and the one that encrypts stored credentials. Running thulu init again keeps them; if you lost the second, every stored credential would become unreadable.

At “Custom Docker” the wizard offers two stacks, both with the published image:

Stack What it is When
“Server with a domain and HTTPS” (docker-compose.caddy.yml) Daemon, Postgres and Caddy in front, which gets the certificate for your domain on its own. Ports 80 and 443 have to reach the machine. A server with a public domain.
“Only on this machine, or behind your own proxy” (docker-compose.standalone.yml) The image and Postgres, with nothing in front, on port 8585. It listens on 127.0.0.1 unless you set another address. To try it on your machine, or behind your own reverse proxy that ends TLS.

“No login” is offered only on the stack without a domain, because there the wizard can keep the port on 127.0.0.1. If you publish that stack on another address, the summary warns that it serves plain HTTP and leaves TLS pending.

Server What it does Default
octopus-api The board for agents: read and update tasks, comments and attachments. It acts with the run’s own key. Checked
argus-github GitHub: pull requests, commits, branches, Actions runs. Checked
argus-browser Headless Chromium (Playwright): browse, click, screenshots attached to the task. Checked
argus-cloud Read-only cloud metrics and logs (GCP, AWS). Needs a cloud credential per environment. Unchecked
argus-figma Read-only Figma files, frames, renders and comments. Needs a Figma token in Credentials. Unchecked

argus-sandbox isn’t in the list because it ships inside the daemon. Each checked server runs as one more container of the stack (on Kubernetes, as a Deployment of the chart). To change the selection, run thulu init again: the checklist starts with what is already installed. Which tools each server grants to each agent is decided later, in the dashboard: see MCPs.

If you uncheck all of them, agents have only the sandbox’s tools: they can’t comment on the task, open PRs or use the browser.

How you get in depends on what you chose in Identity.

The daemon stores a random token. The link http://localhost:8585/?token=… swaps it for a session cookie that lasts 30 days. To open it again:

Terminal window
thulu open

It prints the link and opens the browser. It reads the token from deploy/.env, so run it from the installation folder, or from anywhere if the installation is in ~/.octopus. If you go to the address without the token, the dashboard sends you to the login with this instruction:

Sign in card saying the installation has no login and asking you to open the daemon's link or run thulu open
Without the token there is no session: the login page tells you how to get the link.

Any request with that cookie is treated as owner. That is why the port stays on 127.0.0.1 and you shouldn’t publish it on another address.

The “Setup” page (/onboarding/) reads the installation’s real state and lists what is missing, what happens in the meantime and where to finish it. An item you complete by hand later counts the same. The top bar button is visible only to admins and disappears when nothing is left. The Dashboard shows the same items under “First steps”.

Setup page with the workspace checked and the rest of the items pending, each with its consequence and a link
A freshly made installation without login. Each pending item says what happens in the meantime and, except for identity, leads to the page where it is resolved.
Item What is missing if it isn’t there Where it is resolved
Identity provider Anyone who reaches the port is owner. Run thulu init again and pick a provider.
Workspace Nothing can be created. thulu workspace create (see CLI).
Model key (BYOK) Runs fail at dispatch. Settings → AI engines. See Credentials and engines.
Git credential Agents can only read public repos: no branches or PRs. Credentials or the GitHub App.
First project There is no repository for agents to work in. Projects: a GitHub repo or an uploaded folder.
Agent definitions The board has no agents to dispatch to. Agents, or a JSON file from Workshop → Import file.
A column that runs an agent Moving a card launches nothing. “Default agent” in the column’s Behavior (Boards and stages).
Agents in each project’s template The tasks of that project are born without an agent. The project’s “Stage template”.

Only an admin of the operator workspace sees the first two.

  1. Register an application in your identity provider (Okta, Entra ID, Keycloak, Google…) and note the issuer, the Client ID and the Client secret. If your provider only speaks SAML, put an intermediary that speaks OIDC in front, such as Keycloak, and use that intermediary’s issuer.

  2. Point the domain’s DNS at the server and let ports 80 and 443 through.

  3. On the server, run thulu init, choose “Custom Docker” and the stack “Server with a domain and HTTPS”.

  4. At Identity choose the OIDC provider. The wizard connects to the issuer before moving on, so a typo fails right there:

    Issuer URL: ✓ the provider answers: sign-in at https://accounts.google.com/o/oauth2/v2/auth
  5. Fill in Client ID, Client secret, the button text and the allowed email domains (for example yourcompany.com). The groups claim and the group:role mapping are optional: with no mapping, everyone gets in with the default role and admins are promoted by hand.

  6. At Public address enter the domain, without https://. The wizard prints the redirect URI: register it in the provider before the first login.

  7. At “First workspace” keep the provisional owner email it proposes and, at the next question, enter your identity provider email. It has to be a different one: the wizard rejects the provisional owner’s.

  8. Finish the rest as in the recommended flow, save the key and sign in at https://<domain> once DNS and the certificate resolve.

On Docker, the wizard asks whether agents can also run on your machine, outside the containers, with your git, your tools and your claude session. For now the runner runs only Claude Code, is configured only from the CLI (it has no dashboard screen) and isn’t offered on Kubernetes. If you answer yes, you choose the folders it can work in (each has to be a git repo and becomes a project whose code never leaves that machine) and whether to leave it running as a service. See CLI.

On Kubernetes, agents run as isolated pods in microVMs (Kata) and the daemon touches no Docker socket. This runtime is validated on a single-node k3s with Kata. It needs a license that includes kubernetes: without it the daemon doesn’t start. The cluster has to be prepared first: Kata installed with its RuntimeClass, the tenant namespaces and an external Postgres database (the chart ships no database). The preparation and the variables are in docs/enterprise-setup.md, with the air-gapped checklist.

There are two ways to install the chart:

  • With thulu init, choosing “Kubernetes / k3s”. It asks the same as on Docker plus the namespace, the release and the isolation, writes deploy/values-<release>.yaml and runs helm upgrade --install … --wait --timeout 10m. That file holds the JWT secret, the credentials key and the OIDC secret in plain text: keep it out of git.
  • With a direct helm install on deploy/helm/octopus, with your own values (there are examples in values-production.example.yaml). In that case the workspace and the key are created by hand with the CLI.

The published agent images don’t include Claude Code: they install it from npm the first time. On Kubernetes the installation builds its own agent image with Claude Code inside (a BuildKit Job, once per version) and until it finishes the pods download it at startup. Without access to npm you have to build the image with CLAUDE_CODE_INSTALL=build; the steps are in docs/enterprise-setup.md. With agents on Kubernetes the Claude subscription token (sk-ant-oat…) is turned off: use an Anthropic API key, Bedrock or Vertex.

thulu init is the way to change an answer: each question takes as its default what the installation already has (from deploy/.env and from deploy/.octopus-init.json, which stores the answers without secrets). It doesn’t issue the key again if the workspace already has an active one; an expired one doesn’t count.

Option What it does
-dry-run Asks everything and shows the summary without writing or running anything.
-non-interactive Takes all the defaults, or the answers from the previous run. To repeat an installation or in CI.
-plain Text questions with numbered options, instead of forms.
-stack <file> Picks the compose stack without asking.
-root <dir> Where the installation lives. By default it searches upward from the current folder, and if there is none, ~/.octopus.
  • “cannot reach it from here” in the Postgres section: the configuration is written anyway, but the workspace and the key stay pending. The summary prints the command to create them from the daemon’s container once the database answers (on compose, docker compose -f deploy/<stack>.yml exec octopus ./octopus workspace create … and then … ./octopus keys mint …).
  • You lost the key or it expired after 90 days: only its hash is stored. Mint a new one with thulu keys mint, or from the daemon’s container: docker compose -f deploy/<stack>.yml exec octopus ./octopus keys mint -workspace <id> -role owner -permissions full (more in CLI).
  • The first run takes longer: on Docker, Claude Code is installed from npm the first time each workspace uses it and stays in a volume. Without access to npm you have to build the agent image with CLAUDE_CODE_INSTALL=build (docs/enterprise-setup.md).
  • Agents have no access to the board or to GitHub: check that the MCPs are checked in the checklist and granted to the agent in MCPs.
  • After the OIDC login “Your account is waiting for access” appears and you aren’t the first person: nobody invited you to the workspace. Ask an admin to invite you, log out and sign in again.