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.
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.
Quick Start
Section titled “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
Section titled “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 (with a matching 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:-} # 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: {}2. Create .env
Section titled “2. Create .env”Generate the session signing key first — run this in your terminal and copy the output:
openssl rand -hex 32If 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.comHITKEEP_PUBLIC_URL=http://localhost:8080# Optional SMTP. Powers invites, password resets, and email reports.# Leave the values empty to run without outbound mail.HITKEEP_MAIL_HOST=HITKEEP_MAIL_PORT=587HITKEEP_MAIL_USERNAME=HITKEEP_MAIL_PASSWORD=HITKEEP_MAIL_FROM_ADDRESS=hitkeep@localhostHITKEEP_MAIL_FROM_NAME=HitKeep3. Start HitKeep
Section titled “3. Start HitKeep”docker compose up -ddocker compose logs -f hitkeepThe log lines Starting HitKeep and HTTP server starting mean the service is up.
4. Sign in
Section titled “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 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.
Reverse Proxy Configurations
Section titled “Reverse Proxy Configurations”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: trueTwo values come from your terminal — the session secret and the Caddy network CIDR:
openssl rand -hex 32docker 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-hereHITKEEP_PUBLIC_URL=https://analytics.example.comHITKEEP_HOSTNAME=analytics.example.comCADDY_EMAIL=ops@example.com# Paste the Caddy network subnet from the docker network inspect command.HITKEEP_TRUSTED_PROXIES=paste-the-caddy-subnet-hereHITKEEP_MAIL_HOST=smtp.example.comHITKEEP_MAIL_PORT=587HITKEEP_MAIL_USERNAME=postmaster@example.comHITKEEP_MAIL_PASSWORD=change-meHITKEEP_MAIL_FROM_ADDRESS=analytics@example.comHITKEEP_MAIL_FROM_NAME=HitKeepHITKEEP_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.netwww.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:
- In
compose.yml, bind the port to loopback so only the local nginx can reach the container:
ports: - "127.0.0.1:8080:8080"- 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 the172.16.0.0/12range:
HITKEEP_PUBLIC_URL=https://analytics.example.comHITKEEP_TRUSTED_PROXIES=172.16.0.0/12Then 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.
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:-} # 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
Section titled “Custom tracking domains”Self-hosted teams can use 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
Section titled “External TLS with nginx or Traefik”Add these HitKeep settings when the reverse proxy already handles certificates:
HITKEEP_CUSTOM_TRACKING_TLS_MODE: externalIn 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.confexamples/compose.nginx-custom-tracking.ymlexamples/traefik.custom-tracking.ymlexamples/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
Section titled “Caddy on-demand TLS”Add these HitKeep settings to the hitkeep service:
HITKEEP_CUSTOM_TRACKING_TLS_MODE: caddy-on-demandHITKEEP_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-hereUse a Caddyfile with persistent storage and a restricted on-demand TLS ask endpoint:
{email ops@example.comstorage 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:8080encode 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:
Use S3-compatible backups or archives
Section titled “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/archiveHITKEEP_S3_REGION=eu-central-1# Static credentials are optional when your container runtime provides an AWS credential chain.HITKEEP_S3_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLEHITKEEP_S3_SECRET_ACCESS_KEY=change-meFor 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.
Troubleshooting
Section titled “Troubleshooting”The dashboard shows “An unexpected error occurred” during setup or sign-in. Work through these in order:
- 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 wrongHITKEEP_PUBLIC_URL, not at HitKeep. HITKEEP_PUBLIC_URLmust exactly match the address bar — scheme, host, and port. With anhttps://public URL, session cookies are marked secure and sign-in over plain HTTP silently fails. After changing.env, rundocker compose up -dagain; Compose only applies environment changes when it recreates the container.Setup has already been completed.means a user already exists in the database — typically ahitkeep_datavolume left over from an earlier attempt.docker compose down -vremoves the named volumes and gives you a clean start (this deletes all analytics data).- 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. - Need more detail? Add
HITKEEP_LOG_LEVEL: debugunderenvironment:incompose.ymland rundocker compose up -dagain.
Related
Section titled “Related”- Trusted Proxies
- Custom Tracking Domains
- Configuration Reference
- Data Retention
- S3 Backups
- Binary Installation
Need managed hosting with explicit region choice? HitKeep Cloud → runs HitKeep in your chosen managed region: EU (Frankfurt) or US (Virginia).