> ## Documentation Index
> Fetch the complete documentation index at: https://internal.september.wtf/llms.txt
> Use this file to discover all available pages before exploring further.

# Production deploy

> Step-by-step bring-up of the Engine in production.

This page brings the Engine up in production from a clean infrastructure
slate. It assumes Docker (or any OCI-compatible runtime) and a place to
mount a persistent volume for the brain database.

## 0. Prerequisites

* Container runtime with persistent volume support.
* HTTPS-terminating reverse proxy upstream (Caddy, nginx, ALB, Cloudflare).
* Outbound HTTPS to your LLM provider(s).
* Credentials set up:
  * LLM provider API key.
  * OpenAI API key for embeddings.
  * `AD_ENCRYPTION_KEY` (a Fernet key — see step 3).
  * `ENGINE_KEY_HASH` for the Engine's own auth.

## 1. Build or pull the image

From the engine repo:

```bash theme={null}
docker build -t engine:2.3.0 --target prod .
```

Or pull from your registry. Tag images with the engine version (the
`pyproject.toml` `version` field).

## 2. Provision a volume for the brain

The brain SQLite file lives at `/data/brain.sqlite` inside the container.
Mount a persistent volume there.

| Platform       | How                              |
| -------------- | -------------------------------- |
| Docker Compose | `volumes: ["engine_data:/data"]` |
| Kubernetes     | PVC mounted at `/data`           |
| AWS Fargate    | EFS access point at `/data`      |

Size the volume for at least 1 GB per active user. SQLite handles
multi-GB brains comfortably; backup frequency matters more than ceiling.

## 3. Generate the Asset Directory encryption key

If you'll use any MCP connectors:

```bash theme={null}
python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
```

Store the output as a secret (Vault, AWS Secrets Manager, k8s Secret).
Inject it into the Engine as `AD_ENCRYPTION_KEY`. **Do not commit it.**
**Do not rotate it without a re-encrypt step** — connections encrypted
under the old key will not decrypt.

## 4. Generate the Engine API key hash

Generate a strong API key and its SHA-256 hash:

```bash theme={null}
KEY=$(python -c "import secrets; print(secrets.token_urlsafe(48))")
echo "Plaintext: $KEY"
python -c "import hashlib; print(hashlib.sha256(open('/dev/stdin').read().strip().encode()).hexdigest())" <<< "$KEY"
```

Store the plaintext in your upstream router (BAP) and store the hash as
`ENGINE_KEY_HASH` in the Engine. The plaintext never lives in the
Engine's environment.

## 5. Configure environment

Minimum for production:

```bash theme={null}
# LLM
LLM_PROVIDER=anthropic
LLM_API_KEY=<from secret manager>
LLM_MODEL=claude-sonnet-4-5
OPENAI_API_KEY=<from secret manager>

# Auth
ENGINE_KEY_HASH=<from step 4>

# Asset Directory
AD_ENCRYPTION_KEY=<from step 3>
OAUTH_REDIRECT_BASE_URL=https://engine.your-domain.com

# Storage
SQLITE_DB_PATH=/data/brain.sqlite

# Hardening
ASSET_DIRECTORY_SANDBOXED=true
SUBPROCESS_SCRUB_MODE=strict
ENGINE_LOG_LEVEL=INFO
CORS_ORIGINS=https://app.your-domain.com
```

See [Environment variables](../config/env-vars) for the full catalog.

## 6. Run migrations

Migrations run automatically on Engine startup. Bring the Engine up once
and watch the logs to confirm migrations applied cleanly.

```bash theme={null}
docker run --rm -v engine_data:/data --env-file .env engine:2.3.0
```

If any migration fails, **stop and investigate**. Do not roll forward
with a partial schema.

## 7. Bring up the service

Start the Engine in the foreground first, hit `/health`, then daemonize.

```bash theme={null}
docker run -d \
  --name engine \
  -v engine_data:/data \
  -p 8000:8000 \
  --env-file .env \
  engine:2.3.0
```

Verify:

```bash theme={null}
curl -fsS https://engine.your-domain.com/health
```

You should see `{"status":"ok",...}`.

## 8. Wire up your reverse proxy

Point your HTTPS-terminating proxy at `localhost:8000` (or wherever the
Engine listens). Required:

* TLS termination.
* Pass through `X-Engine-Key` header.
* Long timeouts on `/execute` (SSE streams can run minutes).
* No buffering of the response body (otherwise clients won't see streaming).

Example nginx fragment:

```nginx theme={null}
location / {
    proxy_pass http://localhost:8000;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_buffering off;
    proxy_read_timeout 1h;
    proxy_send_timeout 1h;
}
```

## 9. Schedule backups

The brain database holds the user's entire memory. Back it up.

Options:

* **Volume snapshots.** Daily snapshot of the volume holding `/data`.
  Cheapest. Recovery is point-in-time.
* **`/memory/export`.** Periodic API export to S3 (or equivalent).
  Restorable to a fresh Engine via `/memory/import`.
* **SQLite `.backup`.** Run `sqlite3 brain.sqlite .backup brain-$(date +%s).db`
  inside the container on a schedule. Cheap and incremental-friendly.

Whichever you pick, **test restores quarterly.** A backup you've never
restored is not a backup.

## 10. Configure observability

* Health probe: `GET /health` every 10s.
* Log shipping: configure the container to ship `stdout`/`stderr` to
  your logging stack.
* Metrics: see [Observability](../infra/observability) for what to
  scrape.
* Alerts: see [SLO definitions](../slo/definitions) for the alerts that
  matter.

## You're live

A production Engine is running. Next steps:

* [Set up upgrade rollouts](./upgrade).
* [Plan rollback](./rollback) before you need it.
* Wire alerts to your incident tooling and brief the on-call team.
