feat: harden podman deployment orchestration
This commit is contained in:
+39
-18
@@ -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 }
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user