Skip to content

Liwan

Liwan is a lightweight, privacy-focused web analytics application with embedded DuckDB storage. The HelmForge chart deploys it as one Kubernetes workload with durable /data storage, explicit public URL wiring, and production-oriented security defaults.

Key Features

  • Official image - pinned ghcr.io/explodingcamera/liwan:1.5.0
  • Embedded DuckDB - no external database dependency
  • PVC-backed data - analytics and runtime assets stored under /data
  • Recreate rollout - avoids overlapping DuckDB writers during upgrades
  • Cookie-free tracking - small JavaScript snippet for tracked sites
  • Ingress support - host, path, class, annotations, and TLS
  • Gateway API support - optional HTTPRoute
  • Dual-stack ready Service - optional ipFamilyPolicy and ipFamilies
  • Hardened defaults - non-root user, dropped capabilities, RuntimeDefault seccomp, and no ServiceAccount token

Installation

helm repo add helmforge https://repo.helmforge.dev
helm repo update
helm install liwan helmforge/liwan

OCI registry:

helm install liwan oci://ghcr.io/helmforgedev/helm/liwan

Tracking Script

After deploying Liwan and creating a site in the UI, embed the generated tracking snippet on your website. A typical snippet looks like:

<script defer src="https://analytics.example.com/script.js" data-site-name="my-site"></script>

Set liwan.baseUrl to the same public URL used by browsers and tracking scripts. Consent requirements depend on your jurisdiction, site policy, and any additional data you collect around Liwan.

Deployment Examples

liwan:
  baseUrl: 'https://analytics.example.com'

persistence:
  enabled: true
  size: 5Gi

resources:
  requests:
    cpu: 25m
    memory: 64Mi
  limits:
    cpu: 250m
    memory: 256Mi

ingress:
  enabled: true
  ingressClassName: traefik
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
  hosts:
    - host: analytics.example.com
      paths:
        - path: /
          pathType: Prefix
  tls:
    - secretName: liwan-tls
      hosts:
        - analytics.example.com
liwan:
  baseUrl: 'https://analytics.example.com'

persistence:
  enabled: true
  size: 5Gi

gatewayAPI:
  enabled: true
  parentRefs:
    - name: shared-gateway
      namespace: gateway-system
      sectionName: https
  hostnames:
    - analytics.example.com
liwan:
  baseUrl: 'http://liwan.analytics.svc.cluster.local'

persistence:
  enabled: true
  size: 2Gi

Architecture

The chart renders a Deployment, Service, and PVC by default. Liwan listens on container port 9042; the Kubernetes Service exposes port 80.

The Deployment uses strategy.type: Recreate. DuckDB is embedded and single-writer oriented, so the chart avoids a rolling update that could briefly run two pods against the same data volume.

Set the public URL before installing trackers

Without liwan.baseUrl, generated script URLs can point at an internal or incomplete endpoint. Keep liwan.baseUrl, Ingress hosts, Gateway API hostnames, and TLS hosts aligned.

Configuration Reference

Core

Parameter Type Default Description
nameOverride string "" Override the chart name.
fullnameOverride string "" Override the full release name.
commonLabels object {} Extra labels added to all resources.

Image

Parameter Type Default Description
image.repository string ghcr.io/explodingcamera/liwan Liwan container image.
image.tag string "1.5.0" Image tag.
image.pullPolicy string IfNotPresent Image pull policy.
imagePullSecrets array [] Pull secrets for private registries.

Liwan

Parameter Type Default Description
liwan.port integer 9042 Application HTTP port inside the container.
liwan.baseUrl string "" Public base URL of the Liwan instance.
liwan.extraEnv array [] Extra environment variables for advanced upstream settings.

Persistence

Liwan stores analytics data and runtime assets in /data.

Parameter Type Default Description
persistence.enabled boolean true Enable a PVC for /data.
persistence.size string 2Gi PVC size.
persistence.storageClass string "" StorageClass for the PVC.
persistence.accessModes array ["ReadWriteOnce"] PVC access modes.
persistence.existingClaim string "" Use an existing PVC instead of creating one.

Service

Parameter Type Default Description
service.type string ClusterIP Kubernetes Service type.
service.port integer 80 Service port exposed to the cluster.
service.annotations object {} Annotations for the Service.
service.ipFamilyPolicy string/null null Service IP family policy.
service.ipFamilies array [] Ordered Service IP families.

Dual-stack example:

service:
  ipFamilyPolicy: PreferDualStack
  ipFamilies:
    - IPv4
    - IPv6

Ingress

Parameter Type Default Description
ingress.enabled boolean false Render an Ingress.
ingress.ingressClassName string traefik Ingress class name.
ingress.annotations object {} Ingress annotations.
ingress.hosts array [] Host and path rules.
ingress.tls array [] TLS configuration.

Gateway API

Use gatewayAPI.enabled to render a native Kubernetes HTTPRoute. This requires Gateway API CRDs and a compatible Gateway controller.

Parameter Type Default Description
gatewayAPI.enabled boolean false Render an HTTPRoute.
gatewayAPI.parentRefs array [] Parent Gateway references.
gatewayAPI.hostnames array [] HTTPRoute hostnames.
gatewayAPI.paths array [{ type: PathPrefix, value: "/" }] HTTPRoute path matches.
gatewayAPI.annotations object {} HTTPRoute annotations.

Security

Parameter Type Default Description
serviceAccount.create boolean false Create a dedicated ServiceAccount.
serviceAccount.name string "" Override the ServiceAccount name.
serviceAccount.annotations object {} ServiceAccount annotations.
serviceAccount.automountServiceAccountToken boolean false Mount a Kubernetes API token into the pod.
podSecurityContext.fsGroup integer 1000 Filesystem group for mounted data.
podSecurityContext.fsGroupChangePolicy string OnRootMismatch Avoid recursive ownership changes when possible.
podSecurityContext.seccompProfile.type string RuntimeDefault Use the runtime default seccomp profile.
securityContext.runAsUser integer 1000 Container user ID.
securityContext.runAsGroup integer 1000 Container group ID.
securityContext.runAsNonRoot boolean true Enforce non-root execution.
securityContext.allowPrivilegeEscalation boolean false Prevent privilege escalation.
securityContext.capabilities.drop array ["ALL"] Drop Linux capabilities.

Scheduling and Extensibility

Parameter Type Default Description
resources object {} CPU and memory requests and limits.
nodeSelector object {} Node selector for scheduling.
tolerations array [] Tolerations for scheduling.
affinity object {} Affinity rules.
topologySpreadConstraints array [] Topology spread constraints.
priorityClassName string "" PriorityClass for the pod.
terminationGracePeriodSeconds integer 30 Termination grace period.
podLabels object {} Extra labels for the pod.
podAnnotations object {} Extra annotations for the pod.
extraVolumes array [] Extra volumes to attach to the pod.
extraVolumeMounts array [] Extra volume mounts for the container.
extraManifests array [] Additional Kubernetes manifests rendered with the release.

Operational Patterns

Liwan is intentionally single-instance. The chart does not expose replicaCount because embedded DuckDB is not a shared multi-writer database. Run separate releases for separate environments or sites when isolation is required.

Back up the PVC mounted at /data. For the most consistent snapshot, pause writes or scale the Deployment to zero before taking a storage-level snapshot.

For upgrades, the Recreate strategy causes a short outage while the old pod exits and the new pod starts. That is intentional and avoids overlapping writers on the same DuckDB files.

Common Issues

All analytics data is lost when persistence is disabled

If persistence.enabled is false, Liwan data is stored on emptyDir and is lost when the pod restarts. Keep persistence enabled in production.

Right-size the PVC from the start

DuckDB compresses analytics data efficiently, but retention and traffic vary. Start at 5Gi for public sites and use a StorageClass that supports expansion if long retention is required.

Security Scan

Framework Score
Overall 84.85%
MITRE 100.00%
NSA 80.00%
SOC2 80.00%

The hardening defaults cover non-root execution, dropped Linux capabilities, RuntimeDefault seccomp, and disabled ServiceAccount token automount. Remaining environment-specific findings are resource limits, network policy, and immutable root filesystem.

More Information