Deploying PolicyClue On‑Premise¶
You can run PolicyClue on‑premise using Docker Compose. The official configuration files and setup instructions are available on GitHub:
PolicyClue/policyclue-docker-compose
System Requirements¶
Hardware (small tenants) - 2 vCPU, 4–8 GB RAM, 50+ GB disk (SSD recommended).
Operating System - Linux host with Docker Engine and Docker Compose. (-> You can find the tutorial to it under: https://docs.docker.com/desktop/setup/install/linux/ubuntu/. If you downloaded docker correctly, docker compose should come along with it.)
Networking - Public HTTPS for the portal (behind a reverse proxy such as Traefik). - Access to container ports internally (DB, Redis, Elasticsearch).
Security
- Use your own TLS certificates at the reverse proxy.
- Provide strong passwords via .env (never commit secrets).
- Restrict DB and ES to the private network; expose only HTTPS.
What the stack includes - PostgreSQL, Redis, Elasticsearch, PolicyClue API, the webapp, and a Traefik reverse proxy are started by Docker Compose.
Ports and HTTPS
- Default port: the stack exposes HTTP on host port 80 via Traefik.
- HTTPS: terminate TLS at Traefik and (optionally) redirect ports 80 and 443 with ACME to auto‑issue certificates. You can also use a custom CA.
- Security warning: never expose plain HTTP directly to the public internet. It is intended only for local reverse‑proxy ingress.
Setup¶
-
Clone the repository:
git clone https://github.com/PolicyClue/policyclue-docker-compose.git cd policyclue-docker-compose -
Copy
.env.exampleto.envand fill in the secret values:Adjust values as needed; avoid renaming services unless you know the implications. Make sure you fill up all the needed blanks in that file in order for your database to function properly etc.cp .env.example .env -
Optionally, enable additional services using the override template. Rename
docker-compose.override.templatetodocker-compose.override.ymland uncomment the services you need:# Rename this file to docker-compose.override.yml and enable additional inclusions below include: # - docker-compose-acme.yml # uncomment this line to use it # - docker-compose-pgadmin.yml # uncomment this line to use it # - docker-compose-ollama.yml # uncomment this line to use it (self-hosted LLM for AI features) -
Start the stack:
Database migrations are performed automatically on container startup. After services are healthy, open the portal URL and sign in to create a tenant and configure policies and hostlists.docker compose up -d
Credentials¶
Contact PolicyClue to receive Docker registry login credentials required to pull container images for production use. Keep these credentials secure and restrict access to trusted administrators. Use docker login registry.khost.ch to sign into the registry.
Microsoft Teams DLP (On-Premise)¶
Microsoft Teams DLP monitoring uses the Microsoft Graph API, which sends webhook notifications to PolicyClue when messages are created or edited. This requires Microsoft to reach your PolicyClue portal over the internet.
If your portal is publicly accessible (e.g. behind a reverse proxy with a public domain), no additional configuration is needed. The portal's PCLUE_PORTAL_URL is used automatically as the webhook endpoint.
If your portal is behind a firewall and not directly reachable from the internet, you must expose the webhook path so Microsoft Graph can deliver notifications. You have two options:
Option A: Expose only the webhook path (recommended)¶
Configure your reverse proxy or firewall to forward only the path /api/m365/notifications from an externally accessible URL to your internal portal. No other portal endpoints need to be exposed.
Example with a Cloudflare Tunnel, Azure App Proxy, or nginx reverse proxy:
External: https://policyclue-webhook.yourcompany.com/api/m365/notifications
→ Internal: http://portal:8000/api/m365/notifications
Then enter the external base URL (e.g. https://policyclue-webhook.yourcompany.com) in the External Base URL field on the Deployment → Microsoft 365 → Credentials page. PolicyClue appends /api/m365/notifications automatically.
Option B: Use a tunnel service¶
Services like Cloudflare Tunnel or Azure App Proxy can expose an internal service without opening firewall ports. Route only the /api/m365/notifications path through the tunnel and enter the tunnel's public base URL in the portal.
What exactly needs to be reachable¶
| Path | Direction | Purpose |
|---|---|---|
/api/m365/notifications |
Inbound from Microsoft | Graph API delivers message change notifications |
All other portal paths (/api/portal/*, /api/plugin/*, webapp) can remain internal. The webhook endpoint validates all incoming requests via HMAC signatures, so exposing it does not grant access to any other portal functionality.
Verification¶
After configuring the webhook URL, go to the Status tab in Deployment → Microsoft 365. Both subscriptions (channels and chats) should show as Active. If they remain Pending, Microsoft could not reach the webhook URL - check your firewall rules and reverse proxy configuration.
LLM Gateway (optional)¶
PolicyClue can talk to any OpenAI-compatible LLM endpoint to power AI features such as AI Generate Training. The gateway is disabled by default; configure it through environment variables only - no per-tenant setup is required.
Defaults target a self-hosted Ollama container running Qwen 2.5 14B - an open-source model with strong JSON-schema adherence and multilingual content generation. Override the three env vars to use OpenAI or a LiteLLM proxy instead.
LLM_ENABLED=true
LLM_BASE_URL=http://ollama:11434/v1
LLM_API_KEY=ollama
LLM_MODEL=qwen2.5:14b
LLM_TIMEOUT_SECONDS=120
Three typical deployments:
| Backend | LLM_BASE_URL |
LLM_MODEL |
Notes |
|---|---|---|---|
| Self-hosted Ollama | http://ollama:11434/v1 |
qwen2.5:14b |
Default - fully on-premise; enable the docker-compose-ollama.yml override (see step 3) |
| OpenAI (SaaS) | https://api.openai.com/v1 |
e.g. gpt-4o-mini |
Simplest if you already have an OpenAI key |
| LiteLLM proxy | https://<your-litellm>/v1 |
whatever LiteLLM routes | Use when customers need Claude, Gemini, Azure, or a mix |
Ollama on the same stack: uncomment the docker-compose-ollama.yml include line (see step 3 above), set LLM_ENABLED=true, and pull the model once:
docker compose exec ollama ollama pull qwen2.5:14b
For CPU-only hosts or minimal GPUs, qwen2.5:7b is a lighter alternative (pull it and set LLM_MODEL=qwen2.5:7b).
Health verification: the portal's /api/portal/health/ endpoint includes an llm block reporting connectivity, the configured model, and whether it is served by the backend. If LLM_ENABLED=true but the endpoint is unreachable, the healthcheck returns 503 - set LLM_ENABLED=false to hide AI features without the alert.
Breach & Attack Simulation (optional)¶
The BAS module needs two environment variables on the API container before it
can do anything useful. The API still boots without them - they're only required
for tenants subscribed to the bas module.
PHISHING_SENDING_PROFILES='[{"host":"smtp.example.com","port":587,"username":"phish-bot","password":"<secret>","security":"starttls","from_address":"noreply@example-hr.com","default_from_name":"Example HR"}]'
BAS_HOSTNAMES=bas.example-hr.com,phish.example.com
PHISHING_SENDING_PROFILESis a JSON array. Each entry is one fully- independent sender identity (host/credentials/from_address). Thefrom_addressis unique across the array and is always the visible From-address. Simulations can only override the display name and (optionally) the SMTP MAIL FROM viaenvelope_sender, so DMARC alignment with thefrom_addressis structurally preserved. Misconfigured JSON fails the API at boot.default_from_nameis the display-name fallback used when a simulation leavesemail_from_nameblank. Set this - without it, BAS refuses to send (deliberately: a phishing-simulation From-header that says "PolicyClue" defeats the test).BAS_HOSTNAMESis a comma-separated hostname list shared by both phishing landing pages (/public/p/<token>...) and IoC file delivery (/public/ioc/<token>/...). Requests to any other host return 404.
Reverse-proxy / Caddy / Traefik routing¶
The BAS hostnames must route to the same API container as the rest of the
portal - they're public endpoints served under the shared /public/* prefix.
The split from the API hostname exists so the requests go through your normal
web-proxy / AV inspection path while the API host stays on the (typically
allow-listed) admin path. Don't allow-list the BAS hostnames past your AV
inline scanner - defeats the test.
The bundled Traefik already routes /public/* to the API container, so no
extra rule is needed when the BAS hostnames hit the bundled stack on port 80.
If you front the docker host with your own reverse proxy, just preserve the
Host: header - the host gate inside FastAPI relies on it.
Example Caddy snippet (alternative: bypass bundled Traefik and proxy straight to the api container):
bas.example-hr.com {
reverse_proxy api:8000
# Make sure your inline AV / NDR / web proxy inspects requests to this host.
# Both phishing landing pages and IoC file delivery come through here.
}
Pre-notify your SOC¶
IoC drops trigger AV alerts on real endpoints. Tell the SOC you've enabled the module before the first scheduled run so they don't open an incident on what turns out to be a PolicyClue-driven test.
Web training fallback (extension JS assets)¶
The public training page at /public/train/<token> reuses the chrome
extension's training renderer (decks + quiz) so users without the extension
get an identical UX. Three JavaScript files from policyclue-chrome-extension
need to be present inside the API image at /app/static/training-assets/:
content.jscontent.training.jscontent.training-ui.js
The bundled API image already includes them when built from a workspace where
both repos are sibling directories - the build pipeline runs
scripts/sync-extension-assets.sh (in policyclue-portal/) before
docker compose build api, which copies the four files into
api/static/training-assets/ so they get picked up by the standard COPY ./api/
step in Dockerfile-api.
Override the source path with the EXTENSION_DIR env var if your chrome-
extension checkout lives elsewhere:
EXTENSION_DIR=/path/to/policyclue-chrome-extension/source ./scripts/sync-extension-assets.sh
Inside the running container, the API discovers the files via
POLICYCLUE_EXTENSION_SOURCE_DIR (defaults to /app/static/training-assets).
If the assets are missing, /public/train/<token> returns the page shell but
the JS fails to load - the API logs a warning pointing at the expected path.
Updates¶
Pull the latest images, review the Changelog in this documentation, and restart the stack during a planned maintenance window.
