Manifest is a self-hosted gateway in front of the LLM providers you already pay for. It gives you one OpenAI-compatible endpoint, a dashboard for keys, routing and fallbacks, and a record of what was actually sent and returned.
This chart follows the upstream Docker Compose deployment
(docker/docker-compose.yml,
docker/.env.example)
closely enough that a Compose install maps onto it one setting at a time — see the
environment variable mapping below. It carries over the
same container hardening: a read-only root filesystem, all capabilities dropped, no
privilege escalation, an in-memory /tmp, and the same 1 GiB memory ceiling.
Two things are deliberately different from Compose:
- The chart does not deploy PostgreSQL.
manifest.database.urlpoints at a database you run — an operator such as CloudNativePG, a managed instance, or a plain StatefulSet. A bundled single-replica database is a worse deal in Kubernetes than in Compose: it hides the backup and the major-version upgrade, which are the two things you actually need to own. Migrations still run by themselves when the application boots. - The chart never generates secrets. See Secrets.
Not included, on purpose: no NetworkPolicy (a gateway needs egress to every provider on
the internet, so the policy would be decorative), no PodDisruptionBudget and no
HorizontalPodAutoscaler (the default is a single replica, and scaling out has real
prerequisites — see High availability). The
Compose file's pids_limit: 512 has no pod-level equivalent and is a node setting in
Kubernetes.
Installation#
From the Helm repository:
helm repo add rgielen https://rgielen.github.io/charts
helm repo update
helm install my-manifest-llm-gateway rgielen/manifest-llm-gateway --version 2.11.0
Or directly from the OCI registry:
helm install my-manifest-llm-gateway oci://ghcr.io/rgielen/charts/manifest-llm-gateway --version 2.11.0
Source Code#
Values#
Workload#
| Key | Type | Default | Description |
|---|---|---|---|
| affinity | object | {} |
Affinity rules for pod assignment. |
| containerSecurityContext | object | {"allowPrivilegeEscalation":false,"capabilities":{"drop":["ALL"]},"readOnlyRootFilesystem":true} |
Container-level security context. Mirrors the read_only, cap_drop: ALL and no-new-privileges hardening of the upstream compose file. The compose file's pids_limit: 512 has no pod-level equivalent in Kubernetes and is configured on the node instead. |
| fullnameOverride | string | "" |
Overrides the full name of the generated resources. |
| livenessProbe | object | {"failureThreshold":3,"httpGet":{"path":"/api/v1/health","port":"http"},"periodSeconds":30,"timeoutSeconds":5} |
Liveness probe. |
| nameOverride | string | "" |
Overrides the chart name used in resource names. |
| nodeSelector | object | {} |
Node selector for pod assignment. |
| podAnnotations | object | {} |
Extra annotations for the pod. |
| podDisruptionBudget | object | {"enabled":false,"maxUnavailable":1,"minAvailable":""} |
Voluntary-disruption budget. Off by default because it does nothing useful for a single replica and actively blocks node drains for one; with more than one replica you want it, and the install notes say so. |
| podDisruptionBudget.enabled | bool | false |
Create a PodDisruptionBudget. |
| podDisruptionBudget.maxUnavailable | int | 1 |
Maximum unavailable pods. |
| podDisruptionBudget.minAvailable | string | "" |
Minimum available pods. Takes precedence over maxUnavailable when set. |
| podLabels | object | {} |
Extra labels for the pod. |
| podSecurityContext | object | {"fsGroup":65532,"runAsGroup":65532,"runAsNonRoot":true,"runAsUser":65532,"seccompProfile":{"type":"RuntimeDefault"}} |
Pod-level security context. UID/GID 65532 is the user the upstream image already runs as. |
| priorityClassName | string | "" |
Priority class for the pod. |
| readinessProbe | object | {"failureThreshold":3,"httpGet":{"path":"/api/v1/health","port":"http"},"periodSeconds":10,"timeoutSeconds":3} |
Readiness probe. The health endpoint answers 503 while the process drains on SIGTERM, which takes the pod out of the Service before it stops. |
| replicaCount | int | 1 |
Number of application replicas. More than one needs S3-backed request recordings rather than a ReadWriteOnce volume, and has consequences for rate limiting and threshold alerts that the chart cannot fix — read the high-availability section of the chart README before raising it. |
| resources | object | {"limits":{"memory":"1Gi"},"requests":{"cpu":"100m","memory":"512Mi"}} |
Resource requests and limits. No CPU limit on purpose: CFS throttling on a streaming proxy shows up directly as a worse time-to-first-token. The memory limit mirrors the mem_limit: 1g of the upstream compose file. |
| serviceAccount | object | {"annotations":{},"automountServiceAccountToken":false,"create":true,"name":""} |
Service account used by the pod. |
| serviceAccount.annotations | object | {} |
Annotations for the service account. |
| serviceAccount.automountServiceAccountToken | bool | false |
Mount the service account token. The application never calls the Kubernetes API, so this stays off. |
| serviceAccount.create | bool | true |
Create a dedicated service account. |
| serviceAccount.name | string | "" |
Name of the service account. Generated from the release name when empty. |
| startupProbe | object | {"failureThreshold":36,"httpGet":{"path":"/api/v1/health","port":"http"},"periodSeconds":5} |
Startup probe. Generous by design: a cold start runs database migrations and warms the pricing cache, which the upstream compose file gives 90 seconds. |
| terminationGracePeriodSeconds | int | 30 |
Grace period for the pod to finish in-flight requests. Keep this above manifest.shutdownDrainMs (which is milliseconds), or the kubelet kills the process mid-drain. |
| tmpDir | object | {"sizeLimit":"64Mi"} |
Size of the in-memory /tmp volume. The container runs with a read-only root filesystem, so /tmp has to be mounted separately. |
| tolerations | list | [] |
Tolerations for pod assignment. |
| topologySpreadConstraints | list | [] |
Topology spread constraints for pod assignment. |
| updateStrategy | object | Recreate with a ReadWriteOnce volume, surge-first RollingUpdate otherwise |
Deployment update strategy. Left empty, the chart picks one: Recreate while persistence.enabled uses a ReadWriteOnce claim the replicas cannot share, and a surge-first RollingUpdate (maxUnavailable: 0) otherwise — which is what keeps a multi-replica install available across an upgrade. |
Extensibility#
| Key | Type | Default | Description |
|---|---|---|---|
| extraEnv | list | [] |
Extra environment variables, in Kubernetes env form. Applied last, so they override everything the chart sets — the escape hatch for upstream settings this chart does not model, such as FRAME_ANCESTORS. |
| extraEnvFrom | list | [] |
Extra envFrom sources, applied after the chart's own ConfigMap and Secret but before extraEnv. |
| extraObjects | list | [] |
Extra manifests to render alongside the chart. Each entry is a full Kubernetes object and is passed through tpl. |
| extraVolumeMounts | list | [] |
Extra volume mounts for the container. |
| extraVolumes | list | [] |
Extra volumes for the pod. |
Networking#
| Key | Type | Default | Description |
|---|---|---|---|
| httpRoute | object | {"annotations":{},"enabled":false,"hostnames":[],"matches":[{"path":{"type":"PathPrefix","value":"/"}}],"parentRefs":[]} |
Gateway API route, as an alternative to ingress. Requires the gateway.networking.k8s.io/v1 CRDs in the cluster. |
| httpRoute.annotations | object | {} |
Annotations for the HTTPRoute. |
| httpRoute.enabled | bool | false |
Create an HTTPRoute. |
| httpRoute.hostnames | list | [] |
Hostnames to match. |
| httpRoute.matches | list | [{"path":{"type":"PathPrefix","value":"/"}}] |
Rule matches. The default sends everything below / to the service. |
| httpRoute.parentRefs | list | [] |
Gateways to attach to. Each entry is a parentRef and is passed through unchanged (name, and optionally namespace, sectionName, port). |
| ingress | object | {"annotations":{},"className":"","enabled":false,"hosts":[],"tls":[]} |
Ingress for the dashboard and the gateway API. Both live on the same port and the same host. |
| ingress.annotations | object | {} |
Annotations for the Ingress. Streaming responses need a generous read timeout on the controller; the values differ per controller. |
| ingress.className | string | "" |
Ingress class name. |
| ingress.enabled | bool | false |
Create an Ingress. |
| ingress.hosts | list | [] |
Hosts to serve. Each entry takes host and a list of paths (path, pathType). |
| ingress.tls | list | [] |
TLS configuration, passed through unchanged. |
| service | object | {"annotations":{},"port":2099,"type":"ClusterIP"} |
Service in front of the pods. |
| service.annotations | object | {} |
Annotations for the service. |
| service.port | int | 2099 |
Service port. |
| service.type | string | "ClusterIP" |
Service type. |
Image#
| Key | Type | Default | Description |
|---|---|---|---|
| image | object | {"pullPolicy":"IfNotPresent","repository":"manifestdotbuild/manifest","tag":""} |
Image to deploy. The upstream publishes linux/amd64 and linux/arm64. |
| image.pullPolicy | string | "IfNotPresent" |
Image pull policy. |
| image.repository | string | "manifestdotbuild/manifest" |
Image repository. |
| image.tag | string | the chart's appVersion |
Image tag. |
| imagePullSecrets | list | [] |
Secrets used to pull the image from a private registry. |
Manifest: operations#
| Key | Type | Default | Description |
|---|---|---|---|
| manifest.agentUsage.batchSize | int | 250 |
Rows the worker claims per source table per run (AGENT_USAGE_DAILY_BATCH_SIZE). Lower it to spread the initial backfill of an existing database over more, smaller transactions. |
| manifest.agentUsage.dailyWorker | bool | true |
Run the per-agent daily usage rollup worker (AGENT_USAGE_DAILY_WORKER). Since appVersion 6.25.3 the dashboard's agent usage figures are aggregated into agent_usage_daily by a job that fires every minute, instead of being queried from the raw request tables. It is not gated by deployment mode, so it runs here too. Set false to pause it — reads then fall back to the raw tables, which is correct but slower on a large database. |
| manifest.agentUsage.runBudgetMs | int | 5000 |
Work budget in ms for each one-minute run (AGENT_USAGE_DAILY_RUN_BUDGET_MS). The worker stops claiming batches once it is spent and resumes on the next tick. |
| manifest.migrations.job | object | {"activeDeadlineSeconds":900,"annotations":{},"backoffLimit":3,"enabled":true,"podAnnotations":{},"resources":{"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}},"serviceAccountName":""} |
Apply migrations from a pre-install/pre-upgrade hook Job instead of on application boot. The Job runs the upstream's own migration entry point, which wraps the run in a PostgreSQL advisory lock, and it runs exactly once per release — so the schema is in place before any pod starts, and a failed migration is a failed Job with readable logs rather than a pod stuck in CrashLoopBackOff. |
| manifest.migrations.job.activeDeadlineSeconds | int | 900 |
Hard timeout for the whole Job. A first migration on an empty database builds every index and is not instant. Since appVersion 6.25.3 several migrations build indexes on requests and agent_messages with CREATE INDEX CONCURRENTLY, and the upstream deliberately does not gate them by deployment mode — the dashboard and retention queries they serve run self-hosted too. The default fits a small database, where those builds finish in seconds. Raise it for a large one, whatever manifest.mode is set to; upstream budgets minutes per index on tables of millions of rows. |
| manifest.migrations.job.annotations | object | {} |
Extra annotations for the Job object. |
| manifest.migrations.job.backoffLimit | int | 3 |
Retries before the Job is considered failed. |
| manifest.migrations.job.enabled | bool | true |
Create the migration Job. |
| manifest.migrations.job.podAnnotations | object | {} |
Extra annotations for the migration pod. |
| manifest.migrations.job.resources | object | {"limits":{"memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}} |
Resource requests and limits for the migration pod. |
| manifest.migrations.job.serviceAccountName | string | "" |
Service account for the migration pod. Empty uses default, because the chart's own service account does not exist yet when a pre-install hook runs. Point this at a pre-existing account when the migration pod needs an identity of its own — a cloud workload identity for a managed database, say. When serviceAccount.create is false, the configured account already exists and is used automatically. |
| manifest.runMigrationsOnBoot | bool | false |
Also apply migrations when the application boots (RUN_MIGRATIONS_ON_BOOT). Off, because migrations.job already did it. Turning it on with more than one replica is refused: the boot path takes no lock at all, unlike the Job's entry point, so every replica applies the same pending migrations at once — and several of them are a CREATE INDEX CONCURRENTLY that waits for every other session on the table. Concurrent runners hung indefinitely on exactly those builds before appVersion 6.26.0 even with the lock; the upstream's fix covers the Job's entry point only. |
| manifest.shutdownDrainMs | int | 10000 |
Grace period in ms to finish in-flight requests after SIGTERM (SHUTDOWN_DRAIN_MS). Keep terminationGracePeriodSeconds above it. |
| manifest.throttle.limit | int | 100 |
Maximum requests per window per client (THROTTLE_LIMIT). |
| manifest.throttle.ttl | int | 60000 |
Rate limit window in ms (THROTTLE_TTL). |
Manifest: core#
| Key | Type | Default | Description |
|---|---|---|---|
| manifest.apiKey | string | "" |
Optional key for programmatic access through the X-API-Key header (API_KEY). The dashboard uses cookie sessions and agents use their own mnfst_* keys, so this is only needed for scripting the admin API. |
| manifest.auth.encryptionKey | string | "" |
Separate at-rest encryption key for stored provider API keys and OAuth tokens (MANIFEST_ENCRYPTION_KEY). Falls back to auth.secret when empty, which means one leaked session cookie secret also decrypts every stored provider credential. Set a second, independent 32+ character value. Changing this without previousEncryptionKey makes existing stored credentials unreadable. |
| manifest.auth.previousEncryptionKey | string | "" |
The key that encrypted the stored credentials until now (MANIFEST_ENCRYPTION_KEY_PREVIOUS), set only while rotating encryptionKey — or while introducing one on an install that had been falling back to auth.secret, in which case this is that value. While it is set, a pass after boot rewrites every stored provider key, OAuth token, agent key and e-mail provider key onto the new key; remove it once the log reports nothing left under an older secret. Under 32 characters is ignored. Stored recording bodies are not rewritten and do not survive the change. |
| manifest.auth.secret | string | "" |
Session signing secret (BETTER_AUTH_SECRET), at least 32 characters. Generate with openssl rand -hex 32. Required unless existingSecret provides it — the chart refuses to render without one. It is never generated for you: this chart is meant to be rendered by ArgoCD, where lookup returns nothing and a generated value would be different on every sync, taking every stored provider credential with it. |
| manifest.authAllowedHosts | list | [] |
Further hosts this release answers on, besides the one in publicUrl (BETTER_AUTH_ALLOWED_HOSTS). Joined with commas. Since appVersion 6.25.2 the OAuth callback and the session cookie follow the request host instead of publicUrl — but only for hosts listed here; any other host is sent through publicUrl and leaves its cookie on an origin the browser will not send back. Entries are hostnames with an optional port, or full origins (the scheme is ignored), and *.example.com wildcards are accepted; an IPv6 literal has to be bracketed, [::1], as the upstream's URL parser wants it. Leave empty for the usual single-host install: with one host known the upstream keeps the static origin, which is what it did before 6.25.2. The remote MCP endpoint stays bound to publicUrl either way. |
| manifest.cliToken.absoluteTtlDays | string | "" |
Hard ceiling in days from issuance (CLI_TOKEN_ABSOLUTE_TTL_DAYS). The sliding window above renews on every use, so this is what finally retires a token that is in constant use. Empty for the upstream default of 90; set below ttlDays it retires tokens before the sliding window ever matters. Both take a plain positive integer -- upstream ignores 0 and anything like 30d, and silently applies its own default instead. |
| manifest.cliToken.ttlDays | string | "" |
Sliding lifetime in days of a management token minted by the CLI (CLI_TOKEN_TTL_DAYS). Every successful authentication pushes the token's expiry this far out, so a CLI in regular use never has to log in again while an abandoned one lapses. Empty for the upstream default of 30. |
| manifest.corsOrigins | list | [] |
Extra browser origins allowed to call the gateway (WINGMAN_CORS_ORIGINS). Joined with commas. |
| manifest.disableHsts | bool | false |
Silence the boot warning about the missing HSTS header on a plain-http deployment (MANIFEST_DISABLE_HSTS). Prefer a real https:// publicUrl anywhere reachable from the internet. |
| manifest.existingSecret | string | "" |
Name of an existing Secret holding sensitive settings. Its keys are the upstream environment variable names (BETTER_AUTH_SECRET, DATABASE_URL, EMAIL_API_KEY, ...) and it is mounted with envFrom. Takes precedence over the plain values below, which makes it the right choice for GitOps: keep the Secret in sealed-secrets or external-secrets and leave the values here empty. |
| manifest.mcpEnabled | bool | true |
Serve the remote MCP endpoint at /api/v1/mcp, with its OAuth endpoints (MCP_ENABLED). On by default, and the upstream switches it off by itself when publicUrl is plain http on a host that is not loopback — the MCP resource URL is derived from publicUrl and @better-auth/mcp accepts only HTTPS or loopback — naming the reason on every boot. Set this to false to acknowledge that, or to close the MCP surface on an install that could serve it. Only a falsey string disables it upstream, which is what false here becomes. |
| manifest.mode | string | "selfhosted" |
Deployment mode (MANIFEST_MODE). selfhosted relaxes the SSRF rules so private and plain-http provider URLs are allowed. Set explicitly rather than left to auto-detection, exactly as the upstream compose file does. local is the upstream's legacy alias for selfhosted and behaves identically; prefer selfhosted for anything new. |
| manifest.port | int | 2099 |
Port the application listens on (PORT). |
| manifest.publicUrl | string | derived from the first ingress.hosts entry when an Ingress is enabled |
Public URL the dashboard is reached at (BETTER_AUTH_URL). Must match what the browser actually uses, or logins and OAuth callbacks break. No trailing slash — the application appends paths such as /api/auth/... to this value. Prefer https://, but plain http on a LAN or tailnet host is supported: up to appVersion 6.25.2 it left the pod in CrashLoopBackOff, because the upstream derived its MCP resource URL from this value and the MCP plugin rejects a non-HTTPS one while loading. Since 6.25.3 the upstream decides before building that plugin, so such an install boots and simply serves no MCP endpoint. What remains on plain http: no HSTS (see disableHsts) and no remote MCP (see mcpEnabled). |
Manifest: LLM proxy#
| Key | Type | Default | Description |
|---|---|---|---|
| manifest.credits.autoProvisionAllowlist | list | [] |
User e-mail addresses allowed to auto-provision, joined with commas (CREDITS_AUTO_PROVISION_ALLOWLIST). |
| manifest.credits.baseUrl | string | "" |
Base URL of the managed free-provider service (CREDITS_BASE_URL). |
| manifest.credits.geminiFreeMaxBudget | string | "" |
Budget in USD for each generated key (CREDITS_GEMINI_FREE_MAX_BUDGET). |
| manifest.credits.masterKey | string | "" |
Master key enabling automatic virtual-key provisioning (CREDITS_MASTER_KEY). |
| manifest.ollamaHost | string | "" |
Base URL of a locally reachable Ollama or other OpenAI-compatible server (OLLAMA_HOST), for example http://ollama.ai.svc.cluster.local:11434. Empty by default rather than the compose file's host.docker.internal, which does not exist in Kubernetes. |
| manifest.proxy.codexSemanticOutputTimeoutMs | int | 60000 |
Time in ms to wait for deliverable text or tool output from ChatGPT Codex (CODEX_SEMANTIC_OUTPUT_TIMEOUT_MS). |
| manifest.proxy.concurrencyMax | int | 10 |
Per-tenant limit of concurrent in-flight requests per backend process (MANIFEST_CONCURRENCY_MAX). |
| manifest.proxy.ipRateMaxRequests | int | 500 |
Per-client-IP limit of proxy requests per minute per backend process (MANIFEST_IP_RATE_MAX_REQUESTS). Exceeding it answers HTTP 429 with error code M202. |
| manifest.proxy.providerTimeoutMs | int | 180000 |
Per-attempt timeout in ms for upstream provider requests (PROVIDER_TIMEOUT_MS). Keep it below your client's timeout so the fallback chain still has room to run. |
| manifest.proxy.rateMaxRequests | int | 200 |
Per-tenant limit of proxy requests per minute per backend process (MANIFEST_RATE_MAX_REQUESTS). Exceeding it answers HTTP 429 with error code M201. |
| manifest.proxy.streamIdleTimeoutMs | int | 180000 |
Maximum silence in ms between two chunks of an upstream streaming response before the request fails with HTTP 504 (STREAM_IDLE_TIMEOUT_MS). Distinct from streamWarmupMs, which only covers the wait for the first chunk: raise this one for agent workloads that think for a long time between tokens. |
| manifest.proxy.streamWarmupMs | int | 15000 |
Time in ms to wait for the first chunk of a streaming response before treating it as stalled and failing over (STREAM_WARMUP_MS). |
Manifest: database#
| Key | Type | Default | Description |
|---|---|---|---|
| manifest.database.authPoolMax | int | 5 |
Size of the separate pool Better Auth opens (AUTH_DB_POOL_MAX). |
| manifest.database.migrationUrl | string | "" |
Direct, non-pooled connection string used only for migrations (MIGRATION_DATABASE_URL). Falls back to database.url when empty, which is right whenever that is already a direct connection. Set it when database.url points at a transaction pooler such as PgBouncer: the migration runner takes a session-scoped PostgreSQL advisory lock, and transaction pooling does not preserve one. |
| manifest.database.poolMax | int | 10 |
Size of the application's connection pool (DB_POOL_MAX). |
| manifest.database.tuneSession | string | "" |
Statements applied to each new session (DB_TUNE_SESSION). |
| manifest.database.url | string | "" |
PostgreSQL connection string (DATABASE_URL), for example postgresql://manifest:secret@postgres.databases.svc:5432/manifest. Required unless existingSecret provides it. Special characters in the password must be percent-encoded. This chart does not deploy a database; migrations are applied by the application on boot. |
Manifest: email and OAuth#
| Key | Type | Default | Description |
|---|---|---|---|
| manifest.email.apiKey | string | "" |
API key for the provider (EMAIL_API_KEY). |
| manifest.email.domain | string | "" |
Verified sending domain (EMAIL_DOMAIN). Mailgun only. |
| manifest.email.from | string | "" |
Sender address (EMAIL_FROM). |
| manifest.email.provider | string | "" |
E-mail provider (EMAIL_PROVIDER): resend, mailgun or sendgrid. Without one, signup verification is waived, password reset silently does nothing, and threshold alerts only work where a user configured a provider in the dashboard. |
| manifest.oauth | object | {"discord":{"clientId":"","clientSecret":""},"github":{"clientId":"","clientSecret":""},"google":{"clientId":"","clientSecret":""}} |
OAuth logins. A provider activates as soon as both its client ID and secret are set. Register the callback as <publicUrl>/api/auth/callback/<provider>. |
| manifest.oauth.discord.clientId | string | "" |
DISCORD_CLIENT_ID |
| manifest.oauth.discord.clientSecret | string | "" |
DISCORD_CLIENT_SECRET |
| manifest.oauth.github.clientId | string | "" |
GITHUB_CLIENT_ID |
| manifest.oauth.github.clientSecret | string | "" |
GITHUB_CLIENT_SECRET |
| manifest.oauth.google.clientId | string | "" |
GOOGLE_CLIENT_ID |
| manifest.oauth.google.clientSecret | string | "" |
GOOGLE_CLIENT_SECRET |
| manifest.providerOauth | object | {"minimaxClientId":"","openaiClientId":""} |
Overrides for the OAuth clients Manifest uses to talk to LLM providers on a user's behalf. Only needed if you registered your own apps instead of using the ones shipped with Manifest. |
| manifest.providerOauth.minimaxClientId | string | "" |
MINIMAX_OAUTH_CLIENT_ID |
| manifest.providerOauth.openaiClientId | string | "" |
OPENAI_OAUTH_CLIENT_ID |
Manifest: request recordings#
| Key | Type | Default | Description |
|---|---|---|---|
| manifest.recordings.filesystemPath | string | "/data/request-recordings" |
Mount path for locally stored recordings (REQUEST_RECORDING_FILESYSTEM_PATH). Backed by persistence above. |
| manifest.recordings.retentionDays | string | "" |
Retention in days (REQUEST_RECORDING_RETENTION_DAYS). The application defaults to 365 for self-hosted installations. |
| manifest.recordings.s3.accessKeyId | string | "" |
Access key ID (REQUEST_RECORDING_S3_ACCESS_KEY_ID). |
| manifest.recordings.s3.bucket | string | "" |
Bucket name (REQUEST_RECORDING_S3_BUCKET). |
| manifest.recordings.s3.endpoint | string | "" |
Endpoint for S3-compatible storage (REQUEST_RECORDING_S3_ENDPOINT). |
| manifest.recordings.s3.forcePathStyle | bool | false |
Use path-style addressing (REQUEST_RECORDING_S3_FORCE_PATH_STYLE). Needed by MinIO and most other S3-compatible backends. |
| manifest.recordings.s3.region | string | "" |
Region (REQUEST_RECORDING_S3_REGION). |
| manifest.recordings.s3.secretAccessKey | string | "" |
Secret access key (REQUEST_RECORDING_S3_SECRET_ACCESS_KEY). |
| manifest.recordings.storage | string | "auto" |
Where message bodies are stored (REQUEST_RECORDING_STORAGE): auto picks S3 when a complete S3 configuration is present and the local volume otherwise. Metadata always lives in the database; only the bodies are affected by this. |
Manifest: observability#
| Key | Type | Default | Description |
|---|---|---|---|
| manifest.sentry.dsn | string | "" |
Sentry DSN (SENTRY_DSN). Sentry stays completely uninitialised while this is empty. Error capture only — no tracing, request bodies, headers or user data. |
| manifest.sentry.environment | string | "" |
Environment tag (SENTRY_ENVIRONMENT). |
| manifest.sentry.release | string | "" |
Release tag (SENTRY_RELEASE). |
| manifest.telemetry.disabled | bool | false |
Disable the anonymous usage report the upstream sends once per 24h (MANIFEST_TELEMETRY_DISABLED). Aggregates only: no prompts, no message contents, no API keys. |
| manifest.telemetry.endpoint | string | "" |
Send the report to your own collector instead (TELEMETRY_ENDPOINT). |
| manifest.updateCheck.disabled | bool | false |
Disable the daily call to the GitHub releases API that backs the dashboard's "new version available" badge (MANIFEST_UPDATE_CHECK_DISABLED). Deliberately separate from telemetry: that one stops the report about this install, this one stops the outbound call itself, which is what an air-gapped cluster needs. |
Persistence#
| Key | Type | Default | Description |
|---|---|---|---|
| persistence | object | {"accessModes":["ReadWriteOnce"],"annotations":{},"enabled":false,"existingClaim":"","size":"5Gi","storageClass":""} |
Persistent volume for locally stored request recordings, mounted at manifest.recordings.filesystemPath. Not needed when recordings go to S3, and not needed at all if you do not record message bodies. |
| persistence.accessModes | list | ["ReadWriteOnce"] |
Access modes for the created claim. |
| persistence.annotations | object | {} |
Annotations for the created claim. |
| persistence.enabled | bool | false |
Create and mount a PersistentVolumeClaim. |
| persistence.existingClaim | string | "" |
Use an existing claim instead of creating one. |
| persistence.size | string | "5Gi" |
Size of the created claim. |
| persistence.storageClass | string | "" |
Storage class. Empty uses the cluster default. |
Quick start#
Generate two independent secrets and hand them to the chart out of band:
kubectl create namespace manifest
kubectl --namespace manifest create secret generic manifest-llm-gateway \
--from-literal=BETTER_AUTH_SECRET="$(openssl rand -hex 32)" \
--from-literal=MANIFEST_ENCRYPTION_KEY="$(openssl rand -hex 32)" \
--from-literal=DATABASE_URL='postgresql://manifest:...@postgres.databases.svc:5432/manifest'
helm --namespace manifest install manifest rgielen/manifest-llm-gateway \
--set manifest.existingSecret=manifest-llm-gateway \
--set manifest.publicUrl=https://manifest.example.com \
--set ingress.enabled=true \
--set ingress.hosts[0].host=manifest.example.com \
--set ingress.hosts[0].paths[0].path=/ \
--set ingress.hosts[0].paths[0].pathType=Prefix
On first boot the setup wizard at /setup creates the admin account — there are no
default credentials. Add a provider, copy the generated harness key (it starts with
mnfst_), and point any OpenAI-compatible client at https://manifest.example.com/v1.
Secrets#
Sensitive settings can be given two ways:
manifest.existingSecret— the name of a Secret whose keys are the upstream environment variable names (BETTER_AUTH_SECRET,MANIFEST_ENCRYPTION_KEY,DATABASE_URL,EMAIL_API_KEY,GOOGLE_CLIENT_SECRET, …). It is mounted withenvFromand wins over anything set in values. This is the right choice for GitOps: keep the Secret in sealed-secrets or external-secrets and leave the values empty.- Plain values (
manifest.auth.secret,manifest.database.url, …) — the chart renders them into a Secret of its own. Convenient for a quickhelm install, but they end up in your values file.
Set at least one of them. Without a session secret or a database URL the chart refuses to render, with a message naming both ways out.
The chart never generates a secret for you, and that is not an oversight.
MANIFEST_ENCRYPTION_KEY — or BETTER_AUTH_SECRET, which it falls back to — encrypts
every stored provider API key and OAuth token at rest. ArgoCD renders charts with
helm template and no cluster access, where Helm's lookup returns nothing: a generated
value would come out different on every single sync, and every stored credential would
become undecryptable. A key that changes on its own is still the failure case; a key you
change deliberately is not, and has its own procedure below.
Set manifest.auth.encryptionKey to a second, independent value rather than letting it
fall back. Otherwise one leaked session-signing secret also decrypts every provider
credential you have stored.
Rotating the encryption key#
Changing manifest.auth.encryptionKey on its own leaves every stored credential
undecryptable, and the read paths report that as "provider not connected" rather than as
an error — the damage is silent. Rotate in three steps instead:
- Set
manifest.auth.previousEncryptionKeyto the current key andmanifest.auth.encryptionKeyto the new one, then upgrade. Introducing a dedicated key on an install that had been falling back to the session secret is the same move, withmanifest.auth.secret's value as the previous key. - After boot one pod rewrites every stored provider key, OAuth token, agent key and
e-mail provider key onto the new key, in batches, while the old key keeps working. Wait
for
Nothing left under an older secretin the log. Anything it could not decrypt under either key it leaves untouched and names, rather than destroying it. - Remove
manifest.auth.previousEncryptionKeyand upgrade again. Leaving it set keeps the old key readable — and mounted in the Secret — indefinitely.
Recording bodies are the exception: they are encrypted with the same key but are not rewritten, so a rotation makes existing ones unreadable and retention eventually removes them. Metadata in the database is unaffected.
Reverse proxy and publicUrl#
manifest.publicUrl becomes BETTER_AUTH_URL and must match the URL the browser actually
uses, or logins and OAuth callbacks fail in ways that look like unrelated bugs. When an
Ingress is enabled and publicUrl is empty, the chart derives it from the first Ingress
host, using https if that host appears in ingress.tls.
Prefer https://, but plain http on a LAN or tailnet host is supported again. Between
appVersion 6.24.0 and 6.25.2 it was not: the upstream wired Better Auth's MCP plugin
unconditionally and built the plugin's resource URL out of BETTER_AUTH_URL, the plugin
rejected a non-HTTPS resource URL while the module loaded, and the process exited before it
listened — a pod that never left CrashLoopBackOff (MCP resource URL must use HTTPS).
This chart refused to render that configuration rather than let you find out that way.
Since 6.25.3 the upstream decides whether MCP can run before constructing the plugin, so an HTTP-only install boots and serves the dashboard and the gateway, without the MCP surface, naming the reason in its boot log. The chart's guard is therefore gone. Two consequences remain on plain http, and both are only that:
- No HSTS. The application logs a warning on every boot;
manifest.disableHsts=truesilences it where it is not wanted. - No remote MCP endpoint.
manifest.mcpEnabled=falseacknowledges it and takes the boot line with it. A TLS-terminating proxy in front is what brings MCP back — a self-signed certificate is enough, only the scheme is inspected.
One caveat this removes the guard from: the crash belongs to the image, not to the chart.
Pinning image.tag below the chart's appVersion on a plain-http host brings it back.
Streaming responses need a generous read timeout on the ingress controller. The annotation
differs per controller — for ingress-nginx it is
nginx.ingress.kubernetes.io/proxy-read-timeout, for Traefik it is a ServersTransport.
More than one host#
ingress.hosts takes a list, but publicUrl is one origin, and Better Auth pins both the
OAuth callback and the session cookie to it. A login that starts on the second host is
therefore redirected to a callback on publicUrl and leaves its cookie on an origin the
second host's dashboard never sends back — the login simply does not stick, with nothing
in the logs to say why.
Since appVersion 6.25.2 the upstream resolves the base URL from the request host instead,
restricted to an allow-list, and manifest.authAllowedHosts fills it
(BETTER_AUTH_ALLOWED_HOSTS). Entries are hostnames with an optional port, or full origins
— the scheme is ignored, only the host is kept — and *.example.com wildcards are
accepted. Better Auth derives its trusted origins from the same list, so the CSRF check
follows along and nothing else needs configuring:
ingress:
enabled: true
hosts:
- host: manifest.example.com
paths: [{ path: /, pathType: Prefix }]
- host: gateway.example.com
paths: [{ path: /, pathType: Prefix }]
manifest:
publicUrl: https://manifest.example.com
authAllowedHosts:
- gateway.example.com
Leave it empty for the usual single-host install. With only one host known the upstream
keeps the static origin, which is exactly what it did before 6.25.2 — the chart does not
derive the list from ingress.hosts for you, because trusting a host is a decision, not a
side effect of serving it.
Two things the list does not move. The remote MCP endpoint stays bound to publicUrl: a
self-hosted release advertises one resource URL, and MCP clients keep using that one. And
an entry the upstream cannot parse is dropped without a word — values.schema.json rejects
the obvious typos at install time instead.
Database#
manifest.database.url is a standard PostgreSQL connection string. Percent-encode special
characters in the password (@ → %40, : → %3A, / → %2F). Migrations are applied
by the chart — see Database migrations.
Back up the database, not the cluster: it holds accounts, provider credentials, harness keys and request metadata.
Request recordings#
Message metadata always lives in the database. Recorded message bodies are separate and optional, and go to one of two places:
- A volume, mounted at
manifest.recordings.filesystemPath. Setpersistence.enabledto back it with a PersistentVolumeClaim. Without it the mount is anemptyDirand the bodies are gone on every restart — the mount itself is always present because the root filesystem is read-only. - S3-compatible storage, via
manifest.recordings.s3. A complete S3 configuration takes precedence over the volume whilemanifest.recordings.storageisauto.
Retention defaults to 365 days upstream; override with
manifest.recordings.retentionDays.
Bodies are encrypted at rest with manifest.auth.encryptionKey as of upstream 6.21.0
(recordings written before that stay readable). Unlike stored credentials they are not
rewritten during a key rotation, so see Rotating the encryption key above before
changing it.
Database migrations#
Migrations are applied by a pre-install/pre-upgrade hook Job that runs the upstream's
own migration entry point, not by the application on boot. The Job runs exactly once per
release, so the schema is in place before any pod starts, and a failed migration is a
failed Job with readable logs instead of a pod in CrashLoopBackOff. Its ConfigMap and
Secret are hook-scoped copies — a pre-install hook runs before the release's own
resources exist, and on upgrade the release's copies still hold the previous values.
manifest.runMigrationsOnBoot is off by default because of this. Turning it on alongside
more than one replica is refused outright, and that is not caution:
The boot path applies migrations without the advisory lock the migration entry point takes — no lock at all — so every replica runs the same pending migrations at the same time, and several of them are a
CREATE INDEX CONCURRENTLY, which waits for every other session that can see the table. How badly concurrent index builds go wrong was observed before appVersion 6.26.0, even with the lock: three concurrent runners against an empty database hung indefinitely, the holder's index build waiting on the runners blocked on the lock, a cycle PostgreSQL neither recognises nor breaks. Upstream 6.26.0 fixed that for the migration entry point only — its waiters now poll for the lock instead of blocking — and the boot path still has nothing to wait on.
If manifest.database.url points at a transaction pooler such as PgBouncer, set
manifest.database.migrationUrl to a direct connection. The advisory lock is
session-scoped and transaction pooling does not preserve it.
Those concurrent index builds are not gated by manifest.mode — the dashboard and
retention queries they serve run self-hosted too — so manifest.migrations.job.activeDeadlineSeconds
governs them on every install. The 900 s default is ample for a small database, where they
finish in seconds; on a database of millions of requests, upstream budgets minutes per
index, and the Job's deadline has to cover the whole run.
One migration path the Job cannot cover. Besides the TypeORM migrations, the
application creates Better Auth's own tables on module init, through a separate code path
with no lock and no entry point of its own. On a first install with more than one
replica the pods race on those CREATE TABLEs: one exits with a duplicate-object error
and is restarted, then finds the tables in place and starts normally. The end state is
correct, and rolling upgrades never hit it because only one new pod starts at a time. To
avoid the restart entirely, install with replicaCount: 1 and scale up afterwards.
High availability#
Single-node is not a requirement, and never was — a multi-node cluster with one replica needs no special configuration. More than one replica is a different question.
What the chart handles:
- Rollout strategy. With no ReadWriteOnce volume in play it defaults to a surge-first
RollingUpdate(maxUnavailable: 0), so an upgrade does not interrupt service. Withpersistence.enabledit falls back toRecreate, because a rolling update would wait forever for a claim the old pod still holds. Override withupdateStrategy. - Migrations. See above — the hook Job is what makes multiple replicas safe.
- Recordings. Configure
manifest.recordings.s3; a ReadWriteOnce volume cannot be shared, and the chart refuses that combination rather than leaving podsPending. - Disruption. Set
podDisruptionBudget.enabled: true. It is off by default because for a single replica it only blocks node drains. - Placement. Nothing is spread across nodes unless you say so. This constraint is a no-op on a single-node cluster and spreads on a larger one:
yaml
topologySpreadConstraints:
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app.kubernetes.io/name: manifest-llm-gateway
What the chart cannot fix, because it is upstream behaviour:
Effect with n replicas |
|
|---|---|
| Rate limiting | THROTTLE_LIMIT is enforced in memory, per pod. The effective limit is n × the configured value, and a client can draw the full allowance from each pod. |
| Proxy guardrails | manifest.proxy.concurrencyMax, .rateMaxRequests and .ipRateMaxRequests are per backend process, so the per-tenant and per-IP ceilings multiply the same way. |
| Threshold alerts | The hourly job checks "already sent?" and then sends, with no lock in between. The same alert can go out more than once. |
| Dashboard cache | A bounded in-memory LRU per pod, so two replicas can report different figures until the entries expire. Cosmetic. |
Recording retention cleanup is not on that list: it takes its own advisory lock upstream
and is safe across replicas. Neither is the re-encryption pass that
manifest.auth.previousEncryptionKey triggers — it takes a third lock, and takes it
without waiting, so one pod does the rewrite and the others carry on serving.
If a strict global rate limit matters to you, enforce it at the ingress rather than
relying on THROTTLE_LIMIT.
Upgrading#
Chart version and appVersion move independently, and both appear in every release. New
upstream releases are picked up automatically: a scheduled workflow in this repository
watches the image, bumps appVersion and the chart version together, and opens a pull
request that merges itself once the chart still lints and installs.
Because a rollout restarts the single pod, an upgrade is a short outage on a single-node cluster. Migrations run on boot; take a database backup first for anything that changes the upstream major version.
Use from ArgoCD#
Always pin targetRevision to an explicit chart version:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: manifest
namespace: argocd
spec:
project: infrastructure
source:
repoURL: https://rgielen.github.io/charts
chart: manifest-llm-gateway
targetRevision: 2.11.0
helm:
valuesObject:
manifest:
existingSecret: manifest-llm-gateway
publicUrl: https://manifest.example.com
ingress:
enabled: true
hosts:
- host: manifest.example.com
paths:
- path: /
pathType: Prefix
destination:
server: https://kubernetes.default.svc
namespace: manifest
Environment variable mapping#
Every setting from the upstream .env.example, and where it lives here. Values marked
secret belong in manifest.existingSecret under exactly the environment variable name
in the left column.
| Environment variable | Value |
|---|---|
BETTER_AUTH_SECRET |
manifest.auth.secret (secret) |
MANIFEST_ENCRYPTION_KEY |
manifest.auth.encryptionKey (secret) |
MANIFEST_ENCRYPTION_KEY_PREVIOUS |
manifest.auth.previousEncryptionKey (secret) |
PORT |
manifest.port |
HOST_BIND_ADDRESS, HOST_PORT |
not applicable — use service and ingress |
BETTER_AUTH_URL |
manifest.publicUrl |
BETTER_AUTH_ALLOWED_HOSTS |
manifest.authAllowedHosts (a list; joined with commas) |
MANIFEST_VERSION |
image.tag, defaulting to the chart's appVersion |
MANIFEST_MODE |
manifest.mode |
DATABASE_URL |
manifest.database.url (secret) |
POSTGRES_PASSWORD |
not applicable — the database is external |
DB_POOL_MAX, AUTH_DB_POOL_MAX, DB_TUNE_SESSION |
manifest.database.poolMax, .authPoolMax, .tuneSession |
RUN_MIGRATIONS_ON_BOOT |
manifest.runMigrationsOnBoot, off by default — see Database migrations |
MIGRATION_DATABASE_URL |
manifest.database.migrationUrl (secret) |
REQUEST_RECORDING_STORAGE |
manifest.recordings.storage |
REQUEST_RECORDING_FILESYSTEM_PATH |
manifest.recordings.filesystemPath |
REQUEST_RECORDING_RETENTION_DAYS |
manifest.recordings.retentionDays |
REQUEST_RECORDING_S3_BUCKET |
manifest.recordings.s3.bucket |
REQUEST_RECORDING_S3_ENDPOINT |
manifest.recordings.s3.endpoint |
REQUEST_RECORDING_S3_REGION |
manifest.recordings.s3.region |
REQUEST_RECORDING_S3_FORCE_PATH_STYLE |
manifest.recordings.s3.forcePathStyle |
REQUEST_RECORDING_S3_ACCESS_KEY_ID |
manifest.recordings.s3.accessKeyId (secret) |
REQUEST_RECORDING_S3_SECRET_ACCESS_KEY |
manifest.recordings.s3.secretAccessKey (secret) |
PROVIDER_TIMEOUT_MS |
manifest.proxy.providerTimeoutMs |
STREAM_WARMUP_MS |
manifest.proxy.streamWarmupMs |
STREAM_IDLE_TIMEOUT_MS |
manifest.proxy.streamIdleTimeoutMs |
CODEX_SEMANTIC_OUTPUT_TIMEOUT_MS |
manifest.proxy.codexSemanticOutputTimeoutMs |
MANIFEST_CONCURRENCY_MAX |
manifest.proxy.concurrencyMax |
MANIFEST_RATE_MAX_REQUESTS |
manifest.proxy.rateMaxRequests |
MANIFEST_IP_RATE_MAX_REQUESTS |
manifest.proxy.ipRateMaxRequests |
OLLAMA_HOST |
manifest.ollamaHost |
CREDITS_BASE_URL, CREDITS_AUTO_PROVISION_ALLOWLIST, CREDITS_GEMINI_FREE_MAX_BUDGET |
manifest.credits.baseUrl, .autoProvisionAllowlist, .geminiFreeMaxBudget |
CREDITS_MASTER_KEY |
manifest.credits.masterKey (secret) |
EMAIL_PROVIDER, EMAIL_DOMAIN, EMAIL_FROM |
manifest.email.provider, .domain, .from |
EMAIL_API_KEY |
manifest.email.apiKey (secret) |
GOOGLE_CLIENT_ID, GITHUB_CLIENT_ID, DISCORD_CLIENT_ID |
manifest.oauth.<provider>.clientId |
GOOGLE_CLIENT_SECRET, GITHUB_CLIENT_SECRET, DISCORD_CLIENT_SECRET |
manifest.oauth.<provider>.clientSecret (secret) |
OPENAI_OAUTH_CLIENT_ID, MINIMAX_OAUTH_CLIENT_ID |
manifest.providerOauth.openaiClientId, .minimaxClientId |
API_KEY |
manifest.apiKey (secret) |
CLI_TOKEN_TTL_DAYS, CLI_TOKEN_ABSOLUTE_TTL_DAYS |
manifest.cliToken.ttlDays, .absoluteTtlDays |
MANIFEST_DISABLE_HSTS |
manifest.disableHsts |
MCP_ENABLED |
manifest.mcpEnabled — see Reverse proxy and publicUrl |
WINGMAN_CORS_ORIGINS |
manifest.corsOrigins (a list; joined with commas) |
THROTTLE_TTL, THROTTLE_LIMIT |
manifest.throttle.ttl, .limit |
AGENT_USAGE_DAILY_WORKER |
manifest.agentUsage.dailyWorker |
AGENT_USAGE_DAILY_BATCH_SIZE |
manifest.agentUsage.batchSize |
AGENT_USAGE_DAILY_RUN_BUDGET_MS |
manifest.agentUsage.runBudgetMs |
SHUTDOWN_DRAIN_MS |
manifest.shutdownDrainMs |
SENTRY_DSN |
manifest.sentry.dsn (secret) |
SENTRY_ENVIRONMENT, SENTRY_RELEASE |
manifest.sentry.environment, .release |
MANIFEST_TELEMETRY_DISABLED, TELEMETRY_ENDPOINT |
manifest.telemetry.disabled, .endpoint |
MANIFEST_UPDATE_CHECK_DISABLED |
manifest.updateCheck.disabled |
SEED_DATA, NODE_ENV |
fixed, as in the upstream compose file |
| anything else | extraEnv |
Every variable is spelled out in full rather than abbreviated with a shared prefix, so
.github/scripts/chart_audit.py can check this table for completeness instead of guessing.
Deliberately not modelled#
The upstream reads these and this chart does not expose them. They are decisions, not gaps
— extraEnv reaches any of them if you disagree.
| Setting | Why not |
|---|---|
BACKFILL_DATABASE_URL |
The third fallback after MIGRATION_DATABASE_URL, which manifest.database.migrationUrl already sets, so the backfill resolver is already satisfied. Cloud-only besides. |
AGENT_USAGE_DAILY_READS, AGENT_USAGE_DAILY_READ_TENANTS |
Both override where the dashboard reads agent usage from during the staged cutover to the daily rollups — the first forces rollups or raw queries globally, the second names the tenant IDs allowed to read rollups first. The automatic default already waits for the backfill to reach parity before switching, and a self-hosted install has one tenant and no rollout to stage. manifest.agentUsage.dailyWorker covers the part that is operational here: whether the rollup is written at all. |
PLUGIN_OTLP_ENDPOINT |
Documented in the upstream's .env.example but read nowhere in the server. |
ERROR_PAGE_PUSH_SECRET |
Gates an internal endpoint for publishing curated error pages. Empty rejects every write, which is the right state for a self-hosted install. |
CRM_METRICS_SECRET |
Guards /api/v1/internal/crm-metrics, the feed the hosted service's outreach CRM polls. Cloud-only, and the upstream counts anything shorter than 32 characters as unset, so leaving it empty keeps the route shut — which is what you want from an endpoint that exports user email addresses across tenants. |
MANIFEST_PUBLIC_STATS |
Serves the published error pages at /api/v1/public/error-pages without authentication, for the upstream's own marketing site. The name is historical: since appVersion 6.26.0 the aggregate usage stats it once exposed are gone, and the upstream kept the variable so its existing deployments stay on. |
MAILGUN_API_KEY, MAILGUN_DOMAIN, NOTIFICATION_FROM_EMAIL |
Legacy fallbacks superseded by the EMAIL_* settings above. Modelling both invites a configuration that contradicts itself. |
BIND_ADDRESS, NODE_ENV |
Already set inside the image. Overriding them only adds a way to break the deployment. |
CORS_ORIGIN, FRONTEND_PORT, MANIFEST_FRONTEND_DIR, MANIFEST_EMBEDDED |
Development-only; inert in a production image. |
STRIPE_SECRET_KEY, STRIPE_PRO_PRICE_ID, STRIPE_WEBHOOK_SECRET, PLAN_LIMIT_FREE_REQUESTS, PLAN_LIMIT_PRO_REQUESTS, PLAN_REQUEST_QUOTA_RESET_AT, ANNOUNCE_APP_URL, DOCTOR_TUTORIAL_URL |
Billing and marketing for the hosted service. |
HOST_BIND_ADDRESS, HOST_PORT, MANIFEST_VERSION, POSTGRES_PASSWORD |
Compose-specific. Their Kubernetes equivalents are service, ingress, image.tag and your database's own configuration. |
A value left empty is not passed to the container at all. That matters: the application
reads several settings as Number(env ?? default), where an empty string becomes 0
rather than the documented default.
Maintainers#
| Name | Url | |
|---|---|---|
| rgielen | https://github.com/rgielen |
Published versions#
Every version below is installable and immutable — a published version is never re-released with different content.
| Version | App version | Released | Package | Digest |
|---|---|---|---|---|
2.11.0 |
6.28.1 |
2026-10-01 | .tgz |
500227b0054aa4e789a… |
2.10.0 |
6.26.1 |
2026-10-01 | .tgz |
190a0b2bac58f22e967… |
2.9.0 |
6.26.0 |
2026-10-01 | .tgz |
98b18be3df667f745ab… |
2.8.2 |
6.25.5 |
2026-09-23 | .tgz |
e9fdde77d534ed6d405… |
2.8.1 |
6.25.2 |
2026-09-17 | .tgz |
d6f31cabf4cbd561ce7… |
2.8.0 |
6.25.2 |
2026-09-17 | .tgz |
435d5a4c5a253079af4… |
2.7.1 |
6.25.2 |
2026-09-17 | .tgz |
d240630e4ee8f25af4f… |
2.7.0 |
6.25.1 |
2026-09-16 | .tgz |
6f29e168241244941fe… |
2.6.0 |
6.24.0 |
2026-09-14 | .tgz |
631fad676b523335699… |
2.5.1 |
6.23.4 |
2026-09-12 | .tgz |
1b76d0ecffa3926c34e… |
2.5.0 |
6.23.1 |
2026-09-11 | .tgz |
f03b2469467cd659c51… |
2.4.0 |
6.22.0 |
2026-09-06 | .tgz |
3f5c17c8311b0ae27c9… |
2.3.0 |
6.21.0 |
2026-09-03 | .tgz |
c32f98971cbf38ddd39… |
2.2.2 |
6.20.0 |
2026-09-02 | .tgz |
04efd39d1d817e89511… |
2.2.1 |
6.20.0 |
2026-09-02 | .tgz |
4581c5cf1bed7ac2c4a… |
2.2.0 |
6.20.0 |
2026-09-02 | .tgz |
8729b793e4ffad64097… |
2.1.0 |
6.20.0 |
2026-09-02 | .tgz |
93e321cee7dc48a41f2… |
2.0.3 |
6.19.1 |
2026-09-01 | .tgz |
291cf85d9f8a843e79a… |
2.0.2 |
6.19.1 |
2026-09-01 | .tgz |
6fce782b9a283c525c9… |
2.0.1 |
6.19.1 |
2026-09-01 | .tgz |
950643e59f0325509c4… |
2.0.0 |
6.19.1 |
2026-09-01 | .tgz |
84b4d7df435ffae9068… |
1.0.2 |
6.19.1 |
2026-09-01 | .tgz |
0ec5154386cb93a0f64… |
1.0.1 |
6.19.1 |
2026-09-01 | .tgz |
5c0826a1f659e32657a… |
1.0.0 |
6.19.1 |
2026-09-01 | .tgz |
1416c0fcad0585c3c06… |
0.1.0 |
6.19.1 |
2026-09-01 | .tgz |
4323d42c79e557fa3a4… |
Upstream tracking#
This chart's appVersion follows a container image. A scheduled workflow watches
that image, diffs the watched files between the two image commits, and opens a
pull request that bumps appVersion and the chart version together. It merges
unattended only when the bump is not a major one and the diff is clean.
| Setting | Value |
|---|---|
| Tracked image | docker.io/manifestdotbuild/manifest |
| Tag pattern | ^[0-9]+\.[0-9]+\.[0-9]+$ |
| Release notes | manifest@6.28.1 — not every image tag has one |
| Watched files | docker/.env.example, docker/docker-compose.yml, packages/backend/.env.example, packages/backend/src/config/app.config.ts |
| Config sources | packages/backend/src/config/app.config.ts, packages/backend/.env.example, docker/.env.example |
| Upstream source roots | packages/backend/src |