-
Notifications
You must be signed in to change notification settings - Fork 1
Deployment
Three deployment shapes are first-class: local Docker, Docker Compose behind Traefik, and Kubernetes with Authentik SSO. Pick the lightest one that fits.
docker run -d \
--name agentpulse \
-p 3000:3000 \
-v agentpulse-data:/app/data \
-e DISABLE_AUTH=true \
ghcr.io/jstuart0/agentpulse:latestThat's it. Open http://localhost:3000.
To persist AI features across restarts, also set:
-e AGENTPULSE_AI_ENABLED=true \
-e AGENTPULSE_SECRETS_KEY=$(openssl rand -hex 32) \AGENTPULSE_SECRETS_KEY must be 32+ characters. Write it down — if you lose it, all encrypted credentials (LLM API keys, Telegram bot token) become unrecoverable. Store it in a password manager.
# compose.yaml
services:
agentpulse:
image: ghcr.io/jstuart0/agentpulse:latest
volumes:
- agentpulse-data:/app/data
environment:
AGENTPULSE_AI_ENABLED: "true"
AGENTPULSE_SECRETS_KEY: "${AGENTPULSE_SECRETS_KEY}"
labels:
traefik.enable: "true"
# Dashboard (behind Authentik)
traefik.http.routers.agentpulse.rule: "Host(`agentpulse.example.com`)"
traefik.http.routers.agentpulse.middlewares: "authentik-forwardauth@file"
traefik.http.routers.agentpulse.tls.certresolver: "le"
# Public hook + webhook paths — NO auth middleware
traefik.http.routers.agentpulse-public.rule: "Host(`agentpulse.example.com`) && (PathPrefix(`/api/v1/hooks`) || PathPrefix(`/api/v1/channels/telegram/webhook`) || PathPrefix(`/setup`))"
traefik.http.routers.agentpulse-public.tls.certresolver: "le"
volumes:
agentpulse-data:Two Traefik routers: one with Authentik forwardauth for the dashboard, one without for paths that need to be publicly reachable (hook ingest, Telegram webhook, setup scripts).
The repo's deploy/k8s/ directory has the manifests used in production:
00-namespace.yaml-
01-secret-template.yaml— env vars +AGENTPULSE_SECRETS_KEY(create this out-of-band, never commit). -
02-configmap.yaml— non-secret config. -
03-pvc.yaml— persistent volume claim for/app/data(Ceph-backed in the reference setup). -
04-deployment.yaml— deployment (SQLite default; Postgres overlay enables multi-replica). -
05-service.yaml— ClusterIP. -
06-middleware.yaml— Traefik forwardauth + https-redirect middleware. -
07-ingressroute.yaml— multiple routes: public hooks, public Telegram webhook, public setup scripts, protected dashboard.
kubectl create secret generic agentpulse-secrets \
-n agentpulse \
--from-literal=AGENTPULSE_SECRETS_KEY=$(openssl rand -hex 32)
kubectl apply -k deploy/k8s/As of v0.4.0, multi-replica deployments are supported via the deploy/overlays/postgres/ Kustomize overlay. The overlay:
- Sets
DATABASE_URLto a Postgres connection string (fill insecret-patch.yaml, gitignored). - Removes the SQLite backup sidecar.
- Switches the deployment strategy to
RollingUpdate.
Rolling deploys are safe: each replica acquires a session-level pg_advisory_lock on its migration client connection before running Drizzle migrations. Two replicas booting simultaneously serialize on that lock; no external coordination needed.
# Fill in deploy/overlays/postgres/secret-patch.yaml (see the .example file)
kubectl apply -f deploy/overlays/postgres/secret-patch.yaml -n agentpulse
kubectl apply -k deploy/overlays/postgres/See deploy/overlays/postgres/README.md for the full pre-flight checklist.
Connection pool tuning: AGENTPULSE_PG_POOL_MAX (integer [1, 100], default 10). Size based on your Postgres max_connections and replica count.
SQLite + WAL continues to handle single-pod deployments fine. Use the base manifests (kubectl apply -k deploy/k8s/) for single-replica SQLite deployments.
| Name | Default | Required | Description |
|---|---|---|---|
PORT |
3000 |
Bun HTTP port. | |
HOST |
0.0.0.0 |
Bind address. | |
DISABLE_AUTH |
unset |
true skips all dashboard + hook auth. Local only. |
|
DATABASE_URL |
"" (SQLite) |
Postgres connection string. When set to a postgres://... URL, AgentPulse uses PostgreSQL. Leave unset for SQLite. Example: postgres://agentpulse:pw@host:5432/agentpulse?sslmode=require. |
|
AGENTPULSE_PG_POOL_MAX |
10 |
Postgres connection pool size. Integer [1, 100]. Only relevant when DATABASE_URL is set. |
|
AGENTPULSE_LEGACY_INIT |
unset | SQLite existing-install path. Set "false" to force Drizzle migrate on an existing SQLite install. |
|
DATA_DIR |
/app/data |
SQLite file location (ignored when Postgres is active). | |
PUBLIC_URL |
unset | for webhook mode | Canonical external URL. If unset, the Telegram wizard accepts the browser's window.location.origin. |
AGENTPULSE_AI_ENABLED |
false |
to enable AI | Compile-time toggle for the whole AI control plane. |
AGENTPULSE_SECRETS_KEY |
unset | if AI is on | 32+ char key used to encrypt LLM API keys, Telegram tokens, webhook secrets. Losing it breaks all stored credentials. |
AGENTPULSE_ALLOW_SIGNUP |
true |
First-run admin signup when users table is empty. Set false to disable. |
|
AGENTPULSE_LOCAL_ADMIN_USERNAME |
unset | Bootstrap admin — recreated each boot from env. | |
AGENTPULSE_LOCAL_ADMIN_PASSWORD |
unset | Paired with above. 12+ chars required. | |
AGENTPULSE_OTEL_ENDPOINT |
unset | OpenTelemetry collector URL for ai_metric events. |
|
AGENTPULSE_TELEMETRY |
on |
Set off to opt out of anonymous usage pings. |
|
TELEGRAM_BOT_TOKEN |
unset | Legacy bootstrap — prefer the in-app paste-token wizard. | |
TELEGRAM_WEBHOOK_SECRET |
unset | Same — legacy. |
The Kubernetes manifests use three Traefik middlewares to wire Authentik SSO for the dashboard. The chain runs in this order on every protected route:
-
agentpulse-strip-client-authentik— removes anyX-Authentik-*headers the client supplied, preventing header-forgery attacks. -
agentpulse-forwardauth— Authentik validates the session and injects identity headers (X-authentik-username,X-authentik-email, etc.) in its response; Traefik copies them upstream. -
agentpulse-inject-verify— Traefik appends theX-Authentik-Verifyshared secret. AgentPulse verifies this value againstAGENTPULSE_AUTHENTIK_TRUST_SECRETbefore admitting the identity.
The inject-verify middleware is the key piece: Authentik's Proxy Property Mappings populate JWT
id_token claims, not forwardauth response headers, so the header injection must happen at the Traefik
layer instead. See deploy/k8s/AUTHENTIK-FORWARDAUTH.md in the repo for the full setup steps,
Kustomize overlay pattern for the shared secret, and secret rotation procedure.
Setup summary:
# 1. Generate shared secret
SECRET=$(openssl rand -hex 32)
# 2. Add to the agentpulse Secret
kubectl -n agentpulse patch secret agentpulse-secrets \
--type='json' \
-p='[{"op":"add","path":"/data/AGENTPULSE_AUTHENTIK_TRUST_SECRET","value":"'"$(echo -n "$SECRET" | base64)"'"}]'
# 3. Inject the same value into agentpulse-inject-verify via your private Kustomize overlay
# (do not commit it to the base manifest — base uses an empty placeholder)
# 4. Apply and restart
kubectl apply -k deploy/k8s-homelab/
kubectl -n agentpulse rollout restart deployment/agentpulse| Path | Why |
|---|---|
/api/v1/health |
Liveness / startup probes. |
/api/v1/ready |
Readiness probe — kubelet has no Authentik session. |
/api/v1/hooks |
Hook ingest. Authenticated by API key bearer. |
/api/v1/hooks/status |
Semantic status updates. Same auth. |
/assets/* |
Vite-built JS/CSS chunks. Browsers need these before any auth session exists; Authentik 302s break Vite's dynamic import with a MIME mismatch. |
/api/v1/auth/me, /api/v1/auth/login, /api/v1/auth/logout, /api/v1/auth/signup
|
Login-page bootstrap and local-account auth flows. The login page calls /auth/me to detect whether the user is already signed in. |
/api/v1/channels/telegram/webhook |
Telegram bot callback. Authenticated by HMAC secret header. |
/setup.sh, /setup-relay.sh, /install-local.sh, /install-local.ps1
|
Setup scripts. Anyone who can reach the server can pull them. |
/api/v1/supervisors/* |
Supervisor agents on remote machines have no Authentik session; per-endpoint requireSupervisorAuth() is the auth boundary. |
/api/v1/auth/change-password is intentionally not bypassed — it enforces requireAuth() in-handler.
Everything else must be behind whatever dashboard auth you use (Authentik, local accounts, DISABLE_AUTH).
One file: /app/data/agentpulse.db (plus .db-shm and .db-wal during operation). Stop the container, copy it, start it back. Or use SQLite's .backup command for online backups.
The encrypted credentials are useless without the AGENTPULSE_SECRETS_KEY — store it somewhere separate from your DB backups.
Pull the new image, run the old container down, start the new one up. Migrations are idempotent (CREATE TABLE IF NOT EXISTS + ALTER TABLE ADD COLUMN with retry-on-lock). No downtime migration step required.
SQLite: rolling k8s upgrades work but hit brief write contention during the migration window — the migration loop retries locks with exponential backoff so the new pod never starts against a stale schema. For this reason, the SQLite base manifests use Recreate strategy.
Postgres: rolling upgrades are safe (no write contention during migration) because the pg_advisory_lock serializes replica boot across the migration step. The Postgres overlay uses RollingUpdate.
-
Health —
GET /api/v1/health. -
Logs — structured JSON on stdout (one
{"type":"ai_metric",...}per watcher run, standard Hono access logs). -
OTel — set
AGENTPULSE_OTEL_ENDPOINTto forward metrics + traces to your collector. -
Diagnostics —
GET /api/v1/ai/diagnosticsreturns queue depth, flags, provider reachability.