Unofficial, independently maintained Helm chart for FitPub, a federated fitness tracking platform.
Not affiliated with the FitPub project or any hosted instance.
Status: unofficial and production-oriented. Review the values before pointing it at a public instance.
ct linton every change- Render tests against default values,
examples/production-values.yaml, andexamples/networkpolicy-smoke-values.yaml - Kubernetes API validation in kind with
kubectl apply --dry-run=server - Kind runtime test (the Kind Runtime Test badge): on every PR, every push to
main, and once a week. Spins up kind + PostGIS, runshelm install --wait, waits for the pod to go Ready, and checks the authenticated actuator readiness endpoint. A second job does the same under restricted NetworkPolicy egress. - Releases publish to GitHub Pages and the GHCR OCI registry, GPG-signed with a cosign signature, plus a GitHub Release per version
The chart is linted, rendered, and booted against a real PostGIS database in CI. That is not a substitute for production load, but it catches most regressions before release.
- Runs as the non-root FitPub user (
1001), restricted Pod Security Standard compliant out of the box readOnlyRootFilesystemon by default, with/tmpand/app/logsbacked by emptyDir- PersistentVolumeClaim for uploads at
/app/uploads - Optional Secret mount for Markdown legal/about pages at
/app/pages - ConfigMap/Secret split for plain and secret environment variables
- Probes hit the authenticated actuator health groups (
/actuator/health/readinessand/actuator/health/liveness) via an exec wget that reads the actuator password from the pod env, see docs/troubleshooting.md - CPU/memory limits, a PodDisruptionBudget, and a preStop drain hook on by default
- Optional Ingress, HPA, NetworkPolicy and ServiceMonitor
- Extension points for extra env, envFrom, volumes, mounts, init containers and sidecars
- Brings its own app, but not its database - you supply PostgreSQL with PostGIS
Just want to see it run on kind/minikube/Docker Desktop without standing up a database first? From the repo root:
scripts/local-quickstart.shIt deploys a disposable PostGIS, installs the chart, waits for it to become healthy, and prints how to reach it. The manual steps and a troubleshooting table are in docs/quickstart.md.
- Kubernetes 1.26+
- Helm 3.8+
- External PostgreSQL with PostGIS enabled. A plain PostgreSQL database is not enough.
The chart ships two ways. Pick one.
OCI registry (recommended). No helm repo add, just point at the package:
helm install fitpub oci://ghcr.io/oliinykdm/charts/fitpub --version 0.5.1 -f production-values.yamlThe OCI artifact carries the GPG provenance, so you can verify it on pull:
helm pull oci://ghcr.io/oliinykdm/charts/fitpub --version 0.5.1 --verify \
--keyring <(curl -fsSL https://oliinykdm.github.io/fitpub-helm/pgp-public-key.asc | gpg --dearmor)Classic HTTP repo (GitHub Pages). Still published for tooling that expects it:
helm repo add fitpub https://oliinykdm.github.io/fitpub-helm
helm repo update
helm install fitpub fitpub/fitpub -f production-values.yamlEither way, copy examples/production-values.yaml and adapt it first.
Or install straight from a checkout while hacking on the chart:
git clone https://github.com/oliinykdm/fitpub-helm.git
cd fitpub-helm
helm install fitpub ./charts/fitpub -f examples/production-values.yamlThe repository install still expects an external PostGIS database and a pre-created
Secret when using examples/production-values.yaml. For a working local instance
without manual wiring, use scripts/local-quickstart.sh instead.
Non-secret settings go into config and are rendered into a ConfigMap. Secrets go into applicationSecret.data or, preferably for production, into an existing Kubernetes Secret referenced by applicationSecret.existingSecret.
Minimum production values (use an external Secret - do not commit real credentials to Git):
productionChecks:
enabled: true
config:
FITPUB_DATABASE_URL: "jdbc:postgresql://postgres:5432/fitpub"
FITPUB_DOMAIN: "your-domain.com"
# Must not end with a slash.
FITPUB_BASE_URL: "https://your-domain.com"
FITPUB_PUSH_ENABLED: "false"
# When enabling mail, set FITPUB_MAIL_HOST together with port/auth/starttls - see production-values.yaml.
FITPUB_MAIL_HOST: "smtp.example.com"
FITPUB_MAIL_PORT: "587"
FITPUB_MAIL_SMTP_AUTH: "true"
FITPUB_MAIL_STARTTLS_ENABLE: "true"
FITPUB_MAIL_STARTTLS_REQUIRED: "true"
applicationSecret:
existingSecret: fitpub-secretCreate the Secret before install (inline applicationSecret.data is for controlled testing only - see examples/chart-managed-secret-values.yaml):
kubectl create secret generic fitpub-secret -n fitpub \
--from-literal=FITPUB_DATABASE_USERNAME=fitpub \
--from-literal=FITPUB_DATABASE_PASSWORD="$(openssl rand -base64 32)" \
--from-literal=FITPUB_JWT_SECRET="$(openssl rand -base64 64)" \
--from-literal=FITPUB_EMAIL_SECRET="$(openssl rand -base64 64)" \
--from-literal=FITPUB_ACTUATOR_PASSWORD="$(openssl rand -base64 48)" \
--from-literal=FITPUB_MAIL_USERNAME="smtp-user" \
--from-literal=FITPUB_MAIL_PASSWORD="smtp-password"FITPUB_ACTUATOR_PASSWORD is required on FitPub 1.2.0: actuator endpoints are behind basic auth and the prod profile has no default password, so the pod will not start without it.
The mail user/password are only there because the example sets FITPUB_MAIL_SMTP_AUTH: "true".
productionChecks.enabled=true fails the render early when a required public setting, a chart-managed secret, or part of the push-notification config is missing, instead of surfacing later as a CrashLoopBackOff.
config only carries the keys the chart sets a value for. FitPub reads many more (Hikari pool, ActivityPub inbox tuning, mail, FITPUB_OSM_TILES_ENABLED, FITPUB_WEATHER_ENABLED, log rotation) and applies its own defaults. Set any of those by adding the key to config or via extraEnv; the full list lives in the FitPub source's application-prod.yml.
See values.yaml and examples/production-values.yaml for available options.
Additional examples:
- examples/production-values.yaml: production-style values with an externally managed Secret
- examples/chart-managed-secret-values.yaml: controlled testing values where Helm creates the Secret
- examples/development-values.yaml: development-style values for throwaway clusters
- examples/runtime-smoke-values.yaml: CI-only values used by the runtime smoke test
- examples/networkpolicy-smoke-values.yaml: CI-only values for restricted NetworkPolicy egress
The chart exposes common extension points without editing templates:
extraEnv:
- name: JAVA_TOOL_OPTIONS
value: "-XX:MaxRAMPercentage=70"
extraEnvFrom: []
extraInitContainers: []
sidecars: []
volumes: []
volumeMounts: []
nodeSelector: {}
tolerations: []
affinity: {}
topologySpreadConstraints: []Use these for platform integrations such as sidecar agents, projected Secrets, custom CA bundles or cluster scheduling rules.
Enable Ingress when exposing FitPub publicly:
ingress:
enabled: true
className: traefik
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
hosts:
- host: fitpub.example.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: fitpub-tls
hosts:
- fitpub.example.comSet className to the ingress controller used by your cluster. The example uses Traefik as a common self-hosted default.
FitPub's production profile enables server.forward-headers-strategy: framework and reads X-Forwarded-For and X-Forwarded-Proto from your ingress or reverse proxy. Configure the controller to pass those headers on HTTPS termination so generated URLs, redirects and ActivityPub endpoints use the correct public scheme.
FitPub can read Markdown pages from /app/pages. To provide Kubernetes-native legal pages, create a Secret from files and mount it:
kubectl create secret generic fitpub-pages \
--from-file=terms.md \
--from-file=imprint.md \
--from-file=about.mdpages:
existingSecret: fitpub-pagesSecret updates do not automatically restart the pod. Restart the Deployment if the application does not pick up changed files.
By default, the chart creates a PersistentVolumeClaim for user uploads at /app/uploads.
You can customize storage size and class:
persistence:
enabled: true
storageClass: ""
size: 10GiYou can also reuse an existing claim:
persistence:
enabled: true
existingClaim: fitpub-uploadsBack up the uploads PVC and the external PostGIS database regularly. It's important!
In the prod profile FitPub writes rotated file logs to /app/logs/. The chart
mounts that path as an emptyDir, so the read-only root filesystem is preserved and
logs survive container restarts - but not pod rescheduling, since emptyDir is tied
to the pod lifecycle.
For anything you want to keep, lean on your cluster log collector (stdout carries
the same lines) or ship /app/logs with a sidecar. To grow or shrink the buffer:
ephemeralVolumes:
logs:
sizeLimit: 512MiDefault is replicaCount: 1 with a Recreate strategy. Not because the app falls
over with more pods - the background schedulers claim work with row locks and the
cleanups are idempotent, so they are safe to run concurrently. The real blocker is
the uploads volume: ReadWriteOnce can only mount on one pod at a time.
To scale out, give uploads a ReadWriteMany storage class and then enable
autoscaling. The chart refuses replicaCount > 1 on RWO storage, so the
misconfiguration fails at install time rather than at runtime.
The image runs Java 25 with -XX:MaxRAMPercentage=75 against the cgroup memory
limit. The chart pins request and limit to the same value so the scheduler
reserves enough for heap plus native overhead, and adds a CPU limit to keep GC and
Flyway bursts from stealing a whole core:
resources:
requests:
cpu: 250m
memory: 3072Mi
limits:
cpu: 1500m
memory: 3072MiOOMKilled pods or sluggish Flyway on tiny nodes? Raise resources.limits.memory
or hand the JVM a smaller slice:
extraEnv:
- name: JAVA_TOOL_OPTIONS
value: "-XX:MaxRAMPercentage=60"Off by default. FitPub needs egress to PostgreSQL, SMTP and a long tail of federated/external HTTP hosts, so a half-configured policy mostly just breaks things. Turn it on once you are ready to list those dependencies.
Two things worth knowing before you enable it:
- It assumes a conntrack-stateful engine (Calico, Cilium, etc.). A non-stateful one can drop reply traffic such as DNS answers.
- Some enforcers restrict DNS even under an allow-all egress rule. kindnet (recent
builds) drops UDP 53 to the cluster DNS this way, which surfaces as
UnknownHostExceptionon the database host. If DNS breaks, allow it explicitly - to kube-system on UDP/TCP 53 - and confirm egress works on your CNI first.
Keep networkPolicy.ingress.enabled=true unless you add
networkPolicy.ingress.extraRules. Turning ingress off with no rules locks
everyone out, FitPub included.
Example shape for restricted egress:
networkPolicy:
enabled: true
egress:
allowAll: false
extraRules:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: database
ports:
- protocol: TCP
port: 5432
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
# ClusterIP DNS (some CNIs match policy before DNAT) - UDP and TCP 53.
- to:
- ipBlock:
cidr: 10.96.0.0/12
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
# SMTP to your mail relay (restrict `to:` in production).
- ports:
- protocol: TCP
port: 587
# HTTPS for federation.
- ports:
- protocol: TCP
port: 443Adjust this to your actual PostgreSQL, DNS, SMTP, HTTPS federation and peer egress model.
Kubernetes sends SIGTERM and yanks the pod from endpoints at the same moment, which
can leave a few in-flight requests hitting a dying pod. The chart already ships a
5-second preStop sleep to cover that window. Bump it if your ingress drains slowly:
lifecycleHooks:
preStop:
exec:
command: ["sh", "-c", "sleep 10"]Scheduling knobs (priorityClassName, nodeSelector, tolerations, affinity, topologySpreadConstraints) pass straight through - see values.yaml.
Enable diagnosticMode to run the container as sleep infinity so you can exec in without startup or probe failures blocking access:
helm upgrade fitpub fitpub/fitpub \
--reuse-values \
--set diagnosticMode.enabled=true
kubectl exec -it deployment/fitpub -n fitpub -- shDisable it again when done:
helm upgrade fitpub fitpub/fitpub \
--reuse-values \
--set diagnosticMode.enabled=falseAdd labels and annotations to every resource created by this chart:
commonLabels:
environment: production
team: platform
commonAnnotations:
reloader.stakater.com/auto: "true"Running Prometheus Operator? There is a ServiceMonitor. FitPub 1.2.0 ships
micrometer-registry-prometheus and serves the exposition format at
/actuator/prometheus, so scraping works - but every actuator endpoint is behind
HTTP basic auth, so you must supply credentials. Create a Secret in the
ServiceMonitor namespace and reference it via serviceMonitor.basicAuth:
kubectl create secret generic fitpub-actuator-auth \
--from-literal=username=actuator \
--from-literal=password="$FITPUB_ACTUATOR_PASSWORD"serviceMonitor:
enabled: true
labels:
release: kube-prometheus-stack
basicAuth:
username:
name: fitpub-actuator-auth
key: username
password:
name: fitpub-actuator-auth
key: passwordWithout basicAuth, scrapes return HTTP 401 and the target stays empty. More in
docs/troubleshooting.md.
The chart drops all Linux capabilities, disables privilege escalation, runs as
UID/GID 1001, and mounts the root filesystem read-only. FitPub still needs to
write uploads, logs and temp files, so those paths get their own mounts: the
uploads PVC, plus emptyDir for /tmp and /app/logs. Everything else is locked.
It passes restricted as shipped, no extra knobs required. The volume-permissions
init container is the one thing that would break it - it runs as root to chown the
uploads volume - so it is off by default. fsGroup handles ownership without it.
Only turn it back on for storage that ignores fsGroup (some NFS or hostPath setups):
initContainers:
volumePermissions:
enabled: trueUse the same adapted values file you installed with (commonly copied from examples/production-values.yaml):
helm upgrade fitpub fitpub/fitpub -f production-values.yamlWhen changing ConfigMap or chart-managed Secret values, the Deployment rolls automatically because checksum annotations are included on the pod template.
Read docs/upgrade-notes.md before upgrading across chart minor versions.
This chart intentionally uses an external PostGIS database, a Deployment with a dedicated uploads PVC and a single-replica default. See docs/design.md for the reasoning and current CI guarantees.
Flux and Argo CD examples are available in docs/gitops.md. Production GitOps setups should manage secrets through SOPS, External Secrets Operator, Sealed Secrets or a similar workflow.
See docs/troubleshooting.md for common Kubernetes deployment problems: PostGIS issues, missing secrets, failing health probes, PVC permissions and federation URL mistakes.
A condensed version of the sections above:
- PostgreSQL with PostGIS, not plain PostgreSQL
productionChecks.enabled=trueso bad values fail at install rather than at runtime- Strong
FITPUB_DATABASE_PASSWORD,FITPUB_JWT_SECRET,FITPUB_EMAIL_SECRET,FITPUB_ACTUATOR_PASSWORD(generate withopenssl rand -base64 48) FITPUB_ACTUATOR_PASSWORDis required on FitPub 1.2.0 - the pod CrashLoops without it- Using
applicationSecret.existingSecret? Confirm it has every required key before install - the chart cannot inspect its contents FITPUB_BASE_URLpublic, canonical, no trailing slash- FitPub behind HTTPS
- Back up PostgreSQL and
/app/uploads(the parts you cannot regenerate) - Ship
/app/logssomewhere if you want history - emptyDir does not survive rescheduling FITPUB_PUSH_ENABLED=falseunless VAPID keys andFITPUB_VAPID_SUBJECTare set- Scraping metrics? Set
serviceMonitor.basicAuthagainst the actuator credentials -/actuator/prometheusis behind basic auth on 1.2.0 - Want more than one replica? Switch uploads to
ReadWriteManyfirst
This chart grew out of the Kubernetes manifests discussion in FitPub issue #301.
For now this lives in a personal repo. If it settles into something stable, it can be proposed upstream to FitPub on Codeberg. See CONTRIBUTING.md for the workflow and design principles.
