feat: harden podman deployment orchestration

This commit is contained in:
Yahya Alhinai
2026-06-28 01:44:26 +00:00
parent 5185845090
commit c818d5081e
37 changed files with 1796 additions and 273 deletions
+39 -18
View File
@@ -1,60 +1,81 @@
# DigitalOcean App Platform spec for PodMan.
# Deploy: doctl apps create --spec infra/app.yaml
name: podman
region: nyc
static_sites:
- name: web
github:
repo: <org>/Podman
repo: karti-ai/podman
branch: main
deploy_on_push: true
source_dir: frontend
source_dir: /
build_command: corepack enable && pnpm install --frozen-lockfile && pnpm --filter @podman/shared build && pnpm --filter @podman/frontend build
output_dir: dist
output_dir: frontend/dist
index_document: index.html
error_document: index.html
routes:
- path: /
envs:
- key: VITE_BACKEND_URL
scope: BUILD_TIME
value: ${APP_URL}
- key: VITE_LIVEKIT_URL
scope: BUILD_TIME
services:
- name: api
github:
repo: <org>/Podman
repo: karti-ai/podman
branch: main
deploy_on_push: true
source_dir: backend
build_command: corepack enable && pnpm install --frozen-lockfile && pnpm --filter @podman/shared build && pnpm --filter @podman/backend build
run_command: node dist/server.js
source_dir: /
dockerfile_path: infra/Dockerfile
http_port: 8787
instance_size_slug: apps-s-1vcpu-1gb
instance_count: 1
health_check:
http_path: /health
routes:
- path: /api
preserve_path_prefix: true
- path: /health
envs:
- { key: LIVEKIT_URL, scope: RUN_TIME, type: SECRET }
- { key: PODMAN_PROCESS, scope: RUN_TIME, value: server }
- { key: PORT, scope: RUN_TIME, value: '8787' }
- { key: LIVEKIT_URL, scope: RUN_TIME }
- { key: LIVEKIT_API_KEY, scope: RUN_TIME, type: SECRET }
- { key: LIVEKIT_API_SECRET, scope: RUN_TIME, type: SECRET }
- { key: GEMINI_API_KEY, scope: RUN_TIME, type: SECRET }
- { key: GEMINI_VISION_MODEL, scope: RUN_TIME, value: gemini-2.0-flash }
- { key: GEMINI_LIVE_MODEL, scope: RUN_TIME, value: gemini-live-2.5-flash }
- { key: GITHUB_TOKEN, scope: RUN_TIME, type: SECRET }
- { key: GITHUB_REPO, scope: RUN_TIME, value: <org>/<public-repo> }
- { key: GITHUB_REPO, scope: RUN_TIME, value: karti-ai/podman }
- { key: MONGODB_URI, scope: RUN_TIME, type: SECRET }
- { key: VOYAGE_API_KEY, scope: RUN_TIME, type: SECRET }
- { key: VOYAGE_EMBEDDING_MODEL, scope: RUN_TIME, value: voyage-4-lite }
workers:
- name: podman-agent
github:
repo: <org>/Podman
repo: karti-ai/podman
branch: main
deploy_on_push: true
source_dir: backend
build_command: corepack enable && pnpm install --frozen-lockfile && pnpm --filter @podman/shared build && pnpm --filter @podman/backend build
run_command: node dist/agent.js
source_dir: /
dockerfile_path: infra/Dockerfile
instance_size_slug: apps-s-1vcpu-1gb
instance_count: 1
envs:
- { key: LIVEKIT_URL, scope: RUN_TIME, type: SECRET }
- { key: PODMAN_PROCESS, scope: RUN_TIME, value: agent }
- { key: POD_ROOM, scope: RUN_TIME, value: demo-pod }
- { key: LIVEKIT_URL, scope: RUN_TIME }
- { key: LIVEKIT_API_KEY, scope: RUN_TIME, type: SECRET }
- { key: LIVEKIT_API_SECRET, scope: RUN_TIME, type: SECRET }
- { key: GEMINI_API_KEY, scope: RUN_TIME, type: SECRET }
- { key: GEMINI_VISION_MODEL, scope: RUN_TIME, value: gemini-3.5-flash }
- { key: GEMINI_LIVE_MODEL, scope: RUN_TIME, value: gemini-3.1-flash-live-preview }
- { key: GEMINI_VISION_MODEL, scope: RUN_TIME, value: gemini-2.0-flash }
- { key: GEMINI_LIVE_MODEL, scope: RUN_TIME, value: gemini-live-2.5-flash }
- { key: GITHUB_TOKEN, scope: RUN_TIME, type: SECRET }
- { key: GITHUB_REPO, scope: RUN_TIME, value: <org>/<public-repo> }
- { key: GITHUB_REPO, scope: RUN_TIME, value: karti-ai/podman }
- { key: MONGODB_URI, scope: RUN_TIME, type: SECRET }
- { key: VOYAGE_API_KEY, scope: RUN_TIME, type: SECRET }
- { key: POD_ROOM, scope: RUN_TIME, value: demo-pod }
- { key: VOYAGE_EMBEDDING_MODEL, scope: RUN_TIME, value: voyage-4-lite }
+19
View File
@@ -0,0 +1,19 @@
165-22-129-249.sslip.io {
route {
handle /api/* {
reverse_proxy 127.0.0.1:8787
}
handle /health {
reverse_proxy 127.0.0.1:8787
}
root * /var/www/podman
try_files {path} /index.html
file_server
}
}
lk.165-22-129-249.sslip.io {
reverse_proxy localhost:7880
}
+8 -2
View File
@@ -1,8 +1,13 @@
# PodMan backend agent — built from the monorepo root.
# PodMan backend runtime — built from the monorepo root.
# Build: docker build -f infra/Dockerfile -t podman-backend .
# Run API: docker run --env-file backend/.env -e PODMAN_PROCESS=server -p 8787:8787 podman-backend
# Run agent: docker run --env-file backend/.env -e PODMAN_PROCESS=agent podman-backend
FROM node:24-slim AS base
ENV PNPM_HOME=/pnpm
ENV PATH="$PNPM_HOME:$PATH"
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates openssl \
&& rm -rf /var/lib/apt/lists/*
RUN corepack enable
WORKDIR /app
@@ -21,6 +26,7 @@ RUN pnpm --filter @podman/shared build && pnpm --filter @podman/backend build
# Runtime
FROM base AS runtime
ENV NODE_ENV=production
ENV PODMAN_PROCESS=server
COPY --from=build /app /app
EXPOSE 8787
CMD ["node", "backend/dist/index.js"]
CMD ["sh", "-c", "if [ \"$PODMAN_PROCESS\" = \"agent\" ]; then exec node backend/dist/agent.js; else exec node backend/dist/server.js; fi"]
+58 -8
View File
@@ -2,23 +2,51 @@
Deploy targets for PodMan on DigitalOcean.
- `Dockerfile` — builds the Hermes backend from the monorepo root
- `app.yaml` — DigitalOcean App Platform spec (Hermes web service + frontend static site)
- `Dockerfile` — builds the backend runtime image from the monorepo root
- `app.yaml` — DigitalOcean App Platform spec: static site, API service, agent worker
- `systemd/` — local droplet service units for the API and agent worker
Full deploy spec and env var reference in [`docs/digitalocean.md`](../docs/digitalocean.md).
## Local development
```bash
pnpm --filter backend dev # Hermes on :8787
pnpm --filter frontend dev # PWA on :5173
pnpm --filter @podman/backend dev:server
pnpm --filter @podman/backend dev:agent
pnpm --filter @podman/frontend dev
```
## Local container
```bash
docker build -f infra/Dockerfile -t podman-hermes .
docker run --env-file .env -p 8787:8787 podman-hermes
docker build -f infra/Dockerfile -t podman-backend .
docker run --env-file backend/.env -e PODMAN_PROCESS=server -p 8787:8787 podman-backend
docker run --env-file backend/.env -e PODMAN_PROCESS=agent podman-backend
```
## Local production services
On the demo droplet, serve the API and worker with systemd instead of tmux:
```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 systemctl daemon-reload
sudo systemctl enable --now podman-platform-api podman-platform-agent
sudo systemctl status podman-platform-api podman-platform-agent
```
The services expect:
- built backend artifacts in `backend/dist`
- runtime env in `backend/.env`
- Caddy proxying `/api/*` to `127.0.0.1:8787`
Useful checks:
```bash
curl http://127.0.0.1:8787/health
journalctl -u podman-platform-api -u podman-platform-agent -f
```
## DigitalOcean deploy
@@ -27,8 +55,30 @@ docker run --env-file .env -p 8787:8787 podman-hermes
doctl apps create --spec infra/app.yaml
```
Set secret env vars (LiveKit, Gemini, MongoDB) in the DO dashboard after app creation.
Set secret env vars (LiveKit, Gemini, GitHub, MongoDB) in the DO dashboard after app creation.
Run `pnpm deploy:doctor:strict` with the same environment loaded before treating the
deployment as production-ready.
## Droplet/systemd fallback
The `infra/systemd/` units run the compiled API and LiveKit/Gemini agent from
`/root/podman` and load `/root/podman/backend/.env`, matching the current
droplet layout. `pnpm deploy:doctor` also falls back to that file when root
`.env` is absent. Set `FRONTEND_URL` when the static frontend is served from a
different public origin than `VITE_BACKEND_URL`.
The matching Caddy config is in `infra/Caddyfile`; it serves `/var/www/podman`,
proxies `/api/*` and `/health` to `localhost:8787`, and proxies the optional
local LiveKit host.
```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
systemctl status podman-platform-api podman-platform-agent
```
## Fallback (demo safety)
If DO deploy is flaky on stage, run Hermes locally. The PWA defaults to `http://localhost:8787` via `VITE_BACKEND_URL` fallback — no code change needed.
If DO deploy is flaky on stage, run the API and agent locally. In dev, the PWA
defaults to `http://localhost:8787`; in production it falls back to same-origin.
+63 -8
View File
@@ -1,26 +1,81 @@
# DigitalOcean App Platform spec for the PodMan backend.
# Deploy: doctl apps create --spec infra/app.yaml
# DigitalOcean App Platform spec for PodMan.
# Deploy: doctl apps create --spec infra/app.yaml
name: podman
region: nyc
services:
- name: backend
dockerfile_path: infra/Dockerfile
source_dir: /
static_sites:
- name: web
github:
repo: karti-ai/podman
branch: main
deploy_on_push: true
source_dir: /
build_command: corepack enable && pnpm install --frozen-lockfile && pnpm --filter @podman/shared build && pnpm --filter @podman/frontend build
output_dir: frontend/dist
index_document: index.html
error_document: index.html
routes:
- path: /
envs:
- key: VITE_BACKEND_URL
scope: BUILD_TIME
value: ${APP_URL}
- key: VITE_LIVEKIT_URL
scope: BUILD_TIME
services:
- name: api
github:
repo: karti-ai/podman
branch: main
deploy_on_push: true
source_dir: /
dockerfile_path: infra/Dockerfile
http_port: 8787
instance_size_slug: basic-xxs
instance_size_slug: apps-s-1vcpu-1gb
instance_count: 1
health_check:
http_path: /health
routes:
- path: /api
preserve_path_prefix: true
- path: /health
envs:
- { key: PODMAN_PROCESS, scope: RUN_TIME, value: server }
- { key: PORT, scope: RUN_TIME, value: '8787' }
- { key: LIVEKIT_URL, scope: RUN_TIME }
- { key: LIVEKIT_API_KEY, scope: RUN_TIME, type: SECRET }
- { key: LIVEKIT_API_SECRET, scope: RUN_TIME, type: SECRET }
- { key: GEMINI_API_KEY, scope: RUN_TIME, type: SECRET }
- { key: GEMINI_VISION_MODEL, scope: RUN_TIME, value: gemini-2.0-flash }
- { key: GEMINI_LIVE_MODEL, scope: RUN_TIME, value: gemini-live-2.5-flash }
- { key: GITHUB_TOKEN, scope: RUN_TIME, type: SECRET }
- { key: GITHUB_REPO, scope: RUN_TIME }
- { key: GITHUB_REPO, scope: RUN_TIME, value: karti-ai/podman }
- { key: MONGODB_URI, scope: RUN_TIME, type: SECRET }
- { key: VOYAGE_API_KEY, scope: RUN_TIME, type: SECRET }
- { key: VOYAGE_EMBEDDING_MODEL, scope: RUN_TIME, value: voyage-4-lite }
workers:
- name: podman-agent
github:
repo: karti-ai/podman
branch: main
deploy_on_push: true
source_dir: /
dockerfile_path: infra/Dockerfile
instance_size_slug: apps-s-1vcpu-1gb
instance_count: 1
envs:
- { key: PODMAN_PROCESS, scope: RUN_TIME, value: agent }
- { key: POD_ROOM, scope: RUN_TIME, value: demo-pod }
- { key: LIVEKIT_URL, scope: RUN_TIME }
- { key: LIVEKIT_API_KEY, scope: RUN_TIME, type: SECRET }
- { key: LIVEKIT_API_SECRET, scope: RUN_TIME, type: SECRET }
- { key: GEMINI_API_KEY, scope: RUN_TIME, type: SECRET }
- { key: GEMINI_VISION_MODEL, scope: RUN_TIME, value: gemini-2.0-flash }
- { key: GEMINI_LIVE_MODEL, scope: RUN_TIME, value: gemini-live-2.5-flash }
- { key: GITHUB_TOKEN, scope: RUN_TIME, type: SECRET }
- { key: GITHUB_REPO, scope: RUN_TIME, value: karti-ai/podman }
- { key: MONGODB_URI, scope: RUN_TIME, type: SECRET }
- { key: VOYAGE_API_KEY, scope: RUN_TIME, type: SECRET }
- { key: VOYAGE_EMBEDDING_MODEL, scope: RUN_TIME, value: voyage-4-lite }
@@ -0,0 +1,19 @@
[Unit]
Description=PodMan LiveKit/Gemini agent worker
After=network-online.target mongod.service podman-platform-api.service
Wants=network-online.target
[Service]
Type=simple
WorkingDirectory=/root/podman/backend
Environment=NODE_ENV=production
Environment=POD_ROOM=demo-pod
EnvironmentFile=/root/podman/backend/.env
ExecStart=/usr/bin/node dist/agent.js
Restart=always
RestartSec=3
KillSignal=SIGTERM
TimeoutStopSec=20
[Install]
WantedBy=multi-user.target
+18
View File
@@ -0,0 +1,18 @@
[Unit]
Description=PodMan platform API
After=network-online.target mongod.service
Wants=network-online.target
[Service]
Type=simple
WorkingDirectory=/root/podman
Environment=NODE_ENV=production
EnvironmentFile=/root/podman/backend/.env
ExecStart=/usr/bin/node backend/dist/server.js
Restart=always
RestartSec=3
KillSignal=SIGTERM
TimeoutStopSec=20
[Install]
WantedBy=multi-user.target