DocsSelf-hosting

DocsBuild and run

Self-hosting

One container against a PostgreSQL 17 database you own. It applies its own schema on boot and needs no vault, no cache and no object store.


Prerequisites

  • Docker with the Compose plugin, v2 or newer.
  • PostgreSQL 17. The compose file brings up its own. Point DATABASE_URL elsewhere if you already run one. Older majors are not tested against this schema.
  • Node 22.13 or newer, only if you intend to run from source rather than from the image.
  • A public HTTPS origin, if MCP clients will connect from anywhere other than the machine the gateway runs on. Terminate TLS in a reverse proxy in front of the container.

Quickstart

Take the self-hosting compose file, generate the two secrets, write a .env beside it, and bring it up.

Fetch the compose file
curl -fsSL https://toolspoke.com/self-host/compose.yaml -o compose.self-host.yaml
Generate the two secrets. They must be different values.
echo "BETTER_AUTH_SECRET=$(openssl rand -base64 32)"
echo "CREDENTIALS_ENCRYPTION_KEY=$(openssl rand -base64 32)"
.env, next to compose.self-host.yaml
POSTGRES_PASSWORD=a-password-you-choose
BETTER_AUTH_SECRET=...
CREDENTIALS_ENCRYPTION_KEY=...
APP_URL=https://toolspoke.example.com
Start it
docker compose -f compose.self-host.yaml up -d

Open your origin and create an account. The first account creates the workspace around it, with a Default project inside and 5,000 credits on the Free plan. The signed-in product lives under /app.

The environment it requires

Required environment variables
VariableWhat it is
DATABASE_URLThe PostgreSQL connection string. The application refuses to start without it, rather than failing later and further from the cause.
BETTER_AUTH_SECRETSigns sessions and the OAuth access tokens the gateway verifies. Rotating it signs everyone out.
CREDENTIALS_ENCRYPTION_KEYEncrypts every stored credential with AES-256-GCM before it is written. Minimum 32 characters, and deliberately a different value from the auth secret so that rotating one does not destroy the other's data.
NEXT_PUBLIC_APP_URLThe public origin, with no trailing slash. Canonical URLs, the address MCP clients are handed, and the audience stamped into OAuth tokens all derive from it.

Set BETTER_AUTH_URL to the same origin as NEXT_PUBLIC_APP_URL. The compose file sets both from one variable so they cannot drift: a token minted against one origin and verified against another is rejected as the wrong audience.

Everything else is optional, and the feature it configures is unavailable when it is absent. Stripe gives you checkout, and without it every workspace stays on the Free plan. SMTP gives you outbound mail, and without it invitations are still created and their link is copied by hand. An OpenRouter key gives you the toolkit builder and the semantic pass on tool search. Each one is documented inline in the compose file, next to the service that reads it.

Migrations

There is no migration step. The container's entrypoint applies the schema before the server starts listening. The schema is idempotent and takes an advisory lock, so several replicas booting at once is safe and an existing volume is upgraded in place rather than recreated.

Upgrading, once a published image exists to pull
docker compose -f compose.self-host.yaml up -d

Boot also runs the audit retention sweep, which is why a deploy is when expired rows are removed. The default is to keep audit rows indefinitely, which is the honest default for an install running its own database: the operator already holds the disk and the data, and silently deleting their history on upgrade would be the surprising choice. Set a finite window per workspace to bound it.

Checking it came up

GET /api/health
{
  "app": true,
  "database": true,
  "runner": false,
  "embedded": { "toolkits": 0, "of": 31, "tools": 0, "attempted": 0, "lastError": null },
  "commit": null,
  "checkedAt": "2026-08-27T09:12:44.108Z"
}
  • database is a real query, not a ping.
  • runner is whether the sandbox runtime answered. False is expected on a deployment without one.
  • embedded is how much of the catalogue can be searched by meaning rather than by words.
  • commit is which build is answering, baked in at image build time and null when the application runs from a working tree.

The endpoint has no credential in front of it, so it reports counts and never names, and any string long enough to be a secret is scrubbed out of the last embedding error.

Telemetry

A self-hosted install sends an anonymous usage ping to toolspoke.com/api/telemetry on boot and then once a day. The table below is the complete payload: no names, no emails, no URLs, no tool data, and nothing about what the install is connected to. The sender is scripts/telemetry.mjs in the repository, short enough to read rather than trust.

Everything the telemetry ping contains
FieldWhat it is
instanceIdA random UUID minted on first boot and stored in your database. Derived from nothing, and its only job is to let us count one install, still alive, across days.
versionThe app version, so we know which releases are actually running in the wild.
usersA count of user accounts. A number, nothing about who they are.
workspacesA count of workspaces. Same.
One line in your .env turns it off entirely
TOOLSPOKE_TELEMETRY=off

The ping can never affect your install: it is fire-and-forget with a five-second deadline, runs beside the server rather than in any request path, and a receiver that is slow, gone, or blocked by your firewall costs you nothing.

Sandboxed execution needs Harborbox

Toolspoke does not ship a sandbox runtime. Two kinds of tool need one: CLI toolkits, which run a pinned binary with arguments an agent supplies, and stdio MCP servers, which are booted as a process and spoken to over stdin and stdout. Both are executed by Harborbox, which is a separate service with its own deployment.

Point HARBORBOX_URL and HARBORBOX_API_KEY at it and those tools work. Leave them unset and the rest of the product is unaffected.

What works with and without a sandbox runtime
Tool kindWithout HarborboxWith Harborbox
REST API toolkits, over HTTPSWorksWorks
Streamable HTTP MCP serversWorksWorks
CLI toolkitsUnavailableWorks
stdio MCP serversUnavailableWorks

A deployment without Harborbox is a complete gateway for everything reachable over HTTP, which is most of the catalogue. It is not a partial install or a degraded mode. It simply cannot run the tools that need a machine to run on, and it says so: runner is false at /api/health, and a call that needs the sandbox is refused rather than silently doing nothing.

Hardening a public deployment

  • Terminate TLS in a reverse proxy. The container serves plain HTTP on port 3000.
  • Do not publish PostgreSQL to a public interface. The self-hosting compose file does not.
  • Replace every example value. There are no default credentials in this product, and there should be none in your environment either.
  • Review each connector you enable and the scopes it asks for before anyone installs it.
  • Back up the database and the two secrets together.

Once it is up, the next step is connecting an agent. Use your own origin in place of toolspoke.com in both address forms on that page.