# Deploy (/ship/deploy)

## Measure first [#measure-first]

```bash
cuelo machine check
```

This streams real speech through your actual turn-taking stack at rising
concurrency, and reports how many calls stay real-time. Inside a project it
uses the project's controllers. Elsewhere it uses the default stack. It then
sizes worker processes from your CPU and memory and checks them running
together.

```bash
cuelo machine check --single        # one process only
cuelo machine check --seconds 15    # longer per load level
```

What we measured on an M3 Pro (11 cores, 5 performance):

| Stack                                                     | Per process | Whole machine       | Memory / process |
| --------------------------------------------------------- | ----------- | ------------------- | ---------------- |
| Default (turn-1-mini + MaAI barge-in + MaAI backchannels) | 5 calls     | 8 (4 workers × 2)   | ~1 GB            |
| Light (`backchannel-detector` + `vad` backchannels)       | 64 calls    | 72 (2 workers × 36) | ~160 MB          |

The Python GIL caps one process at about 4 cores. Beyond that, add workers.

Speech and LLM run at the providers, so their cost doesn't depend on your
machine. The turn-taking models run locally.

## Serve [#serve]

```bash
cuelo serve                     # serve.host, serve.port, serve.workers from config.cuelo
cuelo serve --port 8080
```

```yaml title="config.cuelo"
serve:
  host: 0.0.0.0
  port: 4141
  workers: 4 # from cuelo machine check
  api_keys: ["${CUELO_KEY}"] # required on a public host
  cors: ["https://app.example.com"]
```

Each worker is a separate process with its own calls and models. A call stays
on the worker its WebSocket landed on. Under `cuelo-serve` directly, use
`--workers N` or `CUELO_WORKERS`.

## Checklist [#checklist]

- **API keys** set (the server refuses a public host without them). See [Security](/ship/security).
- **TLS** at a reverse proxy or load balancer. Browsers need `wss://` for the
  microphone on non-localhost origins. Enable WebSocket upgrades and raise
  idle timeouts above your longest call.
- **Store** on Postgres or MySQL for more than one machine. `store.audio` on a
  persistent volume.
- **Weights** pre-downloaded in your image (`cuelo models pull --all`, or
  `python -m cuelo.controllers.weights`), so the first call doesn't wait.
  `CUELO_MODELS` sets the cache directory (default `~/.cache/cuelo/models`).
- **Health**: `GET /health` returns `{"ok": true, "agent": ...}` without a key.
- **Region**: run close to your STT, LLM and TTS endpoints. Network round
  trips are a large share of latency.

## Container [#container]

```dockerfile title="Dockerfile"
FROM python:3.13-slim
RUN pip install uv
WORKDIR /app
COPY . .
RUN uv sync --frozen
ENV CUELO_MODELS=/app/.models CUELO_TORCH_THREADS=1
RUN uv run python -m cuelo.controllers.weights   # bake turn-taking weights into the image
EXPOSE 4141
CMD ["uv", "run", "cuelo-serve", "generated/pipeline.py", "--host", "0.0.0.0", "--port", "4141"]
```

Pass credentials at runtime (`--env-file .env` or your platform's secrets).
Don't bake them into the image.