# Deploying Lovelace
Two containers on Azure Container Apps, following the same shape the other
apps in `~/github/one/platform` use:
| Container App | What it is | Ingress | Replicas |
|---|---|---|---|
| `ca-lovelace-gateway` | the Next.js surface | external — the public URL | 1–2 |
| `ca-lovelace-backend` | `jac-serve` | internal only | **1–1, pinned** |
## The constraint that shapes everything
**`jac-serve` holds every repository in memory.** That is milestone-1's stated
scope, not an oversight, and it decides how this can be deployed:
- **The backend is pinned to exactly one replica.** With two, a visitor's
attestation lands in one replica's memory and is invisible from the other;
the same repo would appear settled or unsettled depending on which instance
answered. One replica is the only configuration that is not lying.
- **A restart returns every repo to what is on disk.** Revisions, scale
events, and platform maintenance all restart the container. Anything created
while it ran — an attestation, a remark, an imported repo — is gone.
- **So the deployment is read-mostly by construction.** The histories baked
into the image are durable because they are files; everything a visitor does
is session-scoped and the surface says so.
Persistent stores are the named unblock condition in the architecture. Until
they exist, a deployed Lovelace is a place to *read* provenance, and a local
one is where you *record* it.
## What the image carries
Repos are baked in at `/srv/jack` and loaded at boot via `JAC_HOST_DIR`. Build
a shelf first if you want the deployment to have anything in it:
```bash
cargo run -p jac-serve -- --import-dir ~/github/one --out ./deploy-shelf \
--commits 12 --paths 120
```
Loading verifies every snapshot replays to the address recorded on disk, and
says so loudly when it does not. A jacquard address has provenance and
timestamp hashed into it, so a mismatch means the files and the engine
disagree about what that history *is* — see `crates/jac-serve/src/store.rs`.
The image deliberately does **not** carry whisper.cpp. Local transcription is a
workstation feature; a 150 MB model in a container that scales to zero would be
paid for on every cold start. `WHISPER_MODEL` stays unset, the endpoint answers
503, and the browser falls back to its own recogniser.
## First deploy
The app does not exist in Azure yet. Creating it uses the platform's factory,
which provisions a **management group and a subscription** — real billing, not
a sandbox:
```bash
cd ~/github/one/platform
./create-app.sh --name lovelace --shared-acr --shared-net --shared-kv
```
That scaffolds `~/one/lovelace-infra` with the deploy secrets, and grants
`id-lovelace` AcrPull on the platform registry.
## Every deploy after that
```bash
# From this repo, with the platform SP logged in:
ACR=acrplatform732abfgsg2zsg
TAG=$(git rev-parse --short HEAD)
az acr build --registry "${ACR%%.*}" --image "lovelace-backend:$TAG" --file Dockerfile .
( cd web && npm run build && \
mkdir -p .deploy && cp -r .next/standalone/. .deploy/ && \
mkdir -p .deploy/.next && cp -r .next/static .deploy/.next/static && \
cp -r public .deploy/public && cp Dockerfile .deploy/Dockerfile && \
az acr build --registry "${ACR%%.*}" --image "lovelace-gateway:$TAG" --file Dockerfile .deploy )
```
Then point the Container Apps at the new tags. The backend must keep
`minReplicas: 1, maxReplicas: 1` — see above for why.
## Environment
| Variable | Where | Why |
|---|---|---|
| `JAC_HOST_DIR` | backend | repos to serve; `/srv/jack` in the image |
| `JAC_SERVE_PORT` | backend | 8787, matching every other surface here |
| `S10_INGEST_URL` / `S10_INGEST_KEY` | backend | telemetry; unset means silently disabled |
| `JAC_SERVE_URL` | gateway | the backend's **internal** FQDN |
| `OPENAI_API_KEY` | gateway | Jackie's voice; unset means she stays silent |
`OPENAI_API_KEY` belongs in the app's Key Vault, not in a workflow file. The
browser never sees it — `/speech` proxies, which is why the key can stay server
side at all.