Deploy the GraphWarden proxy with Docker

Run the GraphWarden proxy as a Docker container on any Linux or Windows Docker host. The image is built from the published .NET binaries; configuration is environment-driven (see the environment variables reference). This page covers a Dockerfile pattern, docker-compose orchestration, health checks, and Key Vault access from a container.

Docker Engine 20.10+ on Linux or Windows

Prerequisites

- Docker Engine 20.10+ OR Docker Desktop - Access to the published GraphWarden proxy binaries (or a built image from your build pipeline) - Azure Key Vault with a managed identity or service principal that the container can authenticate as - Outbound firewall rules per [networking and firewall](./networking-firewall) - An [Azure Container Apps](./azure-container-apps) environment or any container host of your choice for runtime

Build a GraphWarden Docker image with an AI assistant

Help me build a GraphWarden proxy Docker image from published binaries.

I have a directory of published .NET 8 binaries including GraphProxy.Proxy.dll (Linux) or GraphProxy.Proxy.exe (Windows). I want:
1. A minimal Dockerfile using mcr.microsoft.com/dotnet/aspnet:8.0 as the runtime base.
2. A docker-compose.yml for local development that injects env vars from a .env file.
3. A HEALTHCHECK that curls the proxy's /health endpoint on the Kestrel port.
4. Guidance on mounting a rules.yaml file and reading Key Vault credentials via managed identity or a service principal.

Reference documentation: https://graphwarden.com/llms.txt

Ask me whether my deployment target is Linux or Windows, and whether I'll use managed identity or a service principal for Key Vault access.

Reference: llms.txt

Image source

The GraphWarden proxy ships as a set of published .NET binaries. The container image is built from those binaries; the project does not currently publish an official image. Your build pipeline copies the publish output into a base image, sets an entrypoint, and pushes the resulting image to your registry (Azure Container Registry, Docker Hub, or a private registry).

An official published Docker image is planned. For now, build locally from the published .NET binaries. The Dockerfile pattern below is stable and tracks the proxy's binary layout.

The proxy is an ASP.NET Core self-hosted Kestrel service (not IIS). On Linux the entrypoint is dotnet GraphProxy.Proxy.dll. On Windows containers the entrypoint is GraphProxy.Proxy.exe. The binding port defaults to 5000 unless overridden by Kestrel configuration.

Dockerfile pattern

The Dockerfile below uses the ASP.NET Core 8.0 runtime as its base, copies the publish output into /app, exposes the Kestrel port, and sets an entrypoint. Adapt the COPY source path to your build pipeline's publish directory.

# Linux variant — use for Azure Container Apps, Kubernetes, and most container hosts.
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
WORKDIR /app

# Copy the published .NET 8 output produced by `dotnet publish -c Release`.
COPY ./publish/ .

# Kestrel binds to 5000 by default; override with ASPNETCORE_URLS if your host
# expects a different port.
ENV ASPNETCORE_URLS=http://+:5000
EXPOSE 5000

# Health check — the proxy exposes /health on the same Kestrel listener.
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
  CMD curl -fsS http://localhost:5000/health || exit 1

ENTRYPOINT ["dotnet", "GraphProxy.Proxy.dll"]

For Windows container hosts, swap the base image and entrypoint:

# Windows variant — use only when your container host runs Windows.
FROM mcr.microsoft.com/dotnet/aspnet:8.0-windowsservercore-ltsc2022 AS runtime
WORKDIR C:\\app

COPY ./publish/ .

ENV ASPNETCORE_URLS=http://+:5000
EXPOSE 5000

ENTRYPOINT ["GraphProxy.Proxy.exe"]

Both variants read the same environment variables. The Windows image does not define a HEALTHCHECK with curl because curl.exe is not present in the base image by default — your orchestrator should probe /health out-of-band (Kubernetes livenessProbe, Azure Container Apps probe, or a custom PowerShell HEALTHCHECK script).

Environment configuration

Every proxy setting is overridable by environment variable with double-underscore (__) nesting. For example, GraphClient:TenantId maps to GraphClient__TenantId. The environment variables reference lists every supported key and its default.

A minimal .env file for container orchestration looks like:

# Identity
GraphClient__TenantId=00000000-0000-0000-0000-000000000000
GraphClient__ClientId=11111111-1111-1111-1111-111111111111

# Key Vault — production path
KeyVault__VaultUri=https://myvault.vault.azure.net/
KeyVault__UseManagedIdentity=true

# JWT signing (32+ chars)
ProxyJwt__SigningKey=replace-with-a-32-plus-character-secret
ProxyJwt__Issuer=graphproxy
ProxyJwt__Audience=graphproxy

# Audit
Audit__Enabled=true
Audit__IngestionUrl=https://app.graphwarden.example.com/api/v1/agent/audit
Audit__DeploymentType=hosted

Do not commit this file. Use your platform's secret store (Azure Container Apps secrets, Kubernetes secrets, Docker Swarm secrets) to inject values at runtime.

docker-compose example

The compose file below wires the proxy to a local .env file, exposes port 5000, and declares a health check that matches the Dockerfile's expectation.

services:
  proxy:
    image: myregistry.azurecr.io/graphwarden-proxy:0.1.0
    container_name: graphwarden-proxy
    restart: unless-stopped
    ports:
      - "5000:5000"
    env_file:
      - ./.env
    volumes:
      - ./rules.yaml:/config/rules.yaml:ro
    healthcheck:
      test: ["CMD", "curl", "-fsS", "http://localhost:5000/health"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 10s

The ./rules.yaml volume mount gives the proxy a read-only ruleset file. The proxy reads this file at startup and on RuleSync refresh (when RuleSync__Enabled=true). If your ruleset is managed entirely from the GraphWarden control plane, omit the volume and leave Rules__FilePath at its default.

Health check

The proxy exposes GET /health on the same Kestrel listener as Graph traffic. A healthy response is 200 OK with a JSON status object. The HEALTHCHECK directive above runs every 30 seconds; adjust the interval if your orchestrator polls liveness independently.

For Kubernetes, use a livenessProbe and readinessProbe pointing at /health on the same port. For Azure Container Apps, the platform probes the configured ingress port at a configurable path (see Azure Container Apps for the ACA-specific snippet).

Key Vault access from a container

The proxy reads Graph credentials, the JWT signing key, and the RuleSync HMAC secret from Azure Key Vault at startup. Three container-deployment patterns are supported.

Workload identity (Azure Container Apps, AKS with workload identity). Set AZURE_CLIENT_ID to the user-assigned managed identity's client id. The proxy's Azure.Identity client acquires a token from the platform's workload-identity endpoint. KeyVault__UseManagedIdentity=true must be set. No secrets live in the container.

System-assigned managed identity (Azure VMs, Azure Container Instances). Leave AZURE_CLIENT_ID unset; Azure.Identity uses the VM metadata service automatically. KeyVault__UseManagedIdentity=true.

Service principal (non-Azure hosts, on-premise Kubernetes). Set AZURE_TENANT_ID, AZURE_CLIENT_ID, and AZURE_CLIENT_SECRET on the container. Azure.Identity uses client-credential authentication against Azure AD, then accesses Key Vault. The service principal needs Secret — Get on the vault. This is the only pattern that places a secret in the container environment; store AZURE_CLIENT_SECRET in your platform's secret store, not a committed .env file.

For air-gapped deployments where Azure Key Vault is not reachable, the proxy supports an in-memory secret store for development. Set KeyVault__UseManagedIdentity=false and provide secrets through environment variables directly (for example GraphClient__ClientSecret=...). This path is not recommended for production.

Running on Linux vs Windows

The Linux variant (dotnet/aspnet:8.0) is the recommended default. It is smaller, starts faster, and has a wider ecosystem of orchestrators. Azure Container Apps, AKS Linux node pools, and most Kubernetes distributions run Linux containers natively.

The Windows variant is useful when the proxy must run alongside other Windows containers that require Windows-specific APIs. It uses the larger Windows Server Core base image and takes longer to start. Path separators in volume mounts use backslashes (C:\\config\\rules.yaml) and the entrypoint executable is GraphProxy.Proxy.exe rather than a dotnet invocation.

Mixed-OS clusters require either separate node pools (one Linux, one Windows) or two distinct images. Do not try to run a Windows container on a Linux host or vice versa — the platform does not emulate.

Troubleshooting

Container starts but /health returns 503

The container process is running but the proxy cannot reach Key Vault. Inspect logs with docker logs graphwarden-proxy and look for Azure.Identity.CredentialUnavailableException or KeyVault: access denied. The usual cause is a missing AZURE_CLIENT_ID environment variable (for workload identity) or a service principal that lacks Secret — Get on the vault. Confirm the managed identity or service principal has the permission and that the container's AZURE_* environment variables match.

curl: command not found in Windows container health check

The Windows Server Core base image does not include curl.exe. Either switch to the Linux variant, or replace the HEALTHCHECK with a PowerShell probe (pwsh -Command "Invoke-WebRequest -UseBasicParsing http://localhost:5000/health"), or rely on your orchestrator's out-of-band liveness probe.

Proxy rejects token requests with 403 signing key mismatch

The ProxyJwt__SigningKey differs between container restarts or between multiple instances behind a load balancer. Store the signing key in your secret manager once and inject the same value into every instance. A rolling restart with a changed signing key invalidates every issued proxy JWT.

docker-compose cannot resolve the image from Azure Container Registry

The Docker daemon is not authenticated to ACR. Run az acr login --name myregistry before docker compose up, or configure ~/.docker/config.json with an access token. For CI pipelines, use docker login with a service principal credential; anonymous pulls are disabled on private ACR registries.

Container exits immediately with exit code 139 on Linux

The proxy hit a segmentation fault in the .NET runtime — almost always a mismatched base image architecture. Confirm you built the image with --platform linux/amd64 if your Docker daemon runs a different default (common on Apple Silicon machines targeting Azure). Rebuilding with docker buildx build --platform linux/amd64 resolves the mismatch.

Last reviewed:

Last reviewed: .