---
title: "Deploy HitKeep on Kubernetes with Helm | HitKeep"
description: "Deploy HitKeep on Kubernetes with the official Helm chart or a StatefulSet, keeping DuckDB data, QR assets, archives, and backups on persistent storage."
canonical: "https://hitkeep.com/guides/installation/kubernetes/"
---

# Deploy HitKeep on Kubernetes with Helm

Deploy HitKeep in your Kubernetes cluster with the official Helm chart when your cluster already uses Helm. The chart creates a StatefulSet, a ClusterIP Service, a headless Service for clustering, optional Ingress, and persistent storage for HitKeep’s DuckDB files and local assets.

Use the plain manifest later on this page if you do not use Helm or want every Kubernetes object visible in one file.

## Helm Chart

The chart is published as an OCI artifact on GitHub Container Registry:

```
helm show values oci://ghcr.io/pascalebeier/charts/hitkeep --version 2.13.12
```

Create a namespace and store the JWT signing secret before installing:

```
kubectl create namespace analytics
kubectl -n analytics create secret generic hitkeep-secrets \
  --from-literal=jwt-secret="$(openssl rand -hex 32)"
```

Save this as `hitkeep-values.yaml`:

```
domain: analytics.example.com

ingress:
  enabled: true
  className: ""

persistence:
  enabled: true
  size: 10Gi
  accessMode: ReadWriteOnce

env:
  HITKEEP_PUBLIC_URL: "https://analytics.example.com"

extraEnv:
  - name: HITKEEP_JWT_SECRET
    valueFrom:
      secretKeyRef:
        name: hitkeep-secrets
        key: jwt-secret
```

Install the chart:

```
helm install hitkeep oci://ghcr.io/pascalebeier/charts/hitkeep --namespace analytics --version 2.13.12 -f hitkeep-values.yaml

kubectl -n analytics rollout status statefulset/hitkeep
```

If your cluster does not have an ingress controller, leave `ingress.enabled` as `false` and expose the Service through your normal cluster pattern.

## Chart Values To Review

| Value | Default | Use |
| --- | --- | --- |
| image.repository | ghcr.io/pascalebeier/hitkeep | Container image repository. |
| image.tag | chart app version, without a leading v | Override only when you intentionally run a different app image from the chart version. |
| env.HITKEEP_PUBLIC_URL | http://localhost:8080 | Browser-visible URL where HitKeep is reached. Set this for production. |
| extraEnv | [] | Secret-backed values such as HITKEEP_JWT_SECRET, SMTP credentials, S3 credentials, or AI provider tokens. |
| persistence.enabled | true | Creates a StatefulSet volume claim template for HitKeep’s data directory. |
| persistence.mountPath | /var/lib/hitkeep/data | Persistent mount used by the chart defaults below. |
| ingress.enabled | false | Creates an Ingress for your cluster ingress controller. |
| customTrackingDomains.enabled | false | Configures HitKeep custom tracking domain runtime settings. |
| customTrackingDomains.ingress.enabled | false | Creates a separate tracking-only Ingress for static tracker hostnames. |
| service.type | ClusterIP | Use LoadBalancer or NodePort only when that matches your cluster. |
| replicaCount | 1 | Set to 2 or more only when you want HitKeep clustering. |

The chart sets these paths unless you override them in `env`:

| Environment variable | Chart default |
| --- | --- |
| HITKEEP_DB_PATH | /var/lib/hitkeep/data/hitkeep.db |
| HITKEEP_DATA_PATH | /var/lib/hitkeep/data |
| HITKEEP_ARCHIVE_PATH | /var/lib/hitkeep/data/archive |
| HITKEEP_SPAM_FILTER_PATH | /var/lib/hitkeep/data/spam-filter.json |

With those defaults, the same PVC stores the shared DuckDB database, tenant DuckDB databases, QR Code graphic assets under `assets/qr-codes`, archive files, and the optional local spam-filter cache.

## Custom Tracking Domains

The Helm chart supports [Custom Tracking Domains](https://hitkeep.com/guides/tracking/custom-tracking-domains/) without bundling an ingress controller. Use the same dashboard flow as other self-hosted installs: add the domain in Team Settings, publish the TXT ownership record, point DNS at the ingress target, and verify once TLS is ready.

For normal Kubernetes ingress controllers, use external TLS mode:

```
customTrackingDomains:
  enabled: true
  tlsMode: external
  # Empty defaults to the host from env.HITKEEP_PUBLIC_URL.
  # Set this when tracker domains point at a separate ingress hostname or IP.
  dnsTarget: ""
  ingress:
    enabled: true
    className: nginx
    annotations:
      cert-manager.io/cluster-issuer: letsencrypt-prod
    hosts:
      - host: tracker.customer-one.example
      - host: tracker.customer-two.example
    tls:
      - hosts:
          - tracker.customer-one.example
          - tracker.customer-two.example
        secretName: hitkeep-tracking-tls
```

The chart emits a separate tracking Ingress named after the Helm release, for example `hitkeep-tracking`, that routes only `/hk.js`, `/hk-vitals.js`, `/ingest`, `/ingest/event`, and `/ingest/web-vitals` to HitKeep. Dashboard, API, auth, share, MCP, QR, and SPA fallback routes are not part of that Ingress. HitKeep also enforces the tracking-only host boundary internally.

For Caddy on-demand TLS, deploy Caddy separately and keep the ask token in a Kubernetes Secret:

```
kubectl -n analytics create secret generic hitkeep-caddy-ask \
  --from-literal=token="$(openssl rand -hex 32)"
```

```
customTrackingDomains:
  enabled: true
  tlsMode: caddy-on-demand
  caddyAskToken:
    existingSecret: hitkeep-caddy-ask
    existingSecretKey: token
  ingress:
    enabled: false
```

Configure the external Caddy listener with `ask http://hitkeep.analytics.svc.cluster.local/internal/caddy/on-demand-tls/<token>`. Caddy sends `?domain=` to that URL before issuing a certificate, and HitKeep allows only enabled DNS-verified custom tracking domains.

## Upgrade With Helm

Keep your `hitkeep-values.yaml` under source control or in your deployment system. Upgrade the chart and app image together by changing only the chart version:

```
helm upgrade hitkeep oci://ghcr.io/pascalebeier/charts/hitkeep --namespace analytics --version 2.13.12 -f hitkeep-values.yaml

kubectl -n analytics rollout status statefulset/hitkeep
```

Review release notes before upgrading across feature releases, especially when you use optional integrations such as MCP, AI model configuration, Google Search Console, SMTP, or S3 backups.

## Optional MCP And AI Configuration

MCP and AI-backed product features are disabled until you enable and configure them. Use `env` for non-secret settings and `extraEnv` for tokens.

```
env:
  HITKEEP_MCP_ENABLED: "true"
  HITKEEP_MCP_PATH: "/mcp"
  HITKEEP_MCP_MAX_RANGE_DAYS: "366"
  HITKEEP_AI_ENABLED: "true"
  HITKEEP_AI_PROVIDER: "openai-compatible"
  HITKEEP_AI_MODEL: "opportunities-json"

extraEnv:
  - name: HITKEEP_AI_API_KEY
    valueFrom:
      secretKeyRef:
        name: hitkeep-ai
        key: api-key
```

See [Official MCP Server](https://hitkeep.com/guides/integrations/mcp/) and [AI Model Configuration](https://hitkeep.com/guides/admin/ai-model-configuration/) before enabling these in production.

## Plain Kubernetes Manifest

Use this manifest if you do not use Helm. It keeps HitKeep’s runtime paths aligned with the official chart.

Save it as `hitkeep.yaml`:

```
apiVersion: v1
kind: Namespace
metadata:
name: analytics
---
apiVersion: v1
kind: Secret
metadata:
name: hitkeep-secrets
namespace: analytics
type: Opaque
stringData:
jwt-secret: "change-me-to-a-long-random-string"
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: hitkeep-data
namespace: analytics
spec:
accessModes: ["ReadWriteOnce"]
resources:
  requests:
    storage: 10Gi
---
apiVersion: v1
kind: Service
metadata:
name: hitkeep-headless
namespace: analytics
spec:
clusterIP: None
publishNotReadyAddresses: true
selector:
  app: hitkeep
ports:
  - name: gossip-tcp
    port: 7946
    protocol: TCP
    targetPort: gossip-tcp
  - name: gossip-udp
    port: 7946
    protocol: UDP
    targetPort: gossip-udp
---
apiVersion: v1
kind: Service
metadata:
name: hitkeep
namespace: analytics
spec:
type: ClusterIP
selector:
  app: hitkeep
ports:
  - name: http
    protocol: TCP
    port: 80
    targetPort: http
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: hitkeep
namespace: analytics
spec:
serviceName: hitkeep-headless
replicas: 1
selector:
  matchLabels:
    app: hitkeep
template:
  metadata:
    labels:
      app: hitkeep
  spec:
    securityContext:
      runAsNonRoot: true
      runAsUser: 65532
      runAsGroup: 65532
      fsGroup: 65532
    containers:
      - name: hitkeep
        image: ghcr.io/pascalebeier/hitkeep:2.13.12
        imagePullPolicy: IfNotPresent
        ports:
          - containerPort: 8080
            name: http
          - containerPort: 7946
            name: gossip-tcp
            protocol: TCP
          - containerPort: 7946
            name: gossip-udp
            protocol: UDP
        env:
          - name: HITKEEP_HTTP_ADDR
            value: ":8080"
          - name: HITKEEP_PUBLIC_URL
            value: "https://analytics.example.com"
          - name: HITKEEP_DB_PATH
            value: "/var/lib/hitkeep/data/hitkeep.db"
          - name: HITKEEP_DATA_PATH
            value: "/var/lib/hitkeep/data"
          - name: HITKEEP_ARCHIVE_PATH
            value: "/var/lib/hitkeep/data/archive"
          - name: HITKEEP_SPAM_FILTER_PATH
            value: "/var/lib/hitkeep/data/spam-filter.json"
          - name: HITKEEP_JWT_SECRET
            valueFrom:
              secretKeyRef:
                name: hitkeep-secrets
                key: jwt-secret
        volumeMounts:
          - mountPath: /var/lib/hitkeep/data
            name: data
        livenessProbe:
          httpGet:
            path: /healthz
            port: http
          initialDelaySeconds: 10
          periodSeconds: 30
        readinessProbe:
          httpGet:
            path: /readyz
            port: http
          initialDelaySeconds: 5
          periodSeconds: 10
    volumes:
      - name: data
        persistentVolumeClaim:
          claimName: hitkeep-data
```

Apply it:

```
kubectl apply -f hitkeep.yaml
kubectl -n analytics rollout status statefulset/hitkeep
```

Add your Ingress resource or external Service according to your cluster’s normal ingress controller or load-balancer setup.

## Health And Readiness Probes

HitKeep exposes two probe endpoints for Kubernetes:

| Endpoint | Purpose |
| --- | --- |
| GET /healthz | Liveness. The process is running. |
| GET /readyz | Readiness. The shared database and every currently open tenant database are healthy. |

These endpoints stay available at the local root even when `HITKEEP_PUBLIC_URL` includes a path prefix.

During database recovery, `/healthz` remains successful so the kubelet does not kill a process that is performing safe recovery work. `/readyz` returns `503`, `Retry-After: 5`, and a JSON reason such as `database_recovering` or `database_needs_attention`, which removes the pod from service until the database is healthy again.

## Trusted Proxies

If your cluster uses an ingress controller such as nginx-ingress, Traefik, or AWS ALB, configure trusted proxy CIDRs so real client IPs are used for analytics and rate limiting:

```
env:
  HITKEEP_TRUSTED_PROXIES: "10.0.0.0/8"
```

See [Trusted Proxies](https://hitkeep.com/guides/installation/trusted-proxies/) for details.

## Backups

Back up the full persistent data path. With the Helm chart defaults and the plain manifest above, the important runtime paths are:

- `/var/lib/hitkeep/data/hitkeep.db` for shared control-plane data
- `/var/lib/hitkeep/data/tenants/*/hitkeep.db` for non-default team analytics data
- `/var/lib/hitkeep/data/assets/qr-codes/*` for QR Code graphic assets
- `/var/lib/hitkeep/data/archive` for local archive artifacts
- `/var/lib/hitkeep/data/backups` if you set `HITKEEP_BACKUP_PATH` to that local path
- `/var/lib/hitkeep/data/recovery` for permission-restricted automatic-recovery bundles and resumable markers, unless you override `HITKEEP_DB_RECOVERY_PATH`

To enable local automatic backup snapshots on the same PVC:

```
env:
  HITKEEP_BACKUP_PATH: "/var/lib/hitkeep/data/backups"
  HITKEEP_BACKUP_INTERVAL: "60"
  HITKEEP_BACKUP_RETENTION: "24"
```

For off-cluster backup storage, use an `s3://` `HITKEEP_BACKUP_PATH` and configure the S3 environment variables in a Kubernetes Secret. See [S3 Backups](https://hitkeep.com/guides/data/s3-backups/) for examples.

## Related

- [Docker Compose Installation](https://hitkeep.com/guides/installation/docker-compose/)
- [Trusted Proxies](https://hitkeep.com/guides/installation/trusted-proxies/)
- [Backups and Restore](https://hitkeep.com/guides/data/backups-and-restore/)
- [S3 Backups](https://hitkeep.com/guides/data/s3-backups/)
- [Configuration Reference](https://hitkeep.com/reference/configuration/)
- [Architecture](https://hitkeep.com/reference/architecture/)

Running HitKeep in Kubernetes but do not want to manage StatefulSets, PVCs, and cluster upgrades? [HitKeep Cloud](https://hitkeep.com/pricing/) handles the infrastructure in your chosen managed region.

[Previous Docker Compose](https://hitkeep.com/guides/installation/docker-compose/)[Next Trusted proxies](https://hitkeep.com/guides/installation/trusted-proxies/)
