---
title: "Self-Host HitKeep with Docker Compose | HitKeep"
description: "Self-host HitKeep with Docker Compose, persistent storage, pre-configured networking, and reverse proxy examples for Caddy, nginx, and Traefik."
canonical: "https://hitkeep.com/guides/installation/docker-compose/"
---

# Self-Host HitKeep with Docker Compose

Docker Compose gives you a reproducible, version-controlled deployment with persistent storage volumes — your analytics data stays in a named Docker volume on your server, including the shared `hitkeep.db` and any tenant-local databases created under `tenants/*/hitkeep.db`.

This page is for running HitKeep as a self-hosted service. If you want the hot-reload contributor environment with Go, Air, Angular, Mailpit, and seeded demo data in Docker, use the [Contributing guide](https://hitkeep.com/guides/contributing/).

HitKeep images are published to two registries on every release:

| Registry | Image |
| --- | --- |
| Docker Hub | pascalebeier/hitkeep |
| GitHub Container Registry | ghcr.io/pascalebeier/hitkeep |

Both registries carry identical, multi-platform images (`linux/amd64`, `linux/arm64`) with signed provenance attestations. Use whichever registry suits your network or pull-rate requirements.

Keep secrets out of compose files

Pass sensitive values (`HITKEEP_JWT_SECRET`, SMTP passwords) via environment variables, a `.env` file, or Docker secrets — not hardcoded in `compose.yml` or command flags. Keep `.env` out of version control. For stricter secret handling, use Docker secrets.

## Quick Start

A deployment is two files in one directory: `compose.yml` (copy it verbatim — you never edit it) and `.env` (your values live here). Compose substitutes every `${VARIABLE}` in `compose.yml` with the matching line from `.env` at startup.

### 1. Create compose.yml

Create a directory and add this `compose.yml`. This baseline keeps the live data, retention archive, and automatic backup snapshots in separate named volumes. The same file is maintained in the repository as [`examples/compose.yml`](https://github.com/PascaleBeier/hitkeep/blob/main/examples/compose.yml) (with a matching [`examples/.env.example`](https://github.com/PascaleBeier/hitkeep/blob/main/examples/.env.example)) if you prefer to copy from source.

```
services:
  hitkeep:
    image: pascalebeier/hitkeep:latest
    container_name: hitkeep
    restart: unless-stopped
    ports:
      - "8080:8080"
    volumes:
      - hitkeep_data:/var/lib/hitkeep/data
      - hitkeep_archive:/var/lib/hitkeep/archive
      - hitkeep_backups:/var/lib/hitkeep/backups
    environment:
      # Public URL must match the browser-visible origin, including any path prefix.
      HITKEEP_PUBLIC_URL: ${HITKEEP_PUBLIC_URL:-http://localhost:8080}
      # Required for stable sessions. Generate with: openssl rand -hex 32
      HITKEEP_JWT_SECRET: ${HITKEEP_JWT_SECRET:?set HITKEEP_JWT_SECRET in .env}
      # Behind a reverse proxy, set to the proxy network CIDR so real client IPs are used.
      HITKEEP_TRUSTED_PROXIES: ${HITKEEP_TRUSTED_PROXIES:-} # config-default-override: require an explicit trusted proxy network
      # Keep live data, retention archives, and backup snapshots on persistent volumes.
      HITKEEP_DB_PATH: /var/lib/hitkeep/data/hitkeep.db
      HITKEEP_DATA_PATH: /var/lib/hitkeep/data
      HITKEEP_ARCHIVE_PATH: /var/lib/hitkeep/archive
      HITKEEP_BACKUP_PATH: /var/lib/hitkeep/backups
      HITKEEP_BACKUP_INTERVAL: ${HITKEEP_BACKUP_INTERVAL:-60}
      HITKEEP_BACKUP_RETENTION: ${HITKEEP_BACKUP_RETENTION:-24}
      # Optional OSS spam-list refresh. Disable for fully offline/air-gapped installs.
      HITKEEP_SPAM_FILTER_AUTO_UPDATE: "true"
      HITKEEP_SPAM_FILTER_UPDATE_INTERVAL: ${HITKEEP_SPAM_FILTER_UPDATE_INTERVAL:-1440}
      HITKEEP_SPAM_FILTER_PATH: /var/lib/hitkeep/data/spam-filter.json
      # Optional read-only MCP endpoint for governed assistant/reporting access.
      HITKEEP_MCP_ENABLED: "true"
      HITKEEP_MCP_PATH: /mcp
      HITKEEP_MCP_MAX_RANGE_DAYS: ${HITKEEP_MCP_MAX_RANGE_DAYS:-366}
      # Optional AI model route for Opportunity enrichment. Provider credentials
      # use the selected goAI provider's own env vars, such as OPENAI_API_KEY.
      HITKEEP_AI_ENABLED: ${HITKEEP_AI_ENABLED:-false}
      HITKEEP_AI_PROVIDER: ${HITKEEP_AI_PROVIDER:-}
      HITKEEP_AI_MODEL: ${HITKEEP_AI_MODEL:-}
      HITKEEP_AI_BASE_URL: ${HITKEEP_AI_BASE_URL:-}
      HITKEEP_AI_REGION: ${HITKEEP_AI_REGION:-}
      HITKEEP_AI_API_KEY: ${HITKEEP_AI_API_KEY:-}
      HITKEEP_AI_REQUEST_LIMIT: ${HITKEEP_AI_REQUEST_LIMIT:-100}
      HITKEEP_AI_TOKEN_LIMIT: ${HITKEEP_AI_TOKEN_LIMIT:-100000}
      HITKEEP_AI_BUDGET_WINDOW: ${HITKEEP_AI_BUDGET_WINDOW:-1440}
      # Optional Google Search Console OAuth integration.
      HITKEEP_GOOGLE_SEARCH_CONSOLE_CLIENT_ID: ${HITKEEP_GOOGLE_SEARCH_CONSOLE_CLIENT_ID:-}
      HITKEEP_GOOGLE_SEARCH_CONSOLE_CLIENT_SECRET: ${HITKEEP_GOOGLE_SEARCH_CONSOLE_CLIENT_SECRET:-}
      HITKEEP_GOOGLE_SEARCH_CONSOLE_REDIRECT_URL: ${HITKEEP_GOOGLE_SEARCH_CONSOLE_REDIRECT_URL:-}
      # Optional Google, GitHub, and Microsoft social sign-in.
      HITKEEP_SOCIAL_GOOGLE_CLIENT_ID: ${HITKEEP_SOCIAL_GOOGLE_CLIENT_ID:-}
      HITKEEP_SOCIAL_GOOGLE_CLIENT_SECRET: ${HITKEEP_SOCIAL_GOOGLE_CLIENT_SECRET:-}
      HITKEEP_SOCIAL_GITHUB_CLIENT_ID: ${HITKEEP_SOCIAL_GITHUB_CLIENT_ID:-}
      HITKEEP_SOCIAL_GITHUB_CLIENT_SECRET: ${HITKEEP_SOCIAL_GITHUB_CLIENT_SECRET:-}
      HITKEEP_SOCIAL_MICROSOFT_CLIENT_ID: ${HITKEEP_SOCIAL_MICROSOFT_CLIENT_ID:-}
      HITKEEP_SOCIAL_MICROSOFT_CLIENT_SECRET: ${HITKEEP_SOCIAL_MICROSOFT_CLIENT_SECRET:-}
      HITKEEP_SOCIAL_MICROSOFT_TENANT: ${HITKEEP_SOCIAL_MICROSOFT_TENANT:-common}
      # SMTP powers invites, password reset, email reports, and security mail.
      HITKEEP_MAIL_HOST: ${HITKEEP_MAIL_HOST:-}
      HITKEEP_MAIL_PORT: ${HITKEEP_MAIL_PORT:-587}
      HITKEEP_MAIL_USERNAME: ${HITKEEP_MAIL_USERNAME:-}
      HITKEEP_MAIL_PASSWORD: ${HITKEEP_MAIL_PASSWORD:-}
      HITKEEP_MAIL_ENCRYPTION: ${HITKEEP_MAIL_ENCRYPTION:-tls}
      HITKEEP_MAIL_FROM_ADDRESS: ${HITKEEP_MAIL_FROM_ADDRESS:-hitkeep@localhost}
      HITKEEP_MAIL_FROM_NAME: ${HITKEEP_MAIL_FROM_NAME:-HitKeep}

volumes:
  hitkeep_data: {}
  hitkeep_archive: {}
  hitkeep_backups: {}
```

### 2. Create .env

Generate the session signing key first — run this in your terminal and copy the output:

```
openssl rand -hex 32
```

If `openssl` is not installed, `head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \n'; echo` produces an equivalent value on any Linux box.

Then create a file named `.env` next to `compose.yml` with your editor and fill in your values:

```
# Required. Paste the generated 64-character hex string here.
HITKEEP_JWT_SECRET=paste-the-generated-value-here
# Required. The exact URL you open in the browser. Behind a reverse proxy
# this is your public domain, e.g. https://analytics.example.com
HITKEEP_PUBLIC_URL=http://localhost:8080
# Optional social sign-in. Set both values for each provider you enable.
HITKEEP_SOCIAL_GOOGLE_CLIENT_ID=
HITKEEP_SOCIAL_GOOGLE_CLIENT_SECRET=
HITKEEP_SOCIAL_GITHUB_CLIENT_ID=
HITKEEP_SOCIAL_GITHUB_CLIENT_SECRET=
HITKEEP_SOCIAL_MICROSOFT_CLIENT_ID=
HITKEEP_SOCIAL_MICROSOFT_CLIENT_SECRET=
HITKEEP_SOCIAL_MICROSOFT_TENANT=common
# Optional SMTP. Powers invites, password resets, and email reports.
# Leave the values empty to run without outbound mail.
HITKEEP_MAIL_HOST=
HITKEEP_MAIL_PORT=587
HITKEEP_MAIL_USERNAME=
HITKEEP_MAIL_PASSWORD=
HITKEEP_MAIL_FROM_ADDRESS=hitkeep@localhost
HITKEEP_MAIL_FROM_NAME=HitKeep
```

.env is not a shell script

`.env` holds plain `KEY=value` lines — no `echo`, no braces, no `$(command)` substitution. Docker Compose reads it literally, so commands pasted into the file arrive as literal text. Quotes around values are unnecessary. Keep the file out of version control (add it to `.gitignore`).

The two files use different syntax on purpose: `compose.yml` is YAML, so environment entries there are written `KEY: value`; `.env` is dotenv format, written `KEY=value`.

### 3. Start HitKeep

```
docker compose up -d
docker compose logs -f hitkeep
```

The log lines `Starting HitKeep` and `HTTP server starting` mean the service is up.

### 4. Sign in

Open `HITKEEP_PUBLIC_URL` in your browser. The setup wizard creates the first account, which automatically becomes the instance owner, and walks you through adding your first site.

The database file lives inside the `hitkeep_data` volume. The default automatic-recovery directory, `/var/lib/hitkeep/data/recovery`, is therefore persistent too. Retention archives live in `hitkeep_archive`, and automatic database snapshots are written to `hitkeep_backups`. Recovery bundles contain database material, are not rotated with backup retention, and must be protected and expired separately. MCP is exposed at `/mcp`, but clients still need scoped API client bearer tokens.

MCP is optional in the product and is enabled in this compose example for teams that want read-only assistant/reporting access. Remove `HITKEEP_MCP_ENABLED`, `HITKEEP_MCP_PATH`, and `HITKEEP_MCP_MAX_RANGE_DAYS` if you do not plan to publish an MCP endpoint.

AI provider enrichment is optional and disabled by default. Keep `HITKEEP_AI_ENABLED=false` until you have chosen a provider/model, configured credentials, and set local budget caps. See [Opportunity Recommendations](https://hitkeep.com/guides/analytics/opportunities/) and the [AI model configuration guide](https://hitkeep.com/guides/admin/ai-model-configuration/) for the exact fields.

Social sign-in is optional. Set both the client ID and client secret for each provider you want HitKeep to show, then register the exact callback URL derived from `HITKEEP_PUBLIC_URL`. The [Social Sign-In guide](https://hitkeep.com/guides/security/social-sign-in/) covers provider registration, callback paths, email-verification behavior, invitations, MFA, and account linking. `HITKEEP_SOCIAL_SIGNUP_ENABLED` does not open public signup on a self-hosted instance.

City, provider, and ASN lookup data is embedded in release images. Runtime Compose deployments do not need `IP2LOCATION_DOWNLOAD_TOKEN`.

## Reverse Proxy Configurations

Run HitKeep behind a reverse proxy for production HTTPS. Configure [Trusted Proxies](https://hitkeep.com/guides/installation/trusted-proxies/) so real client IPs are used for analytics and rate limiting.

`HITKEEP_PUBLIC_URL` may include a path prefix. For example, set `HITKEEP_PUBLIC_URL=https://www.example.net/hitkeep/` when HitKeep is mounted below an existing site. The proxy should publish the same prefix, such as `/hitkeep/*`, and forward it to the HitKeep container. HitKeep then serves the dashboard base href, API requests, `hk.js`, `hk-vitals.js`, and ingest endpoints below that prefix.

- Caddy
- nginx
- Traefik

`caddy-docker-proxy` handles automatic HTTPS (Let’s Encrypt) and generates Caddy config directly from Docker labels. Best practice is to use a dedicated ingress network and trust only that network CIDR in HitKeep.

```
services:
caddy:
  image: lucaslorentz/caddy-docker-proxy:2.9-alpine
  container_name: caddy-proxy
  restart: unless-stopped
  ports:
    - "80:80"
    - "443:443"
    - "443:443/udp"
  environment:
    CADDY_INGRESS_NETWORKS: caddy
  volumes:
    - /var/run/docker.sock:/var/run/docker.sock:ro
    - caddy_data:/data
    - caddy_config:/config
  networks:
    - caddy
  labels:
    caddy.email: ${CADDY_EMAIL:?set in .env}

hitkeep:
  image: pascalebeier/hitkeep:latest
  container_name: hitkeep
  restart: unless-stopped
  networks:
    - caddy
  volumes:
    - hitkeep_data:/var/lib/hitkeep/data
    - hitkeep_archive:/var/lib/hitkeep/archive
    - hitkeep_backups:/var/lib/hitkeep/backups
  environment:
    HITKEEP_NODE_NAME: leader
    # Public URL must match the HTTPS origin served by Caddy, including any path prefix.
    HITKEEP_PUBLIC_URL: ${HITKEEP_PUBLIC_URL:?set in .env}
    # Required for stable sessions. Generate with: openssl rand -hex 32
    HITKEEP_JWT_SECRET: ${HITKEEP_JWT_SECRET:?set in .env}
    # Must be the Caddy network CIDR. This controls real visitor IP, geo/network metadata,
    # country exclusions, rate limiting, and spam checks.
    HITKEEP_TRUSTED_PROXIES: ${HITKEEP_TRUSTED_PROXIES:?set to your caddy network CIDR}
    # Keep live data, retention archives, and backup snapshots on persistent volumes.
    HITKEEP_DB_PATH: /var/lib/hitkeep/data/hitkeep.db
    HITKEEP_DATA_PATH: /var/lib/hitkeep/data
    HITKEEP_ARCHIVE_PATH: /var/lib/hitkeep/archive
    HITKEEP_BACKUP_PATH: /var/lib/hitkeep/backups
    HITKEEP_BACKUP_INTERVAL: ${HITKEEP_BACKUP_INTERVAL:-60}
    HITKEEP_BACKUP_RETENTION: ${HITKEEP_BACKUP_RETENTION:-24}
    # Optional OSS spam-list refresh. Disable for fully offline/air-gapped installs.
    HITKEEP_SPAM_FILTER_AUTO_UPDATE: "true"
    HITKEEP_SPAM_FILTER_UPDATE_INTERVAL: ${HITKEEP_SPAM_FILTER_UPDATE_INTERVAL:-1440}
    HITKEEP_SPAM_FILTER_PATH: /var/lib/hitkeep/data/spam-filter.json
    # Optional read-only MCP endpoint for governed assistant/reporting access.
    HITKEEP_MCP_ENABLED: "true"
    HITKEEP_MCP_PATH: /mcp
    HITKEEP_MCP_MAX_RANGE_DAYS: ${HITKEEP_MCP_MAX_RANGE_DAYS:-366}
    # Optional AI model route for Opportunity enrichment. Leave disabled until a provider is configured.
    HITKEEP_AI_ENABLED: ${HITKEEP_AI_ENABLED:-false}
    HITKEEP_AI_PROVIDER: ${HITKEEP_AI_PROVIDER:-}
    HITKEEP_AI_MODEL: ${HITKEEP_AI_MODEL:-}
    HITKEEP_AI_BASE_URL: ${HITKEEP_AI_BASE_URL:-}
    HITKEEP_AI_REGION: ${HITKEEP_AI_REGION:-}
    HITKEEP_AI_API_KEY: ${HITKEEP_AI_API_KEY:-}
    HITKEEP_AI_REQUEST_LIMIT: ${HITKEEP_AI_REQUEST_LIMIT:-100}
    HITKEEP_AI_TOKEN_LIMIT: ${HITKEEP_AI_TOKEN_LIMIT:-100000}
    HITKEEP_AI_BUDGET_WINDOW: ${HITKEEP_AI_BUDGET_WINDOW:-1440}
    # Optional Google Search Console OAuth integration.
    HITKEEP_GOOGLE_SEARCH_CONSOLE_CLIENT_ID: ${HITKEEP_GOOGLE_SEARCH_CONSOLE_CLIENT_ID:-}
    HITKEEP_GOOGLE_SEARCH_CONSOLE_CLIENT_SECRET: ${HITKEEP_GOOGLE_SEARCH_CONSOLE_CLIENT_SECRET:-}
    HITKEEP_GOOGLE_SEARCH_CONSOLE_REDIRECT_URL: ${HITKEEP_GOOGLE_SEARCH_CONSOLE_REDIRECT_URL:-}
    # Optional Google, GitHub, and Microsoft social sign-in.
    HITKEEP_SOCIAL_GOOGLE_CLIENT_ID: ${HITKEEP_SOCIAL_GOOGLE_CLIENT_ID:-}
    HITKEEP_SOCIAL_GOOGLE_CLIENT_SECRET: ${HITKEEP_SOCIAL_GOOGLE_CLIENT_SECRET:-}
    HITKEEP_SOCIAL_GITHUB_CLIENT_ID: ${HITKEEP_SOCIAL_GITHUB_CLIENT_ID:-}
    HITKEEP_SOCIAL_GITHUB_CLIENT_SECRET: ${HITKEEP_SOCIAL_GITHUB_CLIENT_SECRET:-}
    HITKEEP_SOCIAL_MICROSOFT_CLIENT_ID: ${HITKEEP_SOCIAL_MICROSOFT_CLIENT_ID:-}
    HITKEEP_SOCIAL_MICROSOFT_CLIENT_SECRET: ${HITKEEP_SOCIAL_MICROSOFT_CLIENT_SECRET:-}
    HITKEEP_SOCIAL_MICROSOFT_TENANT: ${HITKEEP_SOCIAL_MICROSOFT_TENANT:-common}
    # SMTP powers invites, password reset, email reports, and security mail.
    HITKEEP_MAIL_HOST: ${HITKEEP_MAIL_HOST:-}
    HITKEEP_MAIL_PORT: ${HITKEEP_MAIL_PORT:-587}
    HITKEEP_MAIL_USERNAME: ${HITKEEP_MAIL_USERNAME:-}
    HITKEEP_MAIL_PASSWORD: ${HITKEEP_MAIL_PASSWORD:-}
    HITKEEP_MAIL_ENCRYPTION: ${HITKEEP_MAIL_ENCRYPTION:-tls}
    HITKEEP_MAIL_FROM_ADDRESS: ${HITKEEP_MAIL_FROM_ADDRESS:-hitkeep@localhost}
    HITKEEP_MAIL_FROM_NAME: ${HITKEEP_MAIL_FROM_NAME:-HitKeep}
  labels:
    caddy: ${HITKEEP_HOSTNAME:?set in .env}
    caddy.reverse_proxy: "{{upstreams 8080}}"
    caddy.encode: "zstd gzip"

volumes:
caddy_data: {}
caddy_config: {}
hitkeep_data: {}
hitkeep_archive: {}
hitkeep_backups: {}

networks:
caddy:
  external: true
```

Two values come from your terminal — the session secret and the Caddy network CIDR:

```
openssl rand -hex 32
docker network inspect caddy --format '{{(index .IPAM.Config 0).Subnet}}'
```

Then create the `.env` file with your editor:

```
# Required. Paste the generated 64-character hex string here.
HITKEEP_JWT_SECRET=paste-the-generated-value-here
HITKEEP_PUBLIC_URL=https://analytics.example.com
HITKEEP_HOSTNAME=analytics.example.com
CADDY_EMAIL=ops@example.com
# Paste the Caddy network subnet from the docker network inspect command.
HITKEEP_TRUSTED_PROXIES=paste-the-caddy-subnet-here
# Optional social sign-in. Set both values for each provider you enable.
HITKEEP_SOCIAL_GOOGLE_CLIENT_ID=
HITKEEP_SOCIAL_GOOGLE_CLIENT_SECRET=
HITKEEP_SOCIAL_GITHUB_CLIENT_ID=
HITKEEP_SOCIAL_GITHUB_CLIENT_SECRET=
HITKEEP_SOCIAL_MICROSOFT_CLIENT_ID=
HITKEEP_SOCIAL_MICROSOFT_CLIENT_SECRET=
HITKEEP_SOCIAL_MICROSOFT_TENANT=common
HITKEEP_MAIL_HOST=smtp.example.com
HITKEEP_MAIL_PORT=587
HITKEEP_MAIL_USERNAME=postmaster@example.com
HITKEEP_MAIL_PASSWORD=change-me
HITKEEP_MAIL_FROM_ADDRESS=analytics@example.com
HITKEEP_MAIL_FROM_NAME=HitKeep
```

`HITKEEP_TRUSTED_PROXIES` must be the CIDR of your reverse-proxy network (not `0.0.0.0/0`).

For a subdirectory mount, keep `HITKEEP_PUBLIC_URL` and the reverse-proxy route prefix aligned. The equivalent Caddy route is:

```
HITKEEP_PUBLIC_URL=https://www.example.net/hitkeep/
HITKEEP_HOSTNAME=www.example.net
```

```
www.example.net {
handle /hitkeep* {
  reverse_proxy hitkeep:8080
}
encode zstd gzip
}
```

For an existing nginx on the host (managing its own certificates, e.g. via certbot), keep the Quick Start `compose.yml` and point nginx at the published port.

Two adjustments to the Quick Start files:

1. In `compose.yml`, bind the port to loopback so only the local nginx can reach the container:

```
    ports:
    - "127.0.0.1:8080:8080"
```

1. In `.env`, set the public URL to the domain nginx serves and trust the Docker network — host traffic reaches the container through the Docker network gateway, which lives in the `172.16.0.0/12` range:

```
HITKEEP_PUBLIC_URL=https://analytics.example.com
HITKEEP_TRUSTED_PROXIES=172.16.0.0/12
```

Then add a server block to nginx:

```
server {
  listen 443 ssl;
  http2 on;
  server_name analytics.example.com;

  # ssl_certificate / ssl_certificate_key as issued by certbot or your CA.

  location / {
      proxy_pass http://127.0.0.1:8080;
      proxy_http_version 1.1;
      proxy_set_header Host $host;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header X-Forwarded-Proto $scheme;
      proxy_set_header X-Forwarded-Host $host;
      # Live dashboard updates stream over server-sent events.
      proxy_set_header Connection "";
      proxy_buffering off;
      proxy_read_timeout 300s;
  }
}
```

Reload nginx (`nginx -t && systemctl reload nginx`) and open `https://analytics.example.com`. If visitors all appear to come from one private IP, revisit [Trusted Proxies](https://hitkeep.com/guides/installation/trusted-proxies/).

For existing Traefik stacks, expose HitKeep with labels:

```
services:
hitkeep:
  image: pascalebeier/hitkeep:latest
  restart: unless-stopped
  volumes:
    - hitkeep_data:/var/lib/hitkeep/data
    - hitkeep_archive:/var/lib/hitkeep/archive
    - hitkeep_backups:/var/lib/hitkeep/backups
  environment:
    # Public URL must match the HTTPS origin served by Traefik, including any path prefix.
    HITKEEP_PUBLIC_URL: ${HITKEEP_PUBLIC_URL:?set in .env}
    # Required for stable sessions. Generate with: openssl rand -hex 32
    HITKEEP_JWT_SECRET: ${HITKEEP_JWT_SECRET:?set in .env}
    # Must be the Traefik network CIDR. This controls real visitor IP, geo/network metadata,
    # country exclusions, rate limiting, and spam checks.
    HITKEEP_TRUSTED_PROXIES: ${HITKEEP_TRUSTED_PROXIES:?set to your traefik network CIDR}
    # Keep live data, retention archives, and backup snapshots on persistent volumes.
    HITKEEP_DB_PATH: /var/lib/hitkeep/data/hitkeep.db
    HITKEEP_DATA_PATH: /var/lib/hitkeep/data
    HITKEEP_ARCHIVE_PATH: /var/lib/hitkeep/archive
    HITKEEP_BACKUP_PATH: /var/lib/hitkeep/backups
    HITKEEP_BACKUP_INTERVAL: ${HITKEEP_BACKUP_INTERVAL:-60}
    HITKEEP_BACKUP_RETENTION: ${HITKEEP_BACKUP_RETENTION:-24}
    # Optional OSS spam-list refresh. Disable for fully offline/air-gapped installs.
    HITKEEP_SPAM_FILTER_AUTO_UPDATE: "true"
    HITKEEP_SPAM_FILTER_UPDATE_INTERVAL: ${HITKEEP_SPAM_FILTER_UPDATE_INTERVAL:-1440}
    HITKEEP_SPAM_FILTER_PATH: /var/lib/hitkeep/data/spam-filter.json
    # Optional read-only MCP endpoint for governed assistant/reporting access.
    HITKEEP_MCP_ENABLED: "true"
    HITKEEP_MCP_PATH: /mcp
    HITKEEP_MCP_MAX_RANGE_DAYS: ${HITKEEP_MCP_MAX_RANGE_DAYS:-366}
    # Optional AI model route for Opportunity enrichment. Leave disabled until a provider is configured.
    HITKEEP_AI_ENABLED: ${HITKEEP_AI_ENABLED:-false}
    HITKEEP_AI_PROVIDER: ${HITKEEP_AI_PROVIDER:-}
    HITKEEP_AI_MODEL: ${HITKEEP_AI_MODEL:-}
    HITKEEP_AI_BASE_URL: ${HITKEEP_AI_BASE_URL:-}
    HITKEEP_AI_REGION: ${HITKEEP_AI_REGION:-}
    HITKEEP_AI_API_KEY: ${HITKEEP_AI_API_KEY:-}
    HITKEEP_AI_REQUEST_LIMIT: ${HITKEEP_AI_REQUEST_LIMIT:-100}
    HITKEEP_AI_TOKEN_LIMIT: ${HITKEEP_AI_TOKEN_LIMIT:-100000}
    HITKEEP_AI_BUDGET_WINDOW: ${HITKEEP_AI_BUDGET_WINDOW:-1440}
    # Optional Google Search Console OAuth integration.
    HITKEEP_GOOGLE_SEARCH_CONSOLE_CLIENT_ID: ${HITKEEP_GOOGLE_SEARCH_CONSOLE_CLIENT_ID:-}
    HITKEEP_GOOGLE_SEARCH_CONSOLE_CLIENT_SECRET: ${HITKEEP_GOOGLE_SEARCH_CONSOLE_CLIENT_SECRET:-}
    HITKEEP_GOOGLE_SEARCH_CONSOLE_REDIRECT_URL: ${HITKEEP_GOOGLE_SEARCH_CONSOLE_REDIRECT_URL:-}
    # Optional Google, GitHub, and Microsoft social sign-in.
    HITKEEP_SOCIAL_GOOGLE_CLIENT_ID: ${HITKEEP_SOCIAL_GOOGLE_CLIENT_ID:-}
    HITKEEP_SOCIAL_GOOGLE_CLIENT_SECRET: ${HITKEEP_SOCIAL_GOOGLE_CLIENT_SECRET:-}
    HITKEEP_SOCIAL_GITHUB_CLIENT_ID: ${HITKEEP_SOCIAL_GITHUB_CLIENT_ID:-}
    HITKEEP_SOCIAL_GITHUB_CLIENT_SECRET: ${HITKEEP_SOCIAL_GITHUB_CLIENT_SECRET:-}
    HITKEEP_SOCIAL_MICROSOFT_CLIENT_ID: ${HITKEEP_SOCIAL_MICROSOFT_CLIENT_ID:-}
    HITKEEP_SOCIAL_MICROSOFT_CLIENT_SECRET: ${HITKEEP_SOCIAL_MICROSOFT_CLIENT_SECRET:-}
    HITKEEP_SOCIAL_MICROSOFT_TENANT: ${HITKEEP_SOCIAL_MICROSOFT_TENANT:-common}
    # SMTP powers invites, password reset, email reports, and security mail.
    HITKEEP_MAIL_HOST: ${HITKEEP_MAIL_HOST:-}
    HITKEEP_MAIL_PORT: ${HITKEEP_MAIL_PORT:-587}
    HITKEEP_MAIL_USERNAME: ${HITKEEP_MAIL_USERNAME:-}
    HITKEEP_MAIL_PASSWORD: ${HITKEEP_MAIL_PASSWORD:-}
    HITKEEP_MAIL_ENCRYPTION: ${HITKEEP_MAIL_ENCRYPTION:-tls}
    HITKEEP_MAIL_FROM_ADDRESS: ${HITKEEP_MAIL_FROM_ADDRESS:-hitkeep@localhost}
    HITKEEP_MAIL_FROM_NAME: ${HITKEEP_MAIL_FROM_NAME:-HitKeep}
  labels:
    - "traefik.enable=true"
    - "traefik.http.routers.hitkeep.rule=Host(`analytics.example.com`)"
    - "traefik.http.routers.hitkeep.entrypoints=websecure"
    - "traefik.http.routers.hitkeep.tls.certresolver=myresolver"
    - "traefik.http.services.hitkeep.loadbalancer.server.port=8080"

volumes:
hitkeep_data: {}
hitkeep_archive: {}
hitkeep_backups: {}
```

## Custom tracking domains

Self-hosted teams can use [Custom Tracking Domains](https://hitkeep.com/guides/tracking/custom-tracking-domains/) with any reverse proxy that terminates TLS and preserves the original `Host` header. Use external TLS mode when certificates are managed by nginx, Traefik, a load balancer, or another ACME process. Use the Caddy profile only when you want dashboard-driven domain onboarding with on-demand certificate issuance.

### External TLS with nginx or Traefik

Add these HitKeep settings when the reverse proxy already handles certificates:

```
HITKEEP_CUSTOM_TRACKING_TLS_MODE: external
```

In external mode, add each tracking hostname to the proxy before the team clicks **Verify** in HitKeep. The verification probe requires `https://<hostname>/hk.js` to return the tracker with a valid certificate.

The HitKeep repository includes non-Caddy examples:

- [`examples/nginx.custom-tracking.conf`](https://github.com/PascaleBeier/hitkeep/blob/main/examples/nginx.custom-tracking.conf)
- [`examples/compose.nginx-custom-tracking.yml`](https://github.com/PascaleBeier/hitkeep/blob/main/examples/compose.nginx-custom-tracking.yml)
- [`examples/traefik.custom-tracking.yml`](https://github.com/PascaleBeier/hitkeep/blob/main/examples/traefik.custom-tracking.yml)
- [`examples/compose.traefik-custom-tracking.yml`](https://github.com/PascaleBeier/hitkeep/blob/main/examples/compose.traefik-custom-tracking.yml)

The nginx example restricts custom tracking hosts to tracker assets and ingest routes at the proxy. The Traefik example uses exact `Host(...)` entries for every tracking hostname; do not replace that with a catch-all rule unless you intentionally want the proxy to accept arbitrary hostnames.

### Caddy on-demand TLS

Add these HitKeep settings to the `hitkeep` service:

```
HITKEEP_CUSTOM_TRACKING_TLS_MODE: caddy-on-demand
HITKEEP_CADDY_TLS_ASK_TOKEN: ${HITKEEP_CADDY_TLS_ASK_TOKEN:?set in .env}
# Optional. Use only when tracker hostnames point somewhere other than
# the host in HITKEEP_PUBLIC_URL.
HITKEEP_CUSTOM_TRACKING_DNS_TARGET: ${HITKEEP_CUSTOM_TRACKING_DNS_TARGET:-}
```

Generate the ask token in your terminal (`openssl rand -hex 32`) and add it as a new line in `.env`:

```
HITKEEP_CADDY_TLS_ASK_TOKEN=paste-the-generated-value-here
```

Use a Caddyfile with persistent storage and a restricted on-demand TLS ask endpoint:

```
{
email ops@example.com
storage file_system {
  root /data
}
on_demand_tls {
  ask http://hitkeep:8080/internal/caddy/on-demand-tls/{$HITKEEP_CADDY_TLS_ASK_TOKEN}
}
}

{$HITKEEP_HOSTNAME} {
reverse_proxy hitkeep:8080
encode zstd gzip
}

https:// {
tls {
  on_demand
}

reverse_proxy hitkeep:8080 {
  header_up Host {host}
  header_up X-Forwarded-Host {host}
  header_up X-Forwarded-Proto {scheme}
}

encode zstd gzip
}
```

Caddy sends `?domain=` to the ask URL before issuing a certificate. HitKeep returns `204` only when the token matches and the requested hostname is an enabled DNS-verified custom tracking domain. Do not run Caddy on-demand TLS without `ask`; otherwise any hostname that reaches your Caddy listener can attempt certificate issuance.

The HitKeep repository includes a complete optional profile:

- [`examples/Caddyfile.custom-tracking`](https://github.com/PascaleBeier/hitkeep/blob/main/examples/Caddyfile.custom-tracking)
- [`examples/compose.caddy-on-demand.yml`](https://github.com/PascaleBeier/hitkeep/blob/main/examples/compose.caddy-on-demand.yml)

## Use S3-compatible backups or archives

The examples above use local Docker volumes. To write backup snapshots or retention archives to object storage, replace the local paths with `s3://` URLs and add the S3 settings to `.env`:

```
# Local or S3 backup snapshots. Empty HITKEEP_BACKUP_PATH disables automatic backups.
HITKEEP_BACKUP_PATH=s3://my-analytics-bucket/hitkeep/backups
# Retention archives can use local paths or S3-compatible URLs.
HITKEEP_ARCHIVE_PATH=s3://my-analytics-bucket/hitkeep/archive
HITKEEP_S3_REGION=eu-central-1
# Static credentials are optional when your container runtime provides an AWS credential chain.
HITKEEP_S3_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
HITKEEP_S3_SECRET_ACCESS_KEY=change-me
```

For MinIO, Cloudflare R2, Backblaze B2, or another S3-compatible endpoint, also set `HITKEEP_S3_ENDPOINT` and, when needed, `HITKEEP_S3_URL_STYLE=path`. See [S3 Backups](https://hitkeep.com/guides/data/s3-backups/) for provider-specific examples.

## Troubleshooting

**The dashboard shows “An unexpected error occurred” during setup or sign-in.** Work through these in order:

1. **Read the container logs first**: `docker compose logs hitkeep`. HitKeep logs every server-side failure at error level. If the browser shows an error but the logs stay silent, the request never reached the container — that points at the reverse proxy or a wrong `HITKEEP_PUBLIC_URL`, not at HitKeep.
2. **`HITKEEP_PUBLIC_URL` must exactly match the address bar** — scheme, host, and port. With an `https://` public URL, session cookies are marked secure and sign-in over plain HTTP silently fails. After changing `.env`, run `docker compose up -d` again; Compose only applies environment changes when it recreates the container.
3. **`Setup has already been completed.`** means a user already exists in the database — typically a `hitkeep_data` volume left over from an earlier attempt. `docker compose down -v` removes the named volumes and gives you a clean start (this deletes all analytics data).
4. **Mail settings are not the culprit during setup.** Creating the first account sends no email; empty `HITKEEP_MAIL_*` values never block it. SMTP only matters later for invites, password resets, and email reports.
5. **Need more detail?** Add `HITKEEP_LOG_LEVEL: debug` under `environment:` in `compose.yml` and run `docker compose up -d` again.

## Related

- [Trusted Proxies](https://hitkeep.com/guides/installation/trusted-proxies/)
- [Social Sign-In](https://hitkeep.com/guides/security/social-sign-in/)
- [Custom Tracking Domains](https://hitkeep.com/guides/tracking/custom-tracking-domains/)
- [Configuration Reference](https://hitkeep.com/reference/configuration/)
- [Data Retention](https://hitkeep.com/guides/data/retention/)
- [S3 Backups](https://hitkeep.com/guides/data/s3-backups/)
- [Binary Installation](https://hitkeep.com/guides/installation/binary/)

Need managed hosting with explicit region choice? [HitKeep Cloud →](https://cloud.hitkeep.eu/signup?plan=free&billing=monthly&utm_source=hitkeep_docs&utm_medium=referral&utm_campaign=cloud_signup&utm_content=docs_inline) runs HitKeep in your chosen managed region: EU (Frankfurt) or US (Virginia).

[Previous Binary installation](https://hitkeep.com/guides/installation/binary/)[Next Kubernetes and Helm](https://hitkeep.com/guides/installation/kubernetes/)
