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

# Docker Compose

> One host, five services, one command. The compose file the desktop app uses, exposed for direct operation.

The compose file at `infra/onprem/docker-compose.yml` is the same one the desktop app runs. Point at it directly when you want Nora on a server or VM without the desktop shell.

## Prerequisites

* **Docker Engine 20.10+** (`docker --version`).
* **The Docker Compose plugin** (`docker compose version`).
* Around **15 GB of free disk** for the container images and Postgres volume.
* **4 GB of RAM available to Docker** (8 GB is more comfortable).

## Quick start

<Steps>
  <Step title="Copy the env template">
    ```bash theme={null}
    cd infra/onprem
    cp .env.example .env.onprem
    ```
  </Step>

  <Step title="Set POSTGRES_PASSWORD">
    Compose refuses to boot without a real password. Open `.env.onprem` and replace `CHANGE_ME_postgres_password` with something you'd treat as a real credential — anything with the compose file can read it.
  </Step>

  <Step title="Bring the stack up">
    ```bash theme={null}
    docker compose --env-file .env.onprem up -d
    ```

    Nora is now serving at `http://localhost:8090`. First boot runs SQL migrations automatically.
  </Step>
</Steps>

## What's in the stack

Five services on an internal Docker network. Only `nora-server` publishes a host port.

| Service | Image | Container name |
| - | - | - |
| `postgres` | `pgvector/pgvector:pg16` | `nora-onprem-postgres` |
| `redis` | `redis:7-alpine` | `nora-onprem-redis` |
| `qdrant` | `qdrant/qdrant:v1.12.4` | `nora-onprem-qdrant` |
| `nora-llm-bridge` | `ghcr.io/conscience-technology/nora-llm-bridge` | `nora-onprem-llm-bridge` |
| `nora-server` | `ghcr.io/conscience-technology/nora-server` | `nora-onprem-server` |

### How images resolve

Both `nora-server` and `nora-llm-bridge` follow the same resolution order:

1. If the image is already present locally (loaded from a tarball, or built by a previous `up`), compose uses it.
2. If the image isn't local but the registry is reachable, compose pulls it from GHCR.
3. If neither works, compose builds from source using the fallback `build:` block. First-time source builds take about 2 minutes for the bridge and 15 – 30 minutes for the server.

Set `NORA_IMAGE_REGISTRY` to point at a private registry — useful when air-gapped operators load the tarball into their own mirror.

## Essential environment variables

Only one variable is required. Everything else has a sensible default.

* **`POSTGRES_PASSWORD`** — the bundled Postgres container's superuser password. Compose refuses to boot without a real value.

The most common overrides:

| Variable | Default | What it does |
| - | - | - |
| `NORA_HOST_PORT` | `8090` | Host port `nora-server` publishes on. |
| `NORA_LICENSE_KEY` | *(empty)* | Paid license activation. Empty means free tier (1 workspace, 3 flows, 100 daily invokes). |
| `NORA_PUBLIC_URL` | `http://localhost:${NORA_HOST_PORT}` | Base URL emitted in MCP responses and OAuth callbacks. Set this when a proxy fronts the stack. |
| `NORA_OAUTH_REDIRECT_BASE` | Same as `NORA_PUBLIC_URL` | OAuth callback base for Google and GitHub sign-in. |
| `NORA_COOKIE_SECURE` | `0` | Set to `1` when TLS terminates at a reverse proxy — the browser needs `Secure` cookies over HTTPS. |
| `NORA_RUN_MIGRATIONS` | `1` | Runs `sqlx migrate run` on boot. Flip to `0` for staged upgrades where a DBA runs migrations manually. |
| `NORA_VECTOR_STORE` | `pgvector` | Switch to `qdrant` and set `NORA_QDRANT_URL` to route retrieval through the Qdrant service instead. |
| `NORA_AIRGAP` | *(empty)* | Set to `1` for isolated deployments. The server exits at boot with code 78 if it can still reach the public internet. |
| `NORA_DEPLOY_FLAVOR` | `onprem` | Leave alone. SaaS-only surfaces rely on this staying `onprem`. |

OAuth and SSO credentials pass through unchanged: `GOOGLE_OAUTH_CLIENT_ID` / `GOOGLE_OAUTH_CLIENT_SECRET`, `GITHUB_OAUTH_CLIENT_ID` / `GITHUB_OAUTH_CLIENT_SECRET`, and provider API keys like `ANTHROPIC_API_KEY`.

## The laptop wrapper

For people who'd rather not memorise compose flags, `infra/onprem/nora-laptop.sh` is a friendlier surface for the same stack.

```bash theme={null}
./nora-laptop.sh start        # Boot the full stack (creates .env.onprem on first run)
./nora-laptop.sh stop         # Stop everything (keeps data volumes)
./nora-laptop.sh restart      # stop → start
./nora-laptop.sh status       # Container states and endpoint URL
./nora-laptop.sh endpoint     # Print just the URL (for scripting)
./nora-laptop.sh logs [svc]   # Follow logs (all services, or one)
./nora-laptop.sh reset        # Stop and drop data volumes (destructive)
```

<Tip>
  `start` generates a random `POSTGRES_PASSWORD` for you on first run and writes it into `.env.onprem`. You can boot without touching the file first. Edit `.env.onprem` later to attach a license key or wire OAuth.
</Tip>

## Where your data lives

* The **`pgdata` volume** holds Postgres data — workspaces, flows, memory, traces, everything transactional.
* The **`qdrantdata` volume** holds the Qdrant vector index, and is only used when `NORA_VECTOR_STORE=qdrant`.
* **`.env.onprem`** holds your configuration.

Back up all three together for a full restore. `docker volume inspect nora-onprem_pgdata` shows where the driver mounts them.

## Upgrade

<Steps>
  <Step title="Point at a newer tag">
    ```bash theme={null}
    export NORA_IMAGE_TAG=onprem-v0.2.0
    ```

    The release pipeline publishes tags to GHCR.
  </Step>

  <Step title="Pull and restart">
    ```bash theme={null}
    docker compose --env-file .env.onprem pull
    docker compose --env-file .env.onprem up -d
    ```

    Migrations run automatically on the new server's first boot.
  </Step>
</Steps>

<Warning>
  Migrations are additive-only. An older server won't start against a newer schema, so plan any downgrade against a matching database snapshot.
</Warning>

## Uninstall

```bash theme={null}
# Stop everything and drop volumes — destructive, all data gone
docker compose --env-file .env.onprem down -v

# Keep the volumes for a later restore
docker compose --env-file .env.onprem down
```

Container images stay in the Docker image store. Remove them with `docker image prune` if you want the disk back.

## Related

<CardGroup cols={2}>
  <Card title="Air-gap tarball" icon="box-archive" href="/local-app/deploy/airgap">
    The same compose file with the images bundled in one archive.
  </Card>

  <Card title="Kubernetes (Helm)" icon="dharmachakra" href="/local-app/deploy/kubernetes">
    The same five services on a cluster with ingress and TLS.
  </Card>

  <Card title="Desktop app" icon="laptop" href="/local-app/desktop/install">
    This compose file wrapped in a native window.
  </Card>
</CardGroup>
