> For the complete documentation index, see [llms.txt](https://docs.drizz.dev/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.drizz.dev/twins/self-hosting/docker-compose.md).

# Docker Compose

Run the twins server, Postgres and the cache on one Linux machine with Docker Compose.

Run the twins server, Postgres and the cache on one Linux machine.

{% hint style="info" %}
**Getting the install files.** The Docker Compose file, Helm chart, Terraform modules and twins images are shared with you by Drizz. Ask your Drizz contact for access before you start.
{% endhint %}

|               |                                                                                               |
| ------------- | --------------------------------------------------------------------------------------------- |
| **Runs on**   | One Linux machine with Docker                                                                 |
| **You need**  | The repository's `deploy` folder, Docker Compose and `openssl`                                |
| **Ports**     | `8090` (server), `3000` (console, optional)                                                   |
| **Watch out** | Ports listen on `127.0.0.1` only. Docker opens a published port ahead of the host's firewall. |

## Prerequisites

* A Linux machine with Docker and Docker Compose.
* `deploy/docker-compose.selfhost.yml` and `deploy/selfhost.env.example`, from the repository.
* `openssl`, to make the passwords.
* `uv`, to run the smoke test from the checkout.

## Copy this

```bash
cp deploy/selfhost.env.example deploy/.env
mkdir -m 700 deploy/secrets
openssl rand -hex 24 > deploy/secrets/postgres_password
openssl rand -hex 24 > deploy/secrets/cache_password
chmod 644 deploy/secrets/*
# set TWINS_WORLDS_BASE in deploy/.env to the address others reach this machine at

docker compose -f deploy/docker-compose.selfhost.yml up --build -d
docker compose -f deploy/docker-compose.selfhost.yml exec worlds twins enroll --email you@acme.com
docker compose -f deploy/docker-compose.selfhost.yml exec worlds twins issue --email you@acme.com
curl http://localhost:8090/ready
```

## What runs

| Service    | Image                                                                | What it is                                                                        |
| ---------- | -------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
| `worlds`   | Built from the repository, or a published one named by `TWINS_IMAGE` | The twins server: every world, its MCP endpoint and API, and the operator routes. |
| `postgres` | `postgres:17.11-alpine3.24`, pinned by digest                        | The organizations, people, API keys and the record of what each did.              |
| `cache`    | `valkey/valkey:9.1.2-alpine3.24` (Valkey, BSD-3), pinned by digest   | The request allowance, counted once for every process.                            |
| `console`  | `ghcr.io/drizzdev/twin-console`                                      | Optional (`--profile console`): the web console, signed in to with an API key.    |

## Steps

{% stepper %}
{% step %}

### Write the settings and the passwords

The settings go in `deploy/.env`. The passwords go in files of their own under `deploy/secrets`, so no container's environment (`docker inspect`) holds them.

```bash
cp deploy/selfhost.env.example deploy/.env
mkdir -m 700 deploy/secrets
openssl rand -hex 24 > deploy/secrets/postgres_password
openssl rand -hex 24 > deploy/secrets/cache_password
chmod 644 deploy/secrets/*
```

The files are readable (644) and the folder is yours alone (700). Compose mounts each file into the containers as it is on this machine, owner and mode included. A file only you could read is one Postgres and the server could not read ([Compose: secrets](https://docs.docker.com/reference/compose-file/services/#secrets)).

Open `deploy/.env` and set `TWINS_WORLDS_BASE` to the address others reach this machine at. See [Settings](#settings).
{% endstep %}

{% step %}

### Start it

```bash
docker compose -f deploy/docker-compose.selfhost.yml up --build -d
```

This builds the image and starts the three services. To run a published image instead, set `TWINS_IMAGE` in `deploy/.env` (for example `ghcr.io/drizzdev/twins:0.1.0`).

From a checkout with `make`, `make rebuild ENV=selfhost` does the same.
{% endstep %}

{% step %}

### Make the first key

Sign-in is off (`TWINS_IDENTITY_SESSIONS=false`), so you mint every key on the server. Enroll an address, then issue a key for it:

```bash
docker compose -f deploy/docker-compose.selfhost.yml exec worlds twins enroll --email you@acme.com
docker compose -f deploy/docker-compose.selfhost.yml exec worlds twins issue --email you@acme.com
```

`issue` prints the key once. Copy it now.

* The first address enrolled from a company domain founds that organization and owns it.
* Every later address from that domain joins it as a member.
* An address on a public domain gets an organization of its own.

The key carries every capability the person's role allows. `twins role` moves a person's role or suspends them, which stops every key they hold. `twins organization` does the same for a whole organization. See [Keys and limits](/twins/reference/keys-and-limits.md).
{% endstep %}

{% step %}

### Check it

```bash
curl http://localhost:8090/ready
```

It answers `{"status":"ready"}` once the server can serve. Then run the smoke test with your key:

```bash
TWINS_API_KEY=<key> uv run twins smoke --base http://localhost:8090 --products calendar,gmail
```

`twins smoke` founds a world per product, opens its MCP endpoint, lists and drives every tool, reads its state with a later token and deletes it. Every line should say `pass`.
{% endstep %}

{% step %}

### Use it

The key goes wherever a Drizz key would. The operator routes are under `/operator`. Each one takes `Authorization: Bearer <key>`. To create a Calendar world:

```bash
curl -X POST http://localhost:8090/operator/worlds \
  -H "Authorization: Bearer $TWINS_API_KEY" -H 'content-type: application/json' \
  -d '{"product": "calendar"}'
```

The answer carries each person's MCP URL and token, for example `http://localhost:8090/calendar/<world>/mcp`. Give those to your agent: see [Connect your agent](/twins/using-twins/connect-your-agent.md).

To point the `twin` CLI at your server, keep the key for it:

```bash
export TWIN_SERVER=https://twins.acme.internal
echo "$KEY" | twin login --server "$TWIN_SERVER" --with-key -
twin whoami
```

`twin whoami` says "the twin takes it" once the key is good. `twin login` without `--with-key` says the server offers no sign-in, and how to get a key. In CI, set `TWINS_API_KEY` instead of logging in. See the [CLI reference](/twins/reference/cli.md).
{% endstep %}
{% endstepper %}

## Ports and the network

The ports listen on `127.0.0.1`, this machine alone, by default.

{% hint style="warning" %}
Docker opens a published port ahead of the host's firewall. A port published on every address is reachable whatever ufw or firewalld say ([Docker: packet filtering and firewalls](https://docs.docker.com/engine/network/packet-filtering-firewalls/)). Put a proxy that serves https in front, or set `TWINS_PUBLISH_ADDRESS=0.0.0.0` only on a network you trust.
{% endhint %}

| Port   | Service                                | Setting              |
| ------ | -------------------------------------- | -------------------- |
| `8090` | Twins server                           | `TWINS_PORT`         |
| `3000` | Web console (with `--profile console`) | `TWINS_CONSOLE_PORT` |

The server also serves metrics on port `9464`. The Compose file does not publish it. See [Monitoring](/twins/self-hosting/monitoring.md).

## The web console

The console is opt-in. Set `TWINS_CONSOLE_SECRET` in `deploy/.env` to at least 32 characters, for example from `openssl rand -hex 32`. Then start it with its profile:

```bash
docker compose -f deploy/docker-compose.selfhost.yml --profile console up -d
```

Under 32 characters, the console refuses to start. People sign in to it with an API key. See [Web console](/twins/using-twins/web-console.md).

## Day to day

| Command                                                            | What it does                                  |
| ------------------------------------------------------------------ | --------------------------------------------- |
| `docker compose -f deploy/docker-compose.selfhost.yml logs worlds` | Shows the server's log.                       |
| `make logs ENV=selfhost`                                           | Follows the log.                              |
| `make down ENV=selfhost`                                           | Stops everything and keeps the volumes.       |
| `make clean ENV=selfhost`                                          | Stops everything and removes the volumes too. |

{% hint style="danger" %}
`make clean ENV=selfhost` deletes the `worlds` and `records` volumes: every world, person and key. Take a backup first. See [Backups](/twins/self-hosting/backups.md).
{% endhint %}

## Settings

Set these in `deploy/.env`. `deploy/selfhost.env.example` lists them.

| Variable                                                                          | Needed        | What it does                                                                                                                                                                      |
| --------------------------------------------------------------------------------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `TWINS_WORLDS_BASE`                                                               | In practice   | The address every URL a world hands out is written against. The default, `http://localhost:8090`, reaches only this machine. Set the address others reach it at.                  |
| `TWINS_PORT`                                                                      | No            | The host port. Default `8090`.                                                                                                                                                    |
| `TWINS_PUBLISH_ADDRESS`                                                           | No            | The address the published ports listen on. Default `127.0.0.1`: this machine alone, behind an https proxy. `0.0.0.0` publishes them on the network, ahead of the host's firewall. |
| `OPENAI_API_KEY`                                                                  | No            | Only a world founded from a written `brief` needs a model. A baseline, a `scenario`, a `population` or a `size` needs none.                                                       |
| `TWINS_CONSOLE_SECRET`                                                            | For `console` | At least 32 characters. Seals the console's cookie.                                                                                                                               |
| `TWINS_CONSOLE_PORT`                                                              | No            | The console's host port. Default `3000`.                                                                                                                                          |
| `LOG_LEVEL`                                                                       | No            | The level the server logs at. Default `INFO`.                                                                                                                                     |
| `TWINS_IMAGE`, `TWINS_CONSOLE_IMAGE`, `TWINS_POSTGRES_IMAGE`, `TWINS_CACHE_IMAGE` | No            | Images to run instead of the defaults.                                                                                                                                            |

And in `deploy/secrets`, each file readable (644) in a folder that is yours alone (700):

| File                | Needed  | What it is                                                                                |
| ------------------- | ------- | ----------------------------------------------------------------------------------------- |
| `postgres_password` | **Yes** | The Postgres password, set on its first start. Compose refuses to start without the file. |
| `cache_password`    | **Yes** | The cache's password.                                                                     |

## Next

* [Upgrades](/twins/self-hosting/upgrades.md): move to a new version, and roll back.
* [Backups](/twins/self-hosting/backups.md): keep the two volumes.
* [Monitoring](/twins/self-hosting/monitoring.md): scrape port 9464, or alert on the server's log.

***

*Last updated: 27 October 2026*
