Quickstart: Self-hosting Dots

Pick a hosting posture, bring the self-contained stack up, point the CLI at your own instance, and prove it answers an authenticated read.

Dots runs on your infrastructure. This page picks where it runs, brings the stack up, points the CLI at it, and proves the instance answers an authenticated read of your own workspace.

Pick A Posture

This is the one choice that decides where your data lives, and three postures ship:

  • Docker self-host — the whole stack on one machine you control, from the committed compose file. Real Postgres with pgvector, a bundled object store, and an in-process identity plane come with it; nothing external is required.
  • Railway self-host — your own Railway project, three services from the committed template descriptor: a private postgres, a publicly routed serve-only app, and a serve-free worker. You get an origin other people can reach, and you bring your own object storage.
  • Bare local daemon — one machine, no containers, no public origin. It is the endpoint the CLI already falls back to when no credential is stored.

Both container postures split serving from background work: the app process runs with its background lifecycle disabled, and a dedicated worker owns the outbox sweep and the durable-job subsystem. Run one without the other and you get an instance that answers requests but never finishes any background work.

Bring The Stack Up

The Docker posture needs no configuration to start. From the repository root you run docker compose -f deploy/self-contained/docker-compose.yml up --build, and the services come up in dependency order: a one-shot secrets service, then Postgres and the object store, then a one-shot init that applies migrations and provisions the bucket, then app and worker together.

Self-host Dots carries the full procedure for every posture, including bringing your own Postgres or S3-compatible storage, the connector keys, and the Railway variant. This page picks up once the stack answers.

Then open http://localhost:3000 and sign up. The first signup becomes the owner and provisions the workspace; no owner credentials ship with the stack. Confirm http://localhost:3000/api/v1/healthz returns a successful response before going on.

What The First Boot Generates

That one-shot secrets service writes six values into a persistent volume: the bundled Postgres password, the identity plane's session-signing secret, the workspace company id, the object store's access key and secret, and the key sealing browser license custody. Each is generated only if absent and never rotated afterwards, so recreating or rebuilding containers reuses them byte for byte. There is nothing here for you to record or rotate.

Treat them as generated defaults rather than operator inputs. The secrets file is sourced so that a value already present in the environment wins, which is exactly how an .env beside the compose file overrides any of them. Two consequences are worth knowing before you reach for that. Postgres applies its password only at initialization, so changing it after the first boot leaves the stack unable to authenticate against its own data directory. And tearing the stack down together with its volumes destroys the secrets alongside the database they unlock — the two are one unit.

For an unattended bring-up rather than a browser signup, set DOTS_INIT_OWNER_EMAIL and DOTS_INIT_OWNER_PASSWORD, and init mints the first owner non-interactively. Set one without the other and init aborts, naming the variable it is missing.

Serve It At A Real Hostname

PUBLIC_APP_URL is the origin the identity plane builds session and invite links against, and it defaults to http://localhost:3000. Plain HTTP is admitted for loopback hosts only, which is why a local evaluation needs no certificate and raises no warning. Point the stack at any other hostname and the origin has to be https:// — the identity configuration refuses to start otherwise, naming the variable it rejected. Set BETTER_AUTH_URL only in the narrower case where the identity origin has to differ from the app's own.

On Railway that variable resolves to the generated public domain of the app service, so the HTTPS requirement is satisfied for you. One value there has to agree by hand: the workspace company id is set on both app and worker, because the worker's composition resolves the same company the app service's pre-deploy init seeded.

Choose The Storage And Chat Seams

The compose stack defaults storage to the bundled object store — path-style addressing, region us-east-1, bucket dots — and init provisions that bucket, so uploads work with no configuration at all. The Railway template defaults to the disk driver instead: it accepts uploads, but onto the app service's own filesystem, which does not survive a redeploy and which the worker cannot see. Set the driver to s3 with the complete credential set to give both services one durable shared sink.

Chat is the one capability that genuinely needs a key from you. The image ships no local agent binaries, so in-app conversations need a remote provider — set OPENROUTER_API_KEY or OPENCLAW_GATEWAY_URL. With neither, the stack still boots, still passes its health probe, and still serves login; only conversations degrade, returning CHAT_UNKNOWN_PROVIDER. ANTHROPIC_API_KEY is a separate concern: it backs AI-fill, image understanding, and auto-titles, and it is not a chat provider.

Point The CLI At Your Instance

By default the CLI talks to whichever API its stored credential names. Point it at the instance you just brought up and store a key for it:

SH
dots auth login --api-url=http://localhost:3000/api/v1

--api-url is a global option rather than a property of login, so it also works per command when you want a single call to hit a different instance without changing what is stored. Login is not a local write: it resolves your identity against the instance before storing anything, so a wrong URL or a bad key fails on the spot instead of persisting a broken record you discover three commands later.

The credential goes into your operating system's keyring when one is available and into a file under your home directory otherwise, written owner-only.

Know Which Endpoint The CLI Is Using

Four sources can name the API, and they resolve in this order:

  1. an explicit --api-url on the command
  2. the base URL inside the stored credential
  3. the API_BASE_URL environment variable
  4. the local daemon at http://127.0.0.1:3687/api/v1

Note the middle pair: a stored credential outranks API_BASE_URL, so exporting that variable does not redirect a CLI that has already logged in somewhere else. The stored API key is sent only when its own base URL matches the one that won; otherwise the CLI falls back to DOTS_API_KEY, and failing that reaches the instance anonymously. Add --verbose to any command and it reports which source supplied the endpoint it used.

SH
dots --version

Prove It Answers

Confirm the API resolves you as a real actor, then that the workspace is provisioned:

SH
dots auth whoami
dots dotabases seed
dots dotabases list

whoami prints the identity your instance resolved for the credential — the user, company, and principal ids, the role and tier it granted, the kind of credential it recognized, and every company you belong to. That last list is why no separate companies command exists.

dotabases seed is idempotent: it provisions the company, the Space estate, and the Foundational Dotabases, and re-running it changes nothing. The call is owner-gated, so a success also proves your credential carries owner authority rather than merely resolving to somebody. Four Foundational Dotabases land — Artifacts, Conversations, HUMAN.md, and Memory Bank — alongside the locked DIIICE Spaces, a General default, and the internal Dotabases the platform's own features sit on.

Reading them back is a survey, so the listing answers at the summary disclosure level and prints a footer naming it. Every field this proof reads — the name and the slug of each — is identity, so it survives even the narrowest --level scan. Reach for --level full only when you want a Dotabase's description, its row-id prefix, or its stored schema, none of which a survey tier carries.

Narrow the proof when the full list is noisy:

SH
dots dotabases list --foundational
dots spaces list

If whoami names you and list returns the Foundational Dotabases, the instance is genuinely serving your own data.

Check Onboarding State

SH
dots onboarding state get

That reads your completion status and the answers already recorded, so you can pick up a partial first run instead of starting it over. The write half takes those answers as JSON, inline or from a file, and requires exactly one of the two.

Issue A Key For An Agent

The family works the same on a self-hosted instance as it does anywhere else:

SH
dots access catalog
dots access agents create reporting-agent
dots access principals list
dots access credentials issue --principal reporting-agent --name nightly-reader

catalog returns the permission atoms your instance recognizes, which doubles as a cheap check that the authority plane seeded alongside the workspace.

The order matters, because the two flags on the last line are not the same kind of value. --name is a free-text label for the credential you are minting. --principal is a lookup: it takes a principal that already exists, either its exact name — matched case-sensitively against the directory principals list prints — or its raw UUID. That is why agents create comes first, and why the name it is given is the name reused on the line below. A reference matching nothing fails as a usage error against the directory, before any credential is minted.

An issued token is surfaced exactly once, with a store-now notice, and the route never returns it again — so capture it on issue. Scope it with --scope and bound it with --expires-at rather than minting a long-lived unscoped key. Issue Scoped Agent Credentials covers that model in full.

Keep The Instance Healthy

Two probes answer different questions, and both are anonymous so a deploy platform can poll them. /api/v1/healthz is liveness: a static payload proving the process answers at all, served ahead of authentication and never rate-limited.

/api/v1/readyz is readiness — the probe the Railway template polls — and it answers whether this container can actually serve. It acquires a database connection under a bounded timeout and reports five components: the database, the job runner, the code graph, the worker, and the connection pools. It returns success once the instance is ready and a service-unavailable status otherwise.

Only the database gates that verdict. A stale code graph or a stopped worker is reported as a degraded component without pulling an otherwise-healthy container out of rotation, because a background staleness is not a reason to stop serving readers. The whole report is memoized for a short window, so a burst of anonymous polls costs one round trip rather than one apiece. Per-component detail strings and the operational counters are operator telemetry and answer on the authenticated admin readiness route instead.

Where To Go Next

Self-host is the full environment reference — every posture, every variable, and the bring-your-own-infrastructure paths. Access and Tenancy explains the roles and the company boundary your instance now enforces. Once the instance is up, Quickstart: Teams brings people into it, and Quickstart: Agents connects an assistant.

Quickstart: Self-hosting Dots · This page in the graph

Connected Records19

Documented by
Mentions
DOT
References
19 connections.

Where To Go Next

Reading this as an agent?For Agents
Something wrong on this page?Dots On GitHub