Docs › Self-Hosted Server
Enterprise
Run a live, access-controlled multi-repo PullGuard dashboard
inside your own boundary, on your own domain (e.g.
https://pullguard.yourcompany.com) — with history, SSO and RBAC.
PullGuard's core promise is that your code never leaves your runners. A vendor-hosted dashboard would break that, so instead you run the server:
This is separate from the PullGuard licensing service — that only validates subscriptions and never receives scan data.
The server is configured entirely with environment variables:
| Variable | Required | Default | Purpose |
|---|---|---|---|
PULLGUARD_LICENSE_KEY | yes | — | Your Enterprise licence (offline key; validates locally with no network). The server refuses to start below Enterprise. |
PULLGUARD_SERVER_INGEST_TOKENS | yes | — | Comma-separated bearer tokens your CI presents on POST /api/scans. Rotate by adding a new one, cutting CI over, then removing the old. |
PORT | no | 8080 | Listen port. |
PULLGUARD_SERVER_HOST | no | 0.0.0.0 | Bind address. |
PULLGUARD_SERVER_DATA_DIR | no | ./data | Directory for the database. Mount a volume here to persist history. |
PULLGUARD_SERVER_MAX_BODY_BYTES | no | 5242880 | Hard cap on request body (oversize → HTTP 413). |
PULLGUARD_SERVER_RETAIN_SCANS | no | 200 | Scans retained per repo (bounds growth + history depth). |
The server fails closed: it won't start without a valid Enterprise licence and at least one ingest token — and, if SSO is enabled, not without a valid SSO configuration.
The server image is a private, Enterprise-only artifact, available two ways — use whichever fits your network policy.
Option A — private registry pull (ghcr.io/pullguard-dev/pullguard-server). With your Enterprise licence you receive a registry read token — authenticate once, then pull normally:
echo "$PULLGUARD_REGISTRY_TOKEN" | docker login ghcr.io -u pullguard-customer --password-stdin
docker pull ghcr.io/pullguard-dev/pullguard-server:latest
Option B — licence-gated HTTPS download. No GitHub identity or registry egress required — the same licence key you already hold (online pg_live_* token or offline pullguard_enterprise_* key) authenticates a direct tarball download:
curl -fL -H "Authorization: Bearer $PULLGUARD_LICENSE_KEY" \
-o pullguard-server.tar \
"https://pullguard.dev/api/server-image?version=latest"
# verify against the sha256 returned in the x-pullguard-sha256 response header
sha256sum pullguard-server.tar
docker load -i pullguard-server.tar
version accepts latest or an exact semver (e.g. 1.4.1). Revocation is the licence itself — expire or revoke the key and the download stops; the endpoint never receives or stores scan data. Rate-limited per source IP.
Pin a version tag for change-controlled environments. Every published server image carries SLSA build provenance and an SBOM — verify with docker buildx imagetools inspect (Option A) or the sha256sum check above (Option B) before first run, exactly as with the scanner image.
Air-gapped? Either option produces a local image: mirror Option A on a connected machine (docker pull → docker save → transfer → docker load), or use Option B directly — the offline licence key validates locally, so the running server never contacts a registry or PullGuard. Don't have your registry token yet? Contact us with your licence org.
export PULLGUARD_LICENSE_KEY=pullguard_enterprise_yourorg_...
export PULLGUARD_SERVER_INGEST_TOKENS=$(openssl rand -hex 24)
docker compose up -d
# dashboard on http://localhost:8080 (put your TLS-terminating proxy in front)
A Dockerfile, docker-compose.yml, and a Helm chart ship with the server package. For Kubernetes, create a docker-registry pull secret from your registry token (imagePullSecrets in the chart values), store the licence and ingest tokens as a secret, and helm install the chart.
Add two inputs to your PullGuard GitHub Action — after each scan it uploads the results to your server (best-effort; a server hiccup never fails your build):
- uses: pullguard-dev/pullguard-action@v1
with:
server-url: https://pullguard.yourcompany.com
server-token: ${{ secrets.PULLGUARD_SERVER_TOKEN }}
Set PULLGUARD_SERVER_TOKEN to one of your ingest tokens. The Action uploads only the finding report — never source.
/) — every repo at a glance: project count, failing repos, open-findings and critical/major totals, plus a sortable table. Click a row to drill in./repos/<org>/<name>) — grade, score, findings over time, with drill-down to file:line.These reuse the same renderers as the air-gapped HTML report — see Reports & Dashboard.
Disposition findings per finding, per line from the dashboard: mark a finding Acknowledged, False positive, or Won’t fix, with a comment. The triage view is a full review surface — a “% triaged” KPI, per-finding effort and estimated cost, code owner (from git-blame), and faceted filters by severity, category and status.
maintainer role (or above); viewers see dispositions but can’t change them. Deployments without an identity provider can opt in to triage with PULLGUARD_SERVER_OPEN_TRIAGE=true (v1.5.1) — decisions stay audit-logged (recorded as unauthenticated), and the server refuses to start if the flag is combined with SSO.
This is distinct from scanner-side .pullguardignore (which suppresses a finding on
future scans, and never for security-category findings). Triage is the
review/governance layer on the results you already have.
A read-only JSON API lets you surface repo grades, security posture, compliance status, finding trends and triage state inside your own control-plane / ops UI — without leaving the tools your team already uses.
GET /api/v1/portfolio — all repos + roll-up.GET /api/v1/repos/<id>/scans — per-repo scan history.GET /api/v1/repos/<id>/scans/<scanId>/findings — findings, each with its triageStatus.GET /api/v1/health — reachability.
Authenticated with a read-only token (PULLGUARD_SERVER_READ_TOKENS, GET-scoped — it cannot mutate data) and an explicit CORS allowlist (PULLGUARD_SERVER_CORS_ORIGINS; no wildcard). Results only — never source.
By default (PULLGUARD_SSO_MODE=none) the dashboard is protected by your network
boundary / reverse proxy, and machine ingestion is always token-authenticated. To require
human login and role-gating, enable OIDC or
SAML 2.0:
PULLGUARD_SSO_MODE=oidc plus your issuer, client ID and secret, and the server's base URL (Authorization-Code + PKCE).PULLGUARD_SSO_MODE=saml plus your IdP metadata / certificate and entity IDs.viewer < maintainer < admin) via PULLGUARD_SSO_ROLE_MAP. Access is deny-by-default: an authenticated user matching no role gets none.Sessions are server-side with idle and absolute expiry and CSRF protection. Full SSO setup ships as docs/sso.md in the server package.
Since v1.4.0, Enterprise license keys can carry a signed repository-count band, and the server meters distinct-repo ingestion against it:
The band is part of the signed key, so it validates locally like the rest of the licence — still no call home, still fully air-gapped.
The scanner already emits pullguard.sarif and a self-contained HTML report on
every run — pipe those into your existing Grafana / SIEM / DefectDojo, or just browse
the offline report. See Reports & Dashboard. The
self-hosted server is the upgrade when you want a live, access-controlled, multi-repo view.
Questions or an Enterprise trial? hello@pullguard.dev.