## 1. Project Overview & Quickstart (rivet-dev/HIDDEN)

# HIDDEN

An ultra-light MacOS utility that helps hide menu bar icons

### Repository Details
- **Repository:** [rivet-dev/HIDDEN](https://github.com/rivet-dev/HIDDEN)
- **GitHub Stars:** ⭐ 13,921
- **Primary Language:** Swift

*Note: High-volume repository documentation is actively indexed and synchronized by YakaAI.*

## 2. Official Technical Reference & Guides (rivet-dev/website)

## File: README.md

# Rivet Website

The source for [rivet.dev](https://rivet.dev) — marketing pages, docs, and blog.

Each product's documentation is authored in that product's own repository and
vendored in here. See [`CLAUDE.md`](./CLAUDE.md) for the layout and
[`HIDDEN.md`](./HIDDEN.md) for what is deliberately unpublished.

## Develop

```bash
pnpm install
pnpm assemble   # link each product's docs bundle
pnpm dev
```

## License

[Apache 2.0](./LICENSE)

---

## File: cloud/docs/content/docs/compute.mdx

---
title: "Rivet Compute"
---

Rivet Compute runs your backend for you. You supply a container image; Rivet builds it, pushes it to its own registry, provisions a managed pool, and gives you a URL.

<Note>
Using an AI coding agent? Open **Connect** on the [Rivet dashboard](https://dashboard.rivet.dev), select **Rivet Cloud**, and paste the one-shot prompt into your agent to have it connect for you.
</Note>

## Requirements

- A [Rivet Cloud](https://dashboard.rivet.dev) account and project
- Docker running locally
- Your app. If you do not have one yet, start from the [Quickstart](/actors/docs/quickstart/backend).

## Steps

<Steps>
<Step title="Add a Dockerfile">

At your project root:

```dockerfile @nocheck
FROM node:24-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
CMD ["node", "src/server.js"]
```

Your server must listen on the port in `RIVET_PORT`, which defaults to `3000`.

</Step>
<Step title="Get your cloud token">

1. Open the [dashboard](https://dashboard.rivet.dev) and go to your project.
2. Click **Connect**, then **Rivet Cloud**.
3. Copy `RIVET_CLOUD_TOKEN`.

This is a `cloud_api_*` management token, not a `pk_*` publishable key. See [Tokens](/cloud/docs#tokens).

</Step>
<Step title="Deploy">

```bash
npx @rivetkit/cli deploy --token cloud_api_xxxxx
```

The token is written to `~/.rivet/credentials`, so later deploys can omit the flag. The CLI resolves your project from the token, builds and pushes the image, upserts the managed pool, and prints the deployment URL once the pool is ready.

</Step>
<Step title="Watch the rollout">

The dashboard shows live status:

| Status | Meaning |
| --- | --- |
| Initializing | Starting the runtime environment |
| Deploying | Pulling and launching your container |
| Binding | Connecting the runner to the network |
| Ready | Live, and accepting actor connections |

<LLMOnly>

If you are an agent polling the API rather than watching the dashboard, use the managed-pool endpoint on the Cloud API.

`RIVET_CLOUD_TOKEN` is a `cloud_api_*` token scoped to `cloud-api.rivet.dev`. Send it as `Authorization: Bearer ...`. A `pk_*` publishable key is scoped to the Engine API at `api.rivet.dev` and will 401 here.

Substitute `$CLOUD_API_URL` (usually `https://cloud-api.rivet.dev`), `$PROJECT`, `$ORG`, `$CLOUD_NAMESPACE`, and `$CLOUD_TOKEN`. Poll every 5 seconds until `status` is `ready`. Stop and investigate if `status` is `error`.

```bash
curl -s "$CLOUD_API_URL/projects/$PROJECT/namespaces/$CLOUD_NAMESPACE/managed-pools/default?org=$ORG" -H "Authorization: Bearer $CLOUD_TOKEN"
```

</LLMOnly>

</Step>
<Step title="Add CI">

Once local deploys work, install the GitHub Actions workflow:

```bash
npx @rivetkit/cli setup-ci
```

This writes `.github/workflows/rivet-deploy.yml`. Add the token as a repository secret:

```bash
gh secret set RIVET_CLOUD_TOKEN
```

The workflow deploys production on push to your default branch and creates an isolated namespace per pull request. See [Preview Deployments](/cloud/docs/preview-deployments).

</Step>
</Steps>

## Logs

`rivet logs` prints the last 100 lines from the `default` pool in the `production` namespace, oldest first, then exits.

```bash
npx @rivetkit/cli logs
```

Token resolution matches `deploy`: the `--token` flag, then `RIVET_CLOUD_TOKEN`, then `~/.rivet/credentials`.

Add `--follow` (`-f`) to stream instead of fetching history:

```bash
npx @rivetkit/cli logs --follow
```

Each line is `<timestamp> [<SEVERITY>] <region> <message>`:

```
2026-06-16T18:26:51.160Z [INFO] eu-central-1 server listening on port 3000
2026-06-17T11:24:20.425Z [ERROR] us-east-1 failed to connect to upstream
```

More examples:

```bash
# Last 200 lines from a specific namespace
npx @rivetkit/cli logs --namespace production -n 200

# A specific compute pool
npx @rivetkit/cli logs --pool my-pool

# Live tail, filtered
npx @rivetkit/cli logs --follow --contains error

# JSON, piped to jq
npx @rivetkit/cli logs -n 50 --json | jq .
```

## Troubleshooting

<AccordionGroup>
<Accordion title="Stuck in Initializing">

If the status stays in **Initializing** for more than a few minutes:

- Check that `RIVET_CLOUD_TOKEN` is set correctly as a repository secret.
- Check the GitHub Actions run logs for a failed step.

</Accordion>
<Accordion title="Status shows Error">

The container is failing to start or exiting immediately. Test the image locally with `docker run` first. Common causes:

- The server file never calls `registry.start()`.
- A runtime crash on startup.
- The server is not listening on `RIVET_PORT` (default `3000`).

</Accordion>
</AccordionGroup>

## Pricing

You are billed for the compute your deployment uses while it is running.

<ComputePricing />

---

## File: cloud/docs/content/docs/index.mdx

---
title: "Rivet Cloud"
---

Rivet Cloud runs the control plane for you. You keep writing the same code and deploy it one of two ways:

- **Bring your own compute.** Run workers on your own infrastructure and point them at Rivet Cloud. Every platform in the [Self-Host](/actors/self-host/workers) worker guides works this way.
- **Rivet Compute.** Hand Rivet a container and it runs your backend as well. See [Rivet Compute](/cloud/docs/compute).

Nothing here is required to use Rivet. If you would rather run the control plane yourself, see [Self-Host](/actors/self-host).

## Getting started

1. Create a project at [dashboard.rivet.dev](https://dashboard.rivet.dev).
2. Open **Connect** and pick how you want to deploy.
3. Copy the endpoint values, or the cloud token if you are deploying with Rivet Compute.

## Tokens

Rivet Cloud issues three kinds of token, and mixing them up is the most common source of `401`s.

| Token | Prefix | Scope | Used for |
| --- | --- | --- | --- |
| Secret | `sk_` | Engine API at `api.rivet.dev` | `RIVET_ENDPOINT`, server-side only |
| Publishable | `pk_` | Engine API at `api.rivet.dev` | `RIVET_PUBLIC_ENDPOINT`, safe for browsers |
| Cloud API | `cloud_api_` | Cloud API at `cloud-api.rivet.dev` | `rivet deploy`, CI, managed-pool APIs |

A `pk_` token will fail against the Cloud API, and a `cloud_api_` token will fail against the Engine API. They are not interchangeable.

## In this section

- [Rivet Compute](/cloud/docs/compute): deploy your backend as a container
- [Preview Deployments](/cloud/docs/preview-deployments): an isolated namespace per pull request

---

## File: cloud/docs/content/docs/preview-deployments.mdx

---
title: "Preview Deployments"
---

A preview deployment gives each pull request its own Rivet namespace, so branch work never touches production state. The namespace is created when the PR opens and destroyed when it closes.

Which setup you use depends on where your workers run.

## With Rivet Compute

`setup-ci` writes a workflow that already does this:

```bash
npx @rivetkit/cli setup-ci
gh secret set RIVET_CLOUD_TOKEN
```

The generated `.github/workflows/rivet-deploy.yml` deploys production on push to your default branch, creates a namespace per pull request, posts the preview link as a comment, and cleans the namespace up on close. See [Rivet Compute](/cloud/docs/compute).

## With Vercel

Use the [preview-namespace-action](https://github.com/rivet-dev/preview-namespace-action). It creates the namespace and writes `RIVET_ENDPOINT` and `RIVET_PUBLIC_ENDPOINT` into the Vercel preview environment, so the deployment picks them up with no manual configuration.

<Steps>
<Step title="Add the repository secrets">

- `RIVET_CLOUD_TOKEN` from the [dashboard](https://dashboard.rivet.dev) under **Settings → Advanced → Cloud API Tokens**
- `VERCEL_TOKEN` from [Vercel account settings](https://vercel.com/account/tokens)

</Step>
<Step title="Add the workflow">

```yaml .github/workflows/rivet-preview.yml
name: Rivet Preview

on:
  pull_request:
    types: [opened, synchronize, reopened]
  push:
    branches: [main]

concurrency:
  group: rivet-preview-${{ github.event.pull_request.number || github.ref }}
  cancel-in-progress: true

jobs:
  rivet-preview:
    runs-on: ubuntu-latest
    permissions:
      pull-requests: write
    steps:
      - uses: rivet-dev/preview-namespace-action@v1
        with:
          platform: vercel
          rivet-token: ${{ secrets.RIVET_CLOUD_TOKEN }}
          vercel-token: ${{ secrets.VERCEL_TOKEN }}
```

</Step>
</Steps>

The action also configures the Vercel Deployment Protection bypass header, which is otherwise a common source of `401`s from Rivet into preview deployments. See [Vercel](/actors/self-host/workers/vercel).

## On other platforms

Any platform works, as long as the pull-request build gets endpoint values for a fresh namespace instead of production's. Create the namespace with the Cloud API using a `cloud_api_*` token, inject the resulting endpoints as build-time environment variables, and delete the namespace when the pull request closes.

<Warning>
Never point a preview build at your production namespace. Previews create and destroy actors freely, and that state is shared with whatever else uses the namespace.
</Warning>

## Next steps

- [Rivet Compute](/cloud/docs/compute)
- [Tokens](/cloud/docs#tokens)

---

## File: packages/icons/README.md

<h1 align="center">Rivet Icons</h1>


## ⚠️ Legal Notice

**This package is licensed exclusively for use in Rivet products and services.** Using this package in any other product, project, or commercial application is strictly prohibited and may constitute a violation of Font Awesome's Terms of Service and intellectual property rights.

This package includes icons from Font Awesome Pro, which are licensed to Rivet for use in Rivet products only. If you wish to use Font Awesome Pro icons in your own projects, you must obtain your own license from [Font Awesome](https://fontawesome.com/plans).

## Overview

Rivet Icons is an icon library built on Font Awesome that provides pre-generated SVG icons for use in Rivet products. All icons (including Pro icons) are committed to the repository, making this package work out-of-the-box with **no Font Awesome token required** for end users.

## Installation

```bash
pnpm add @rivet-gg/icons
```

## Usage

```tsx
import { Icon, faCheckCircle } from "@rivet-gg/icons";


```

## Contributing

### Prerequisites

**For maintainers only:** Obtain a Font Awesome Pro license and set the `FONTAWESOME_PACKAGE_TOKEN` environment variable to your Font Awesome Pro token. This is only required when vendoring new icons.

### Adding new icons

1. Ensure you have a `FONTAWESOME_PACKAGE_TOKEN` environment variable set
2. Modify [scripts/generate-manifest.js](scripts/generate-manifest.js) to include new icons
3. Run `pnpm manifest` to generate a new `manifest.json` file
    - If you're getting an error about missing packages, run `pnpm install` in the `src` folder first
4. Run `pnpm vendor` to generate icon files:
    - `src/index.gen.js`
    - `src/index.gen.ts`
    - `dist/index.js`
5. Commit all changes including the generated files

## Troubleshooting

### Icons not showing up

All icons (including Font Awesome Pro icons) are pre-generated and committed to this repository. If icons aren't showing:

1. Make sure you have the peer dependencies installed:
   - `@fortawesome/fontawesome-svg-core`
   - `@fortawesome/free-solid-svg-icons`
   - `@fortawesome/free-brands-svg-icons`
   - `@fortawesome/react-fontawesome`
   - `react` and `react-dom`

2. Check that you're importing icons correctly:
   ```tsx
   import { Icon, faCheckCircle } from "@rivet-gg/icons";

   
   ```

3. If the problem persists, contact us on [Discord](https://www.rivet.dev/discord)

### For Maintainers: `Could not find package @fortawesome/pro-solid-svg-icons`

This error occurs when running the vendor script without access to Font Awesome Pro packages. Make sure:

1. `FONTAWESOME_PACKAGE_TOKEN` environment variable is set
2. Run `pnpm install` in the `src` folder to install FA Pro packages
3. Run `pnpm vendor` again

---

## File: src/content/self-host/control-plane/aws-ecs.mdx

---
title: "AWS ECS"
---

## Requirements

- An AWS account with ECS and Fargate available
- An EFS file system, or an RDS PostgreSQL instance, for persistence
- An Application Load Balancer in front of the service
- AWS Secrets Manager, or SSM Parameter Store, for the admin token

<Note>
A published Terraform module for the control plane does not exist yet. The [AWS ECS template](https://github.com/rivet-dev/template-aws-ecs) covers workers, not the control plane. The steps below are the generic container requirements mapped onto ECS.
</Note>





## Steps

<Steps>

<Step title="Provision storage">

Fargate task storage is ephemeral, so the data directory must live outside the task.

- **Single node:** create an EFS file system and mount it into the task at `/data`.
- **Multi-node:** create an RDS PostgreSQL instance instead and skip the volume. Use the direct endpoint, not RDS Proxy, which is not supported. See <SelfHostLink to="control-plane/storage">Storage</SelfHostLink>.

</Step>

<Step title="Store the admin token">

```bash
aws secretsmanager create-secret \
  --name rivet/admin-token \
  --secret-string "$(openssl rand -hex 32)"
```

Reference it from the task definition's `secrets` block rather than putting it in `environment`.

</Step>

<Step title="Define the task">

Use a pinned image tag, expose both ports, and mount the volume.

```json
{
  "family": "rivet-engine",
  "requiresCompatibilities": ["FARGATE"],
  "networkMode": "awsvpc",
  "cpu": "1024",
  "memory": "2048",
  "containerDefinitions": [
    {
      "name": "rivet-engine",
      "image": "rivetdev/engine:2.3.3",
      "portMappings": [
        { "containerPort": 6420 },
        { "containerPort": 6421 }
      ],
      "environment": [
        { "name": "RIVET__FILE_SYSTEM__PATH", "value": "/data" }
      ],
      "secrets": [
        {
          "name": "RIVET__AUTH__ADMIN_TOKEN",
          "valueFrom": "arn:aws:secretsmanager:REGION:ACCOUNT:secret:rivet/admin-token"
        }
      ],
      "mountPoints": [
        { "sourceVolume": "rivet-data", "containerPath": "/data" }
      ]
    }
  ],
  "volumes": [
    {
      "name": "rivet-data",
      "efsVolumeConfiguration": { "fileSystemId": "fs-XXXXXXXX" }
    }
  ]
}
```

1 CPU and 2 GB of RAM is the recommended minimum per instance.

</Step>

<Step title="Raise the ALB idle timeout">

The ALB default is 60 seconds, which severs long-lived WebSockets and causes reconnect storms.

```bash
aws elbv2 modify-load-balancer-attributes \
  --load-balancer-arn <arn> \
  --attributes Key=idle_timeout.timeout_seconds,Value=3600
```

Point the target group at container port `6420` and terminate TLS on the listener. See <SelfHostLink to="control-plane/ports">Ports</SelfHostLink>.

</Step>

<Step title="Configure health checks">

Set the target group health check path to `/health` on port `6421`, with a five second timeout.

</Step>

<Step title="Verify">

```bash
aws ecs describe-services --cluster <cluster> --services rivet-engine \
  --query 'services[0].deployments[0].rolloutState'
```

Expect `COMPLETED`. Then open `/ui` on the load balancer hostname and log in with the admin token.

</Step>
<Step title="Create the runner config">

A runner config declares a pool that workers connect to. Without one the control plane refuses every worker, so create it before deploying any:

```bash
curl -X PUT "http://<load-balancer-host>/runner-configs/default?namespace=default" \
  -H "Authorization: Bearer $RIVET_ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"datacenters":{"default":{"normal":{}}}}'
```

Use `normal` for long-lived workers. See <SelfHostLink to="workers">Runner configs</SelfHostLink>.

</Step>


</Steps>





## Scaling past one task

EFS-backed file system storage is single-node: RocksDB cannot be shared between tasks. Before raising the desired count above one, move to RDS PostgreSQL and add NATS for pub/sub. See <SelfHostLink to="control-plane/storage">Storage</SelfHostLink>.

Once on RDS, ECS Service Auto Scaling can drive the desired count. Use target tracking on average CPU at 60%, matching the <SelfHostLink to="control-plane/kubernetes">Kubernetes</SelfHostLink> manifests, and keep a minimum of two tasks so the service survives losing one. Leave the desired count at one for as long as the task is on EFS.

## Next steps

- <SelfHostLink to="control-plane/production-checklist">Production checklist</SelfHostLink>
- <SelfHostLink to="workers/aws-ecs">Deploy workers on AWS ECS</SelfHostLink>
- <SelfHostLink to="control-plane/configuration">Configuration reference</SelfHostLink>

---

## File: src/content/self-host/control-plane/backups.mdx

---
title: "Backup & Restore"
---

Everything the control plane persists lives in its storage backend, so backing up the control plane means backing up that backend. The engine itself is stateless between restarts and needs no separate backup.

What you must be able to restore:

- **The storage backend.** Actor state, namespaces, and runner configuration.
- **Your configuration.** The config file or environment variables, including the admin token. Losing the admin token locks you out of the dashboard and the API.

## By backend

<Tabs>
<Tab title="PostgreSQL">

Use your provider's automated backups, or `pg_dump` on a schedule. Point-in-time recovery is worth enabling if your provider offers it.

```bash
pg_dump "postgresql://user:password@host:5432/rivet" --format=custom --file=rivet-$(date +%F).dump
```

Restore into an empty database, then start the control plane against it:

```bash
pg_restore --dbname="postgresql://user:password@host:5432/rivet" --clean --if-exists rivet-2026-01-01.dump
```

Managed providers generally do this better than you will. Prefer their automated backups plus a standby replica over a cron job.

</Tab>
<Tab title="File system">

Stop the control plane before copying the data directory. RocksDB is not safe to copy while a process holds it open, and a copy taken from a running node can restore into a corrupt state.

```bash
# Stop the control plane first
tar czf rivet-data-$(date +%F).tar.gz -C /var/lib/rivet data
```

Restore by extracting into the same path and starting the control plane again. If you cannot tolerate the downtime this implies, that is a signal to move to PostgreSQL.

</Tab>
<Tab title="FoundationDB">

FoundationDB has its own backup agent and continuous backup support. Backup topology is covered during enterprise onboarding. Contact [enterprise support](/sales).

</Tab>
</Tabs>

## Test the restore

A backup you have never restored is not a backup. Restore into a scratch environment on a schedule and confirm:

1. The control plane starts and reports healthy on `/health`.
2. The dashboard loads and your namespaces are present.
3. A worker connects and appears under **Runners**.
4. An actor created before the backup still resolves and reads its state.

## Next steps

- <SelfHostLink to="control-plane/storage">Storage backends</SelfHostLink>
- <SelfHostLink to="control-plane/upgrades">Upgrades</SelfHostLink>
- <SelfHostLink to="control-plane/production-checklist">Production checklist</SelfHostLink>

---

## File: src/content/self-host/control-plane/configuration.mdx

---
title: "Configuration"
---

import EngineConfigSchema from "@/components/docs/EngineConfigSchemaClient.astro";

The control plane is the Rivet Engine. It reads configuration from files, from environment variables, or from both.

## Configuration sources

The engine accepts JSON, JSON5, JSONC, YAML, and YML, plus environment variables.

### Environment variables

Prefix a setting with `RIVET__` and separate each level of nesting with `__`. Names are case-insensitive. To set `postgres.url`:

```bash
RIVET__POSTGRES__URL="postgresql://user:password@host:5432/database"
```

### Configuration files

Files are discovered automatically in a platform-specific directory:

| Platform | Path |
| --- | --- |
| Linux | `/etc/rivet/config.json` |
| macOS | `/Library/Application Support/rivet/config.json` |
| Windows | `C:\ProgramData\rivet\config.json` |

Multiple files in the same directory are merged. `/etc/rivet/config.json` and `/etc/rivet/database.json` are loaded together.

Override the search path with `--config`:

```bash
# Load from a specific file
rivet-engine --config /path/to/config.json

# Load from a directory
rivet-engine --config /etc/rivet

# Load multiple paths, merged in order
rivet-engine --config /etc/rivet/base.json --config /etc/rivet/override.json
```

## Reference

<EngineConfigSchema />

### Envoy load balancing

`pegboard.envoy_load_balancer` selects how actors are assigned to envoys. The `hash` strategy uses a hash ring:

```json
{
	"pegboard": {
		"envoy_load_balancer": {
			"hash": {
				"virtual_nodes": 8,
				"samples": 2,
				"max_scan": 16,
				"use_snapshot_read": true
			}
		}
	}
}
```

Use `samples: 1` for a uniform random pick that skips slot reads, or `samples: 2` or higher for power-of-K choices over envoy slot counts. Treat `virtual_nodes` as an operational invariant once envoys have registered: changing it reshuffles the ring.

## Telemetry

The engine exports traces and metrics over OpenTelemetry.

| Variable | Purpose | Default |
| --- | --- | --- |
| `RIVET_OTEL_ENABLED` | Set to `1` to enable export | off |
| `RIVET_OTEL_GRPC_ENDPOINT` | Collector endpoint | `http://localhost:4317` |
| `RIVET_OTEL_SAMPLER_RATIO` | Trace sampling ratio | `0.001` |

## Next steps

- <SelfHostLink to="control-plane/storage">Storage backends</SelfHostLink>
- <SelfHostLink to="control-plane/ports">Ports</SelfHostLink>
- <SelfHostLink to="control-plane/production-checklist">Production checklist</SelfHostLink>

---

## File: src/content/self-host/control-plane/custom.mdx

---
title: "Custom Platform"
---

The control plane is a single container image, `rivetdev/engine`. Any platform that can run it with a persistent volume and a stable address can host it.

## Requirements

Your platform must provide:

- **A container runtime**, or a Linux host you can run the binary on.
- **A persistent volume.** The control plane is stateful. Storage that resets on redeploy will lose actor state.
- **A stable network identity**, so workers keep reaching the same place.
- **Long-lived connections.** Platforms that cap request duration or idle time below an hour will sever WebSockets. This rules out serverless platforms entirely.
- **`SIGTERM` plus a grace period**, so the control plane can drain and flush state before it is killed.





## Steps

<Steps>

<Step title="Generate an admin token">

```bash
openssl rand -hex 32
```

Store it wherever your platform keeps secrets. Without it, the API is unauthenticated.

</Step>

<Step title="Run the image">

Pin the tag. Never run `latest` in production.

```bash
docker run -d \
  --restart unless-stopped \
  -p 6420:6420 \
  -p 6421:6421 \
  -v rivet-data:/data \
  -e RIVET__FILE_SYSTEM__PATH="/data" \
  -e RIVET__AUTH__ADMIN_TOKEN="<your-token>" \
  rivetdev/engine:2.3.3
```

For multi-node, drop the file system variable, set `RIVET__POSTGRES__URL` instead, and add NATS for pub/sub. Do not replicate or autoscale a node that is still on the file system backend: RocksDB is single-node. See <SelfHostLink to="control-plane/storage">Storage</SelfHostLink>.

</Step>

<Step title="Expose the ports">

`6420` carries API, dashboard, and WebSocket traffic and should sit behind TLS. `6421` serves health checks and should stay internal. See <SelfHostLink to="control-plane/ports">Ports</SelfHostLink>.

</Step>

<Step title="Raise the load balancer idle timeout">

Set idle, read, and send timeouts to at least `3600` seconds on every proxy in the path. The 30 to 60 second defaults sever live WebSockets and cause reconnect storms.

</Step>

<Step title="Verify">

```bash
curl -i http://<host>:6421/health
```

Expect `200`. The dashboard is at `/ui` on port `6420`.

</Step>
<Step title="Create the runner config">

A runner config declares a pool that workers connect to. Without one the control plane refuses every worker, so create it before deploying any:

```bash
curl -X PUT "http://<host>:6420/runner-configs/default?namespace=default" \
  -H "Authorization: Bearer $RIVET_ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"datacenters":{"default":{"normal":{}}}}'
```

Use `normal` for long-lived workers. See <SelfHostLink to="workers">Runner configs</SelfHostLink>.

</Step>


<Step title="Connect a worker">

Point a worker at the control plane and confirm it appears under **Runners**:

```bash
RIVET_ENDPOINT="https://default:<admin-token>@your-control-plane.example.com"
```

</Step>

</Steps>





## Troubleshooting

**Workers connect, then drop every 30 to 60 seconds.** The load balancer idle timeout is still at its default.

**State disappears after a deploy.** The volume is not persistent, or the data path does not point at it.

**The dashboard rejects your token.** `RIVET__AUTH__ADMIN_TOKEN` is not reaching the container. Confirm the variable name, including the double underscores.

## Next steps

- <SelfHostLink to="control-plane/configuration">Configuration reference</SelfHostLink>
- <SelfHostLink to="control-plane/production-checklist">Production checklist</SelfHostLink>
- <SelfHostLink to="workers">Deploy your workers</SelfHostLink>

---

## File: src/content/self-host/control-plane/docker-compose.mdx

---
title: "Docker Compose"
---

## Requirements

- Docker with the Compose plugin
- A host with a persistent volume





## Steps

<Steps>

<Step title="Create docker-compose.yaml">

This is a single-node deployment on the file system backend. The named volume is what makes the data survive a restart.

```yaml
services:
  rivet-engine:
    image: rivetdev/engine:latest
    ports:
      - "6420:6420"
    volumes:
      - rivet-data:/data
    environment:
      RIVET__FILE_SYSTEM__PATH: "/data"
      RIVET__AUTH__ADMIN_TOKEN: "${RIVET_ADMIN_TOKEN}"
    restart: unless-stopped

volumes:
  rivet-data:
```

</Step>

<Step title="Generate an admin token">

```bash
echo "RIVET_ADMIN_TOKEN=$(openssl rand -hex 32)" >> .env
```

Without a token the API is unauthenticated. Keep `.env` out of version control.

</Step>

<Step title="Start it">

```bash
docker compose up -d
```

</Step>

<Step title="Verify">

```bash
curl -i http://localhost:6420/health
```

Expect `200`. The dashboard is at `http://localhost:6420/ui`; log in with the admin token.

</Step>

<Step title="Create the runner config">

A runner config declares a pool that workers connect to. Without one the control plane refuses every worker, so create it before adding your app:

```bash
curl -X PUT "http://localhost:6420/runner-configs/default?namespace=default" \
  -H "Authorization: Bearer $RIVET_ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"datacenters":{"default":{"normal":{}}}}'
```

Use `normal` for the long-lived workers this file runs. See <SelfHostLink to="workers">Runner configs</SelfHostLink>.

</Step>

<Step title="Add a worker">

Add your app to the same Compose file. Inside the Compose network it reaches the control plane at `rivet-engine:6420`.

```yaml
  my-app:
    build: .
    environment:
      RIVET_ENDPOINT: "http://default:${RIVET_ADMIN_TOKEN}@rivet-engine:6420"
    depends_on:
      - rivet-engine
    restart: unless-stopped
```

```bash
docker compose up -d
```

Confirm the worker appears under **Runners** in the dashboard.

</Step>

</Steps>





## Using PostgreSQL

Swap the file system backend for PostgreSQL when you outgrow one node.

```yaml
services:
  postgres:
    image: postgres:15
    environment:
      POSTGRES_DB: rivet
      POSTGRES_USER: rivet
      POSTGRES_PASSWORD: rivet_password
    volumes:
      - postgres-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U rivet -d rivet"]
      interval: 5s
      timeout: 5s
      retries: 12
    restart: unless-stopped

  rivet-engine:
    image: rivetdev/engine:latest
    ports:
      - "6420:6420"
    environment:
      RIVET__POSTGRES__URL: postgresql://rivet:rivet_password@postgres:5432/rivet
      RIVET__AUTH__ADMIN_TOKEN: "${RIVET_ADMIN_TOKEN}"
    depends_on:
      postgres:
        condition: service_healthy
    restart: unless-stopped

volumes:
  postgres-data:
```

This file still runs a single engine container, so it is not yet a multi-node deployment. It is the prerequisite for one: the file system backend is single-node RocksDB and cannot be shared, while PostgreSQL can. Multi-node deployments also need NATS. See <SelfHostLink to="control-plane/storage">Storage</SelfHostLink>.

## Using a config file

Mount JSON instead of setting environment variables:

```yaml
    volumes:
      - ./rivet-config.json:/etc/rivet/config.json:ro
      - rivet-data:/data
```

See <SelfHostLink to="control-plane/configuration">Configuration</SelfHostLink> for the full schema.

## Next steps

- <SelfHostLink to="control-plane/production-checklist">Production checklist</SelfHostLink>
- <SelfHostLink to="workers">Deploy your workers</SelfHostLink>
- <SelfHostLink to="control-plane/configuration">Configuration reference</SelfHostLink>