# DigitalOcean Deployment Spec

PodMan deploys as three App Platform components from the same monorepo:

- `web`: static Vite/React frontend
- `api`: health-checked HTTP backend on port `8787`
- `podman-agent`: background LiveKit/Gemini worker with no HTTP health check

This split is intentional. The LiveKit agent subscribes to rooms and samples
screen-share frames, so it must run as a worker rather than as a web service.

---

## Canonical Spec

Use [`infra/app.yaml`](../infra/app.yaml):

```bash
doctl apps create --spec infra/app.yaml
```

The mirror at `infra/.do/app.yaml` is kept identical for DO UI/import workflows.

---

## Components

### Static Site: `web`

- Source: monorepo root
- Build:
  `corepack enable && pnpm install --frozen-lockfile && pnpm --filter @podman/shared build && pnpm --filter @podman/frontend build`
- Output: `frontend/dist`
- Routes: `/`
- Build-time env:
  - `VITE_BACKEND_URL`
  - `VITE_LIVEKIT_URL`
  - In the App Platform spec, `VITE_BACKEND_URL=${APP_URL}` keeps frontend API
    calls on the same deployed origin. If it is omitted, the production frontend
    also falls back to same-origin.

### HTTP Service: `api`

- Source: monorepo root
- Dockerfile: `infra/Dockerfile`
- Runtime selector: `PODMAN_PROCESS=server`
- Port: `8787`
- Health check: `/health`
- Routes:
  - `/api` with `preserve_path_prefix: true`
  - `/health`

### Worker: `podman-agent`

- Source: monorepo root
- Dockerfile: `infra/Dockerfile`
- Runtime selector: `PODMAN_PROCESS=agent`
- No HTTP route and no HTTP health check
- Default room: `POD_ROOM=demo-pod`

---

## Required Runtime Environment

```bash
LIVEKIT_URL=wss://your-livekit-server.livekit.cloud
LIVEKIT_API_KEY=...
LIVEKIT_API_SECRET=...

GEMINI_API_KEY=...
GEMINI_VISION_MODEL=gemini-2.0-flash
GEMINI_LIVE_MODEL=gemini-3.1-flash-tts-preview
GEMINI_TTS_VOICE=Charon

GITHUB_TOKEN=...
GITHUB_REPO=karti-ai/podman

MONGODB_URI=mongodb+srv://...
VOYAGE_API_KEY=...
VOYAGE_EMBEDDING_MODEL=voyage-4-lite

PORT=8787
POD_ROOM=demo-pod
```

`VOYAGE_API_KEY` is optional for local/demo fallback. Without it, Mongo exact
signature recall still works; Atlas Vector Search recall is skipped.

---

## Container Checks

Build once:

```bash
docker build -f infra/Dockerfile -t podman-backend .
```

Or use the repository script and verifier:

```bash
pnpm build:container
pnpm verify:containers
```

`verify:containers` uses Docker by default to match `build:container`. To verify
against a Podman image store instead, run
`VERIFY_CONTAINER_RUNTIME=podman pnpm verify:containers`.

The image entrypoint runs `node backend/dist/server.js` when
`PODMAN_PROCESS=server`, and `node backend/dist/agent.js` when
`PODMAN_PROCESS=agent`. Do not run the combined Hermes supervisor inside App
Platform; DO already supervises the service and worker separately.

Run the API:

```bash
docker run --env-file backend/.env -e PODMAN_PROCESS=server -p 8787:8787 podman-backend
```

Run the worker:

```bash
docker run --env-file backend/.env -e PODMAN_PROCESS=agent podman-backend
```

---

## Deploy Checklist

- [ ] `VITE_BACKEND_URL` is `${APP_URL}` or points to the deployed API origin.
- [ ] `FRONTEND_URL` is set for `pnpm deploy:doctor:strict` if the SPA is on a
      different origin than the API.
- [ ] `LIVEKIT_URL` points to the LiveKit Cloud project.
- [ ] LiveKit API key/secret are set for both `api` and `podman-agent`.
- [ ] Gemini API key is set for both backend components.
- [ ] MongoDB Atlas allows DigitalOcean outbound access.
- [ ] `GET /` returns the built frontend HTML and JavaScript bundle.
- [ ] `GET /health` returns `{ "ok": true }`.
- [ ] `GET /api/pods` returns pod data.
- [ ] Worker logs show `podman-hermes joined room demo-pod`.
- [ ] `pnpm deploy:doctor:strict` passes with production env loaded.

Run a non-failing readiness report any time:

```bash
pnpm deploy:doctor
```

Use the strict gate before calling a deployment production-ready:

```bash
pnpm deploy:doctor:strict
```

---

## Local Fallback

```bash
pnpm install --frozen-lockfile
pnpm build
pnpm --filter @podman/backend start:server
pnpm --filter @podman/backend start:agent
pnpm --filter @podman/frontend dev
```

For this droplet deployment, Caddy serves `frontend/dist` from
`/var/www/podman` and proxies `/api/*` to `localhost:8787`.

The systemd fallback units live in `infra/systemd/` and load
`/root/podman/backend/.env` on the current droplet:

```bash
sudo cp infra/systemd/podman-platform-*.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now podman-platform-api podman-platform-agent
```

The droplet production fallback uses systemd units from `infra/systemd/`:

```bash
sudo install -m 0644 infra/systemd/podman-platform-api.service /etc/systemd/system/
sudo install -m 0644 infra/systemd/podman-platform-agent.service /etc/systemd/system/
sudo install -m 0644 infra/systemd/podman-hermes-*.service infra/systemd/podman-hermes-*.timer /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now podman-platform-api podman-platform-agent podman-hermes-watchdog.timer podman-hermes-sync-deploy.timer
```

Expected runtime proof:

```bash
systemctl is-active podman-platform-api podman-platform-agent
systemctl is-active podman-hermes-watchdog.timer
systemctl is-active podman-hermes-sync-deploy.timer
curl http://127.0.0.1:8787/health
journalctl -u podman-platform-agent -n 20 --no-pager
journalctl -u podman-hermes-watchdog -n 40 --no-pager
```

## Hermes-managed operations layer

The app processes are still supervised by systemd, but Hermes now owns the
operations loop around them:

- `pnpm hermes:watchdog` checks systemd services, public routes, `/health`,
  `/api/pods`, and `pnpm deploy:doctor`.
- `podman-hermes-watchdog.timer` runs that watchdog every five minutes.
- `podman-hermes-sync-deploy.timer` polls `origin/main` every two minutes. If
  the tree is clean and the remote moved, it fast-forwards, installs, builds,
  publishes `frontend/dist` to `/var/www/podman`, restarts the API/agent/Caddy,
  and runs the strict watchdog.
- Failed URL checks trigger restarts of the PodMan API, PodMan agent, and Caddy.
- Failed service checks restart only the unhealthy service.
- Caddy is validated and reloaded after public route failures.
- Reports are written to `/var/log/podman/hermes-watchdog-latest.json`.
- Set `PODMAN_ALERT_WEBHOOK_URL` to send failed reports to Discord, Slack, or a
  generic webhook receiver.
- `pnpm hermes:install` installs the timer units and a local pre-push hook that
  gates major pushes with typecheck, lint, and a non-remediating watchdog check.

The strict gate for production readiness is:

```bash
pnpm hermes:watchdog:strict
```

Manual deploy-sync run:

```bash
pnpm hermes:sync-deploy
```
