The README Quick Start covers the interactive setup. This page holds everything else you need to run OneCLI yourself: the install script, raw compose, version pinning, registration rules, and upgrade notes.
No Node toolchain handy? The install script does the same as pnpm run setup
without a clone, writing its configuration to ~/.onecli/.env:
curl -fsSL https://onecli.sh/install | shPut the three required secrets in docker/.env — beside the compose
file, which is where compose reads them — then bring it up:
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 --waitOpen http://localhost:10254, create your account, then create an agent,
store a model key, grant it to the agent, and start talking. (Keep
SECRET_ENCRYPTION_KEY safe — it encrypts your stored secrets.)
Every service image defaults to :latest. For anything you intend to keep
running, pin one release in the same .env — ONECLI_VERSION=v2.0.0 pins
all services at once, and the agent sandbox image follows it
(ghcr.io/onecli/onecli-agent:$ONECLI_VERSION) unless you point
RUNNER_AGENT_IMAGE somewhere else. Upgrades are then a deliberate edit +
docker compose up -d, never a surprise pull. On every up, a one-shot
migrations service (its own small image, onecli-migrations, pinned by the
same ONECLI_VERSION so schema and code always move together) applies any
pending database migrations before the api starts (view its output with
docker compose logs migrations; re-run it with
docker compose up -d or explicitly docker compose run --rm migrations). If
a migration fails, the stack refuses to start rather than serving against a
half-migrated schema — fix the cause, then up -d again. When upgrading an
install created by install.sh, re-run the installer rather than a bare
docker compose pull, so the compose file itself stays current alongside the
images. Sizing a host for hosted
agents (memory per sandbox, the held-awake ceiling) is covered in
apps/runner/README.md.
The first visit asks you to create an account — email and password. Registration stays open by design: every new account gets its own organization, fenced from everyone else's, so a stranger signing up takes nothing from existing users. Joining somebody else's organization goes through an invitation; registering without one starts a fresh org. A deployment that must not accept strangers keeps its dashboard behind the network boundary — there is deliberately no registration switch. (The one narrow refusal: sign-ups are blocked during a pre-2.0 upgrade window until the legacy account is adopted.)
Setting GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET adds a "Continue with
Google" button beside the password form (redirect URI:
<API_URL>/auth/callback/google).
A hosted agent needs a granted model key. The order matters: store the key, grant it to the agent, then chat. A sandbox will not start without one — the agent answers in the thread telling you so.
You can change your password any time from Account → Preferences, which also signs out every other session.
Your existing organization, workspaces, agents and API keys move to the
account you create — nothing to migrate by hand. Register immediately
after upgrading: the old install's data is handed to the first account
that registers, so on a reachable host don't let a stranger get there first
(that release let in anyone who could reach it; creating your account is
what ends that). If your .env still sets NEXTAUTH_SECRET, rename it to
BETTER_AUTH_SECRET; everyone signs in again once.
| Variable | Description | Default |
|---|---|---|
DATABASE_URL |
PostgreSQL connection string | Written by pnpm dev |
BETTER_AUTH_SECRET |
Signs session cookies | Generated by pnpm dev/setup |
SECRET_ENCRYPTION_KEY |
AES-256-GCM key for stored secrets | Generated by pnpm dev/setup |
GATEWAY_INTERNAL_SECRET |
Authenticates the gateway to the API | Generated by pnpm dev/setup |
GOOGLE_CLIENT_ID |
Optional — adds a Google sign-in button | — |
GOOGLE_CLIENT_SECRET |
Optional — Google OAuth client secret | — |
Every required value is generated for you — by pnpm dev into .env for
development, by pnpm run setup into docker/.env for a self-host stack,
and by the install script into ~/.onecli/.env. .env.example documents
the optional settings.
ONECLI_EXTERNAL_URL is the one networking variable most installs set: the
URL people open OneCLI at. Every other address derives from it by one rule:
http://…means ports mode. The api and gateway are advertised on the same host, on their own ports (10256/10255by default).https://…means proxy mode. One origin; your reverse proxy terminates TLS and routes/v1+/authto the api (:10256) and/gw/*(prefix-stripped) to the gateway (:10255); everything else goes to the dashboard (:10254).
The cookie Secure flag, OAuth redirect URIs, the CLI's api-host, install
snippets, emails, Slack buttons, and the links the gateway hands agents all
follow from it. Unset means http://localhost:10254 — correct for a laptop
and for tunnel access.
ONECLI_BIND_HOST is the separate, listen-only knob: which interface the
published ports bind on. The installers detect and persist it; it never
shapes a URL.
| Scenario | What to set |
|---|---|
| Laptop / trying it out | Nothing |
Isolated VM reached via a tunnel (ssh -L, SSM, IAP) |
Nothing — localhost is the correct advertised address; forward ports 10254, 10255 and 10256 |
| Permanent machine on a LAN | ONECLI_EXTERNAL_URL=http://192.168.1.20:10254 and ONECLI_BIND_HOST=0.0.0.0 (or run the wizard, which asks both questions) |
| Public domain behind a TLS proxy | ONECLI_EXTERNAL_URL=https://onecli.example.com — bind stays local, the proxy connects from the same host |
| Split hosts (app + api domains) | ONECLI_EXTERNAL_URL=https://app.example.com plus the API_URL=https://api.example.com override |
| Reachable at two addresses (IP + DNS name) | Add the second one to ONECLI_TRUSTED_ORIGINS (comma-separated) |
Legacy names keep working forever: APP_URL is a read-alias of the canonical
URL (it never derives the api/gateway origins — set those explicitly with
it), API_URL/GATEWAY_API_URL remain per-origin overrides, and
GATEWAY_BASE_URL still feeds the agent proxy address under its new name,
ONECLI_AGENT_PROXY_ADDRESS.
Services bind to localhost (or the docker bridge) by default, so a VM with no
open ports is a first-class deployment: forward the three ports and browse
http://localhost:10254.
ssh -N -L 10254:127.0.0.1:10254 -L 10255:127.0.0.1:10255 -L 10256:127.0.0.1:10256 user@vmOn a bare-metal Linux VM the ports bind to the docker bridge address instead
of loopback (so containers on the host can reach the gateway proxy) — the
install's success output and ~/.onecli/.env record the exact address to
target in -L.
Caddy:
onecli.example.com {
handle /v1/* {
reverse_proxy 127.0.0.1:10256
}
handle /auth/* {
reverse_proxy 127.0.0.1:10256
}
handle_path /gw/* {
reverse_proxy 127.0.0.1:10255
}
handle {
reverse_proxy 127.0.0.1:10254
}
}nginx (the /gw/ trailing slash performs the prefix strip; disable buffering
so token streams and the approvals long-poll flow):
location /v1/ { proxy_pass http://127.0.0.1:10256; proxy_buffering off; }
location /auth/ { proxy_pass http://127.0.0.1:10256; }
location /gw/ { proxy_pass http://127.0.0.1:10255/; proxy_buffering off; }
location / { proxy_pass http://127.0.0.1:10254; }The gateway's CONNECT proxy (agent traffic) is raw TCP on :10255 and cannot
sit behind a path-routing HTTP proxy: agents on other machines need that port
reachable directly (a VPN is the recommended transport), with
ONECLI_AGENT_PROXY_ADDRESS=onecli.example.com:10255 when the control plane
hands out remote containers.