> ## Documentation Index
> Fetch the complete documentation index at: https://docs.promptguard.co/llms.txt
> Use this file to discover all available pages before exploring further.

# Self-host configuration reference

> Environment variables, Helm chart settings, licensing and monitoring for a self-hosted PromptGuard engine.

The settings to look up when you run the engine yourself. The deployment steps
are in [Enterprise setup](https://docs.promptguard.co/production/enterprise-setup.md#self-hosted-deployment),
and what differs from PromptGuard Cloud is in
[Self-host vs Cloud](https://docs.promptguard.co/production/self-host-vs-cloud.md).

## Environment variables

The settings that matter most, all documented inline in `deploy/docker-compose.yml`:

| Variable | What it controls |
| - | - |
| `PROMPTGUARD_VERSION` | Required. The release tag of the signed images to run; there is no floating default. |
| `DASHBOARD_API_URL` | The API origin browsers use, read by the dashboard at start (default `http://localhost:8080`). |
| `TRUSTED_PROXY_HOPS` | Default `1`, for the bundled nginx, which publishes the API on `:8080` and at `<host>/api/v1`. The API reads the caller's IP from the last `X-Forwarded-For` entry this many proxies appended, so keep the API reachable only through them. Serving the API without nginx? Publish its port and set `0`. |
| `DEPLOYMENT_MODE=airgap` | Default. Event forwarding and the license heartbeat are off. GeoIP and the ML classifiers run in-process and release lookups are off. Pair it with a default-deny egress firewall all the same: the engine can still attempt a few outbound calls when configured for them. |
| `ML_INFERENCE_MODE=local` | Default. The prompt-injection and toxicity classifiers run inside the API container from model weights you fetch yourself with your own Hugging Face token and mount at `LOCAL_INFERENCE_DIR` (PromptGuard does not ship them; see [Third-Party Models](https://docs.promptguard.co/security/third-party-models.md)), and nothing calls hosted inference. Without the weights they are listed as unavailable. The LLM judges need a judge server (Docker Compose ships none; the Helm chart's optional judge service, a bring your own GPU model server not validated by PromptGuard, or your own at `LLM_GUARD_BASE_URL`) and without one are listed on every scan as unavailable. `off` runs the deterministic detectors only. |
| `SELF_HOST_LICENSE` / `LICENSE_FILE` | Your signed license — inline base64, or a mounted file. |
| `NODE_ENV=production` | Set by the Compose file and the Helm chart; leave it. Interactive API docs are off (`ENABLE_API_DOCS=true` turns them on), sign-in cookies are `Secure` (reach the dashboard and API over HTTPS, or on `localhost`), and error responses carry no internal detail. |
| `PROMPTGUARD_LICENSE_GRACE_DAYS` | Renewal buffer after license expiry. |

## Helm chart settings

The Helm chart ships the controls you would otherwise write by hand. Set them in
your values file rather than adding your own manifests:

| Values key | What it does | Default |
| - | - | - |
| `api.autoscaling.*` | HorizontalPodAutoscaler on CPU (`minReplicas`, `maxReplicas`, `targetCPUUtilizationPercentage`) | on: 2 to 10 replicas at 70% CPU |
| `api.pdb.minAvailable` | PodDisruptionBudget | 1 |
| `networkPolicy.enabled` | Default-deny egress NetworkPolicy, the control behind Air-gapped mode; open only what the API must reach with `ingressNamespaces`, `egressNamespaces` and `egressCIDRs` (internal ranges only) | off |
| `monitoring.prometheusRule.enabled` | Installs the alert rules under [Monitoring](#monitoring) | off |

Pods run as non-root with a read-only root filesystem and every capability
dropped (`podSecurityContext`, `securityContext`).

## Licensing

Your license is an **Ed25519-signed file** verified **locally** against
PromptGuard's public verification key (embedded in the deployment package).
There is no license server in the request path, and licensing sends no content:

* **Signature verification is local and offline** — the engine checks the
  signature and expiry itself, on startup.
* **Air-gapped mode makes no licence calls.** With `DEPLOYMENT_MODE=airgap`
  (the deployment package default) the license heartbeat is disabled entirely —
  the license is enforced by signature and expiry alone.
* **Connected mode's only licence call is a daily, metadata-only heartbeat**
  (license id, node fingerprint, customer name — never prompts, verdicts, or
  any request content). If the heartbeat can't get out, the engine keeps
  serving through a configurable grace window
  (`PROMPTGUARD_LICENSE_GRACE_DAYS`). If the window runs out, billable
  requests get a 503 until the next heartbeat that reaches us — the engine
  keeps trying hourly, and recovery needs no restart.
* **What you can verify yourself:** the license and heartbeat code paths are in
  the source you receive (`shared/licensing/`, `shared/billing/license.py`) —
  auditable end-to-end, including exactly what the heartbeat payload contains.

## Monitoring

### Scraping `/metrics`

A self-hosted API serves Prometheus metrics at `/metrics` to a scraper that presents its `METRICS_TOKEN`. [Monitoring a self-hosted instance](https://docs.promptguard.co/production/self-host-vs-cloud.md#monitoring-a-self-hosted-instance) says how to set the token and what the endpoint carries; on Cloud it is not open to customers. A Prometheus outside Kubernetes scrapes it like this:

```yaml theme={"system"}
# prometheus-config.yml
global:
  scrape_interval: 15s
  evaluation_interval: 15s

rule_files:
  - "promptguard-alerts.yml"

scrape_configs:
  - job_name: 'promptguard-api'
    static_configs:
      - targets: ['promptguard-api-1:8080', 'promptguard-api-2:8080', 'promptguard-api-3:8080']
    metrics_path: /metrics
    scrape_interval: 30s
    authorization:
      type: Bearer
      credentials_file: /etc/prometheus/promptguard-metrics-token

  - job_name: 'postgres'
    static_configs:
      - targets: ['postgres-primary:9187']

  - job_name: 'redis'
    static_configs:
      - targets: ['redis-1:9121', 'redis-2:9121', 'redis-3:9121']

alerting:
  alertmanagers:
    - static_configs:
        - targets:
          - alertmanager:9093
```

### Alerting Rules

The Helm chart installs the alert rules we run on Cloud, written against these series: set `monitoring.prometheusRule.enabled=true`. For a Prometheus outside Kubernetes, copy the rules from `deploy/helm/promptguard/templates/prometheusrule.yaml` in the release you run. They are the maintained set. The examples below show the series they are built on:

```yaml theme={"system"}
# promptguard-alerts.yml
groups:
- name: promptguard-examples
  rules:
  # Requests served WITHOUT a detection stage running. A fail-open request is
  # logged as an ordinary allow, so nothing else shows this.
  - alert: DetectionFailingOpen
    expr: sum by (stage) (increase(promptguard_detection_engine_failures_total{outcome="failed_open"}[5m])) > 0
    for: 0m
    labels:
      severity: critical

  - alert: HighErrorRate
    expr: |
      sum(rate(promptguard_http_requests_total{status=~"5.."}[5m]))
        / clamp_min(sum(rate(promptguard_http_requests_total[5m])), 0.001) > 0.02
    for: 30m
    labels:
      severity: warning

  # One detector running past its budget (see Latency Budgets).
  - alert: SlowDetector
    expr: histogram_quantile(0.95, sum by (le, detector) (rate(promptguard_detector_latency_seconds_bucket[10m]))) > 1
    for: 15m
    labels:
      severity: warning
```

### Grafana Dashboards

```json theme={"system"}
{
  "dashboard": {
    "title": "PromptGuard",
    "panels": [
      {
        "title": "Request Rate",
        "type": "timeseries",
        "targets": [{ "expr": "sum by (route) (rate(promptguard_http_requests_total[5m]))", "legendFormat": "{{route}}" }]
      },
      {
        "title": "Events by Decision",
        "type": "stat",
        "targets": [{ "expr": "max by (decision) (promptguard_security_events_by_decision)", "legendFormat": "{{decision}}" }]
      },
      {
        "title": "Detector p95",
        "type": "timeseries",
        "targets": [{ "expr": "histogram_quantile(0.95, sum by (le, detector) (rate(promptguard_detector_latency_seconds_bucket[5m])))", "legendFormat": "{{detector}}" }]
      },
      {
        "title": "5xx by Route",
        "type": "timeseries",
        "targets": [{ "expr": "sum by (route) (rate(promptguard_http_requests_total{status=~\"5..\"}[5m]))", "legendFormat": "{{route}}" }]
      }
    ]
  }
}
```

`promptguard_security_events_by_decision` is read from the database, so every pod reports the same total: take `max`, not `sum`, across pods.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.