Skip to content
Start in Cloud

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.

HitKeep images are published to two registries on every release:

RegistryImage
Docker Hubpascalebeier/hitkeep
GitHub Container Registryghcr.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.

Create a compose.yml. This baseline keeps the live data, retention archive, and automatic backup snapshots in separate named volumes.

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}
# 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:-}
# 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: {}

Create a .env file alongside it (add to .gitignore):

Terminal window
{
echo "HITKEEP_JWT_SECRET=$(openssl rand -hex 32)"
echo "HITKEEP_PUBLIC_URL=http://localhost:8080"
echo "HITKEEP_MAIL_HOST="
echo "HITKEEP_MAIL_PORT=587"
echo "HITKEEP_MAIL_USERNAME="
echo "HITKEEP_MAIL_PASSWORD="
echo "HITKEEP_MAIL_FROM_ADDRESS=hitkeep@localhost"
echo "HITKEEP_MAIL_FROM_NAME=HitKeep"
} > .env

Start it:

Terminal window
docker compose up -d
docker compose logs -f hitkeep

The database file lives inside the hitkeep_data volume. Retention archives live in hitkeep_archive, and automatic database snapshots are written to hitkeep_backups. 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 and the AI model configuration guide for the exact fields.

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

Run HitKeep behind a reverse proxy for production HTTPS. Configure 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-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:-}
# 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

Create a .env file:

Terminal window
{
echo "HITKEEP_JWT_SECRET=$(openssl rand -hex 32)"
echo "HITKEEP_PUBLIC_URL=https://analytics.example.com"
echo "HITKEEP_HOSTNAME=analytics.example.com"
echo "HITKEEP_TRUSTED_PROXIES=$(docker network inspect caddy --format '{{(index .IPAM.Config 0).Subnet}}')"
echo "HITKEEP_MAIL_HOST=smtp.example.com"
echo "HITKEEP_MAIL_PORT=587"
echo "HITKEEP_MAIL_USERNAME=postmaster@example.com"
echo "HITKEEP_MAIL_PASSWORD=change-me"
echo "HITKEEP_MAIL_FROM_ADDRESS=analytics@example.com"
echo "HITKEEP_MAIL_FROM_NAME=HitKeep"
} > .env

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:

Terminal window
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
}

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:

Terminal window
# 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 for provider-specific examples.

Need managed hosting with explicit region choice? HitKeep Cloud → runs HitKeep in your chosen managed region: EU (Frankfurt) or US (Virginia).