Ultra-lightweight, privacy-first web analytics with embedded DuckDB storage. Liwan runs as a single binary with
minimal resource requirements and no external database dependency. All analytics data — including GeoIP
location enrichment — lives in a local DuckDB file on a persistent volume.
Key Features
Single binary — no external database, no sidecar, minimal footprint
Privacy-first — no cookies, no cross-site tracking, GDPR-compliant by design
DuckDB storage — columnar analytics engine embedded in the binary
GeoIP enrichment — automatic IP-to-location lookup stored alongside DuckDB data
Cookie-free tracking — JavaScript snippet embeddable on any site
Ingress support — TLS via cert-manager with configurable ingress class
Gateway API support — optional HTTPRoute for native Kubernetes routing
Dual-stack ready Service — optional ipFamilyPolicy and ipFamilies
Non-root by default — runAsNonRoot: true and fsGroup: 1000 preconfigured
After deploying Liwan and creating a site in the UI, embed the tracking snippet on your website. Replace
analytics.example.com with your actual Liwan instance URL and my-site with your site slug:
The script is cookie-free and GDPR-compliant. No consent banner is required under most EU interpretations
when using Liwan’s privacy-preserving tracking model.
# values.yaml — Liwan with GeoIP enrichment and explicit resource limits# GeoIP data is automatically downloaded to /data on first startup.# A larger PVC is recommended when GeoIP enrichment is active.liwan: baseUrl: 'https://analytics.example.com'persistence: enabled: true size: 5Gi # GeoIP database adds ~100MB to the DuckDB data directory storageClass: fast-ssdresources: requests: memory: 128Mi cpu: 50m limits: memory: 512Mi cpu: 500mingress: 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
# values.yaml — Minimal Liwan, internal access only (no Ingress)liwan: baseUrl: 'http://liwan.analytics.svc.cluster.local'persistence: enabled: true size: 2Gi
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 Configuration
Parameter
Type
Default
Description
liwan.port
integer
9042
Application HTTP port inside the container.
liwan.baseUrl
string
""
Public base URL of the Liwan instance (e.g. https://analytics.example.com).
liwan.extraEnv
array
[]
Extra environment variables for advanced configuration.
baseUrl is required for correct tracking script URLs
Set liwan.baseUrl to your public URL. Without it, the JavaScript tracking snippet URL generated by Liwan will be
incorrect, causing the tracker to fail on embedded sites. This is the most common reason for a Liwan deployment that
appears to work but collects no data.
Persistence
Liwan stores both the DuckDB analytics database and the GeoIP enrichment database in the /data directory.
GeoIP data is downloaded automatically on first startup and adds approximately 100 MB to the volume.
Parameter
Type
Default
Description
persistence.enabled
boolean
true
Enable a PVC for /data (DuckDB database + GeoIP data).
Use gatewayAPI.enabled to render a native Kubernetes HTTPRoute for Liwan. This requires Gateway API CRDs and a
compatible Gateway controller in the cluster.
Liwan ships with a non-root security context preconfigured. The fsGroup: 1000 ensures the DuckDB and GeoIP
files in /data are writable by the container user without requiring privileged access.
Parameter
Type
Default
Description
resources
object
{}
CPU and memory requests and limits.
podSecurityContext.fsGroup
integer
1000
Filesystem group for the pod.
securityContext.runAsUser
integer
1000
UID for the container process.
securityContext.runAsGroup
integer
1000
GID for the container process.
securityContext.runAsNonRoot
boolean
true
Enforce non-root execution.
Service Account
Parameter
Type
Default
Description
serviceAccount.create
boolean
false
Create a dedicated ServiceAccount.
serviceAccount.name
string
""
Override the ServiceAccount name.
serviceAccount.annotations
object
{}
Annotations for the ServiceAccount.
Scheduling
Parameter
Type
Default
Description
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.
Extra
Parameter
Type
Default
Description
extraVolumes
array
[]
Extra volumes to attach to the pod.
extraVolumeMounts
array
[]
Extra volume mounts for the container.
extraManifests
array
[]
Extra Kubernetes manifests deployed alongside the chart.
Common Issues
All analytics data lost when persistence.enabled: false
If persistence.enabled is false, all DuckDB data — including historical analytics and GeoIP files — is lost when
the pod restarts. Always enable persistence in production.
Right-size your PVC from the start
DuckDB compresses analytics data efficiently. For most small-to-medium sites, 2Gi is sufficient for years of
analytics. Add ~100 MB for the GeoIP database. PVC resize requires a StorageClass with allowVolumeExpansion: true.
Start at 5Gi for peace of mind.