SSD Nodes Learn 🎉 VPS from $5.50/mo
Guides Matt ConnorBy Matt Connor · Updated 2026-08-20

Self-host OneCLI: one agent per person

OneCLI hands every person a sandboxed agent and keeps the keys in one gateway. Self-host it on a VPS: Docker Compose, PostgreSQL, and real sizing numbers.

What you get when you self-host OneCLI

Self-host OneCLI and every person in your team gets their own agent, each one running in its own sandbox, with the API keys held in a gateway that the agents never read. The install is a Docker Compose stack with PostgreSQL behind it, reachable at http://localhost:10254. Plan for a real box. The documented default is 2 GiB of memory per agent sandbox, so this is not a workload for a 1 GB VPS.

Seven pieces ship in that stack, and knowing which is which makes the rest of this guide easier to read.

  • Web dashboard (Next.js), port 10254. Agent creation, chat, memory and skills editing, connections and secrets.
  • API server, port 10256. The control plane: database, conversation handling, work queues.
  • Rust gateway, port 10255. Intercepts outbound requests from agents and injects credentials.
  • Runner. The README describes it as the component that "starts, parks and reaps agent sandboxes. Outbound-only, and never touches the database."
  • Sandbox Supervisor. The README describes it as running "inside each sandbox, speaking a vendor-neutral harness interface so the agent runtime is swappable."
  • Channel adapter. A daemon that connects a Slack app, so an agent answers in channels and DMs under its own name.
  • PostgreSQL. The shipped compose file runs postgres:18-alpine with a pgdata volume.

The name says CLI, the product is a server

OneCLI is a server platform. The name points at a command line tool you install on a laptop, and that picture is wrong for the thing in this guide. A separate command line client does exist, in the onecli/onecli-cli repository, and it routes a local coding agent's traffic through a gateway. What you deploy here is a multi-user web application: an account system where the first account owns the instance, a database of conversations and secrets, and a runner that starts containers.

The per-person model is the whole design. From the README: "You create an agent per person, give each agent the access it needs, and it works in a sandbox, routed through a gateway that injects the credentials and enforces your policy." Each agent has its own filesystem and shell, its own conversation page, memory the platform keeps, and skills you write once. Credentials work the opposite way round from the usual setup. Rather than copying an API key into each person's environment, you store the key once and grant it to the agents allowed to use it.

What the box needs before you start

  • Docker, with the Compose plugin at version 2.19 or newer. The compose file uses a one-shot migrations service that the API waits on, and that dependency form needs 2.19.
  • Memory, which is the real constraint. Read the sizing section below before you pick a plan.
  • Free loopback ports 10254, 10255, 10256 and 5432.

You do not need to install PostgreSQL yourself: the compose file runs it as a service. You do not need Node.js or Rust either. Those are only for the build-from-source path, where mise pins the toolchain.

How many agent sandboxes fit on your VPS?

The runner's own documentation gives real numbers instead of vibes. Each sandbox gets 2048 MB of memory (RUNNER_SANDBOX_MEMORY_MB), one CPU (RUNNER_SANDBOX_CPUS) and 512 processes (RUNNER_SANDBOX_PIDS). The concurrency cap is 4 (RUNNER_MAX_SANDBOXES), and the documentation recommends around 10 GiB of free memory beyond the base stack to serve that cap.

ChartConcurrent agent sandboxes per box, at the documented 2 GiB default
The data behind this chart
[
  {
    "plan": "2 GB box",
    "ram_gb": 2,
    "sandbox_slots": 0
  },
  {
    "plan": "4 GB box",
    "ram_gb": 4,
    "sandbox_slots": 1
  },
  {
    "plan": "8 GB box",
    "ram_gb": 8,
    "sandbox_slots": 3
  },
  {
    "plan": "16 GB box",
    "ram_gb": 16,
    "sandbox_slots": 7
  },
  {
    "plan": "32 GB box",
    "ram_gb": 32,
    "sandbox_slots": 15
  }
]

Those slot counts are arithmetic, not a benchmark: total memory, minus roughly 2 GB for PostgreSQL and the four long-running services, divided by the 2 GiB sandbox cap. On that basis the 2 GB box fits 0 sandboxes, so the cheapest plan cannot run a hosted agent at all. A 16 GB box leaves room for 7, comfortably above the default cap of four and above the roughly 10 GiB of free memory the runner documentation asks for. The 32 GB box takes you to 15.

Two things bend that arithmetic. A sandbox with a background process running never parks, so it holds its slot permanently, which means you size RUNNER_MAX_SANDBOXES for sustained load and not for the busiest minute. And memory runs out before CPU does. Each sandbox is capped at one CPU, so four busy agents want four cores, but four idle-but-awake agents still hold 8 GiB.

Runner settings you may want to change
  • RUNNER_MAX_SANDBOXES (default 4): how many sandboxes run at once.
  • RUNNER_SANDBOX_MEMORY_MB (default 2048): memory cap per sandbox.
  • RUNNER_SANDBOX_CPUS (default 1): CPU cap per sandbox.
  • RUNNER_SANDBOX_PIDS (default 512): process cap per sandbox.
  • RUNNER_NETWORK_INTERNAL (default true): keeps the sandbox network with no route out. Leave it on.
  • RUNNER_SANDBOX_NETWORK (default onecli-sandboxes): the network sandboxes join.
  • RUNNER_RECONCILE_SECONDS (default 60): how often the runner reconciles state.
  • RUNNER_ORPHAN_GRACE_SECONDS (default 3600): age at which orphaned containers and volumes are destroyed.
  • RUNNER_AGENT_IMAGE: overrides the sandbox image, which otherwise follows ONECLI_VERSION.

Install OneCLI with Docker Compose

The upstream self-hosting document gives this exact sequence. It writes three secrets into docker/.env beside the compose file, then starts the stack.

git clone https://github.com/onecli/onecli.git && cd onecli/docker
cat > .env <<EOF
SECRET_ENCRYPTION_KEY=$(head -c 32 /dev/urandom | base64)
GATEWAY_INTERNAL_SECRET=$(head -c 32 /dev/urandom | base64)
BETTER_AUTH_SECRET=$(head -c 32 /dev/urandom | base64)
COMPOSE_PROFILES=runner
EOF
chmod 600 .env
docker compose up -d --wait

Read that block before you run it. The heredoc marker is unquoted, so your shell runs each head -c 32 /dev/urandom | base64 and writes the result rather than the literal text. SECRET_ENCRYPTION_KEY is the AES-256-GCM key for every secret in the database. GATEWAY_INTERNAL_SECRET authenticates the gateway to the API. BETTER_AUTH_SECRET signs session cookies. COMPOSE_PROFILES=runner is the line that matters most, because the runner service sits behind a Compose profile: leave it out and the stack comes up healthy while no agent sandbox ever starts.

--wait holds the shell until every service reports healthy, so a non-zero exit is your first signal that something is wrong. Then look at what actually came up.

docker compose ps
docker compose logs migrations

Pin the version. ONECLI_VERSION sets the tag for every service at once, and the agent sandbox image follows it unless RUNNER_AGENT_IMAGE points somewhere else. As of 19 August 2026 the current release is v2.0.1, published on 18 August 2026. Add it to the same file and bring the stack back up.

echo 'ONECLI_VERSION=v2.0.1' >> .env
docker compose up -d --wait

An installer also exists, curl -fsSL https://onecli.sh/install | sh, which writes its configuration to ~/.onecli/.env and does the same work. The Compose path is the one where you can read every file before anything runs, and it is the one to use on a box that already carries other Compose stacks. Building from source is a third path, documented as pnpm install then pnpm run setup in the cloned repository. That path wants mise, Rust for the gateway, and Docker anyway, and it exists for people who intend to change the code.

Reaching the dashboard from your laptop

Every published port in the shipped compose file binds to ${ONECLI_BIND_HOST:-127.0.0.1}. On a VPS that means the dashboard is running and nothing outside the box can reach it. That default is correct. Keep it, and tunnel:

ssh -N -L 10254:127.0.0.1:10254 you@your-server

Now open http://localhost:10254 on your laptop. The traffic rides the SSH connection, so there is no unencrypted dashboard on the public internet and no extra port to firewall.

Setting ONECLI_BIND_HOST=0.0.0.0 publishes the dashboard over plain HTTP, and it publishes PostgreSQL alongside it. If several people need the dashboard, put a reverse proxy with TLS (transport layer security) in front of port 10254 and leave the bind host alone. Do that before the instance has an owner. The upstream documentation is blunt about why: "Until you do, the instance has no owner, and on a reachable host whoever gets there first becomes one." If that proxy already fronts your other self-hosted apps, forward auth through a self-hosted single sign-on layer puts the dashboard behind the login your team already has, so removing someone in one place closes this door too.

Create the first account, then grant a model key

Open the dashboard and create the account immediately. That account owns the instance, and once it exists, joining needs an invitation.

Then store a model key before you create an agent. A hosted agent needs a granted model key, and the order matters: store the key in the dashboard, grant it to the agent, and only then start a conversation. Skip the grant and the sandbox never launches, which shows up as an agent that sits there doing nothing.

Grant narrowly. Each agent gets only what you granted it and the gateway enforces that on every request, so an agent that reads one repository has no path to your payment provider's key. The same grant list is your lever on spend. A per-person agent that can call any model you own is a per-person invoice, so it is worth reading how to cap what an agent can spend on model calls before you hand out ten of them.

How the gateway keeps keys out of the agents

The gateway is an HTTPS proxy written in Rust, listening on port 10255. An agent's HTTP client is pointed at it and the agent carries a placeholder credential instead of a real one. The gateway matches the outbound request against that agent's grants, decrypts the real secret, swaps it into the request, and forwards it. Secrets sit in PostgreSQL encrypted with AES-256-GCM (advanced encryption standard, 256-bit, Galois/counter mode) and are decrypted only at request time. Every call is logged with the agent's identity and its target, which is an audit trail you cannot get when keys live in ten people's shell profiles.

Two mechanics decide how you deploy it.

  • HTTPS interception is a man-in-the-middle. The gateway generates a local certificate authority, the agent trusts it, and the gateway terminates the agent's TLS connection then opens a fresh one to the upstream service. That is why an agent whose HTTP client does not trust the gateway certificate authority fails with a certificate verification error rather than an authentication error.
  • The agent identifies itself with a Proxy-Authorization header. On a single box, where agents and gateway share an internal Docker network, that header never crosses a network you do not own. Point an off-box agent at the gateway and the proxy port needs its own TLS, because that header is a bearer token.

The honest trade: the gateway reads every request your agents make, in plaintext, by design. It is the most sensitive process on the machine. Treat its host accordingly, and keep the number of people who can log in small with least-privilege Linux users.

Why the runner needs no inbound port

The runner is outbound-only. From its documentation: "it holds no ports the outside world can reach, so a laptop, a homelab, or a VPC behind NAT all work with no ingress, no tunnel, and no TLS termination story." NAT is network address translation, the thing a home router does. The runner dials out to the control plane and pulls work from it, so there is nothing to forward and nothing to open.

That design pays off in the sandbox network. The compose file defines a second network marked internal: true, which in Docker means no route out of the host at all. Sandboxes join it. The gateway is dual-homed across both networks, so it is the only way out. The runner documentation states the point plainly: "an internal network with the gateway dual-homed onto it is what makes gateway-only egress a boundary rather than a suggestion." An agent that decides to send your source code to an address of its own choosing has no route to do it.

Confirm it on your own box rather than trusting the paragraph above.

docker network ls
docker network inspect onecli-sandboxes | grep -i internal

You should see "Internal": true. If it reads false, the egress control is off and the gateway is a suggestion again. Use whichever sandbox network name docker network ls prints, since onecli-sandboxes is only the default.

How strong is the OneCLI sandbox?

Read this section slowly, because "sandboxed" carries a lot of weight in the project's own description while the mechanism is documented in exactly one place.

The README says each agent gets "its own isolated sandbox, with a filesystem and a shell", and names the Sandbox Supervisor as the component that "runs inside each sandbox, speaking a vendor-neutral harness interface so the agent runtime is swappable". Neither sentence says what the isolation is made of. The runner's documentation does: the default backend is Docker (RUNNER_BACKEND=docker), and a sandbox is a Docker container with a memory cap, a CPU cap and a process cap, attached to the internal network. The code carries a seam for other backends, and the documentation names things like Kubernetes and microVMs as modules someone would have to write. Today, on your box, a sandbox is a container.

What the documentation does not say matters just as much. There is no threat model. There is no statement about running the Docker daemon rootless, about user namespace remapping, about seccomp or AppArmor profiles beyond Docker's defaults, and no claim of a kernel boundary such as gVisor or a microVM. So take the narrow reading. The caps are resource caps. The internal network is a genuine egress control. The isolation between an agent and your host is whatever a stock Docker container gives you, and a container shares the host kernel.

There is a second fact to weigh. The runner service mounts /var/run/docker.sock, because that is how it creates sandboxes. Access to the Docker socket is equivalent to root on the host, since whoever can call that API can start a container with the host filesystem mounted inside it. Every Docker-backed runner works this way. The consequence is that the runner process is as sensitive as the gateway.

Treat the boundary as unproven until upstream writes it down. In practice that means three habits.

  1. Run OneCLI on a box that does nothing else. No unrelated production service, no shared database, no other team's data.
  2. Assume an agent that gets arbitrary code execution inside its sandbox could reach the host, and make that outcome survivable with backups kept off the box.
  3. Read apps/runner/src or ask upstream before you tell a colleague that the agent is contained.

For a picture of what a documented boundary reads like, and the questions worth asking upstream, compare this against what a real agent sandbox boundary looks like. The difference is whether someone has written down the mechanism and what it does not stop.

The licence split, and why to check before you build

OneCLI's core is Apache-2.0, and self-hosting it in production is allowed. Directories named ee/ fall under a separate OneCLI Enterprise License: free for development, testing and evaluation, with a subscription required for production use. The v2.0.1 release notes from 18 August 2026 mention restoring a GitHub-detectable Apache-2.0 licence file, so the badge on the repository page has moved recently. Check the tag you actually deploy rather than a summary written on some other date.

cd onecli && find . -type d -name ee -not -path '*/node_modules/*'

Anything under those paths is the commercial half. If a feature you plan to depend on lives there, price it before you build a process around it.

Upgrades, migrations, and the one file you cannot lose

Upgrades are a version bump and a restart. A one-shot migrations service runs before the API on every up, and if a migration fails the stack refuses to start rather than serving against a half-migrated schema. That is the behaviour you want, because a failed upgrade then looks like an outage instead of like quiet corruption, and docker compose logs migrations says why.

cd onecli/docker
docker compose pull
docker compose up -d --wait
docker compose logs migrations

If you installed with the install script instead, re-run that script rather than pulling by hand, so the compose file stays in step with the images it references.

Back up two things. PostgreSQL holds the agents, conversations, memory and the encrypted secrets. The docker/.env file holds SECRET_ENCRYPTION_KEY, and without that key the encrypted secrets are unreadable, so a database dump on its own restores nothing usable.

cd onecli/docker
docker compose exec -T postgres pg_dump -U onecli onecli | gzip > ~/onecli-db.sql.gz
install -m 600 .env ~/onecli-env.backup

Keep both copies off the box. The routine is the same one any stateful Compose stack needs, so if you already back up and upgrade a Docker Compose stack on a schedule, add these two paths to it and stop thinking about it.

When it does not work

  • The stack never comes healthy and docker compose up -d --wait exits non-zero. Read docker compose logs migrations first, because the API deliberately waits on that service.
  • An agent sits idle and no sandbox appears. Check that COMPOSE_PROFILES=runner is in docker/.env and that docker compose ps lists a runner. Then check the agent has a granted model key, since sandboxes do not launch without one.
  • No slots left. RUNNER_MAX_SANDBOXES defaults to 4, and a sandbox with a background process running holds its slot permanently. docker ps shows what is really alive.
  • Containers vanish, or the host crawls. You are out of memory. dmesg -T | grep -i oom records kernel out-of-memory kills, and a single sandbox can claim 2048 MB on its own.
  • An agent's HTTPS calls fail with certificate verification errors rather than authentication errors. Its HTTP client does not trust the gateway's certificate authority.
  • Old containers or volumes linger after you delete an agent. The runner reconciles every 60 seconds and destroys orphans older than RUNNER_ORPHAN_GRACE_SECONDS, which defaults to 3600, so wait an hour before you call it a leak.

Is this the right thing for you to run?

The fit test is short. OneCLI earns a box when several people each need an agent and you want the credentials in one place: one store to rotate, one audit log to read, one dashboard where revoking a person's access actually revokes it. That is a real operational problem, and copying an API key into six laptops is a worse answer to it.

For one person it is a lot of machinery for no gain. You would run PostgreSQL, a control plane, a gateway and a runner to give yourself a single agent, and the credential problem the gateway solves barely exists when the only holder of the key is you. Run a single harness on a smaller box instead: a single agent harness on a VPS does that job on a fraction of the memory. If you have not picked a direction at all yet, the survey in self-hosted AI agents compared is the cheaper first step.

FAQ

What are the minimum server requirements to self-host OneCLI?

Docker with the Compose plugin at 2.19 or newer, and enough memory. PostgreSQL comes with the compose file, so you do not install it separately. Memory is what decides the plan: the runner allocates 2048 MB per agent sandbox by default, its documentation asks for around 10 GiB free beyond the base stack to serve the default cap of four sandboxes, and roughly 2 GB goes to PostgreSQL and the four long-running services. A 4 GB box runs one agent at a time. A 16 GB box comfortably covers the default cap. A 1 GB or 2 GB VPS cannot start a hosted agent at all.

Does OneCLI need PostgreSQL, or can it use SQLite?

It needs PostgreSQL. DATABASE_URL is documented as a PostgreSQL connection string, the shipped compose file runs postgres:18-alpine with a pgdata volume, and a separate migrations service applies the schema before the API starts. No SQLite option is documented. If you already run PostgreSQL elsewhere, point DATABASE_URL at it and keep the migrations service, since a failed migration stops the stack rather than letting it serve a half-applied schema.

Is the OneCLI agent sandbox a real security boundary?

The documented mechanism is a Docker container with memory, CPU and process caps, attached to a network marked internal: true so it has no route out except through the gateway. The egress control is real and you can verify it with docker network inspect. The host isolation is container-strength, and upstream publishes no threat model, no rootless or user-namespace claim, and no kernel boundary such as gVisor or a microVM. The runner also mounts /var/run/docker.sock, which is equivalent to root on the host. Treat the agent-to-host boundary as unproven until upstream states it, run OneCLI on a dedicated box, and keep backups off that box.

Do I need to open any inbound ports for OneCLI?

No. The runner is outbound-only and holds no ports the outside world can reach, so it works behind NAT with no tunnel. The compose file binds the dashboard, gateway, API and PostgreSQL to 127.0.0.1 by default. Reach the dashboard over an SSH tunnel, or put a reverse proxy with TLS in front of port 10254 if several people need it. The gateway on 10255 is for agents, and on a single box those agents reach it over the internal Docker network.

Is OneCLI free to use inside a company?

The core is Apache-2.0 and self-hosted production use is allowed with no commercial licence. Directories named ee/ are covered by the OneCLI Enterprise License, which is free for development, testing and evaluation but needs a subscription in production. The split moves between releases, and the v2.0.1 notes from 18 August 2026 mention restoring a GitHub-detectable Apache-2.0 licence file, so check LICENSE and the ee/ directories in the exact tag you deploy before you build a workflow on any single feature.