Docs › Self-Hosted Server

Enterprise

Self-hosted server

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.

How it works — and why it's safe

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.

Prerequisites

Configuration

The server is configured entirely with environment variables:

VariableRequiredDefaultPurpose
PULLGUARD_LICENSE_KEYyesYour Enterprise licence (offline key; validates locally with no network). The server refuses to start below Enterprise.
PULLGUARD_SERVER_INGEST_TOKENSyesComma-separated bearer tokens your CI presents on POST /api/scans. Rotate by adding a new one, cutting CI over, then removing the old.
PORTno8080Listen port.
PULLGUARD_SERVER_HOSTno0.0.0.0Bind address.
PULLGUARD_SERVER_DATA_DIRno./dataDirectory for the database. Mount a volume here to persist history.
PULLGUARD_SERVER_MAX_BODY_BYTESno5242880Hard cap on request body (oversize → HTTP 413).
PULLGUARD_SERVER_RETAIN_SCANSno200Scans 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.

Getting the image

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 pulldocker 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.

Quick start (Docker Compose)

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.

Wire your CI to upload results

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.

The dashboards

These reuse the same renderers as the air-gapped HTML report — see Reports & Dashboard.

Triage & governance — with an immutable audit trail

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.

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.

Query API — embed PullGuard in your own UI

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.

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.

Human login — SSO & RBAC

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:

Sessions are server-side with idle and absolute expiry and CSRF protection. Full SSO setup ships as docs/sso.md in the server package.

Security posture

Repository-band licensing

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.

Prefer not to host anything yet?

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.