Skip to content

Repository files navigation

FitPub Helm Chart

FitPub Helm Chart

Unofficial, independently maintained Helm chart for FitPub, a federated fitness tracking platform.
Not affiliated with the FitPub project or any hosted instance.

Artifact Hub Chart Version App Version License

Lint and Test Kind Runtime Test Release Kubernetes Helm

Status: unofficial and production-oriented. Review the values before pointing it at a public instance.

What CI checks

  • ct lint on every change
  • Render tests against default values, examples/production-values.yaml, and examples/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, runs helm 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.

Features

  • Runs as the non-root FitPub user (1001), restricted Pod Security Standard compliant out of the box
  • readOnlyRootFilesystem on by default, with /tmp and /app/logs backed 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/readiness and /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

Quick start (local)

Just want to see it run on kind/minikube/Docker Desktop without standing up a database first? From the repo root:

scripts/local-quickstart.sh

It 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.

Prerequisites

  • Kubernetes 1.26+
  • Helm 3.8+
  • External PostgreSQL with PostGIS enabled. A plain PostgreSQL database is not enough.

Installation

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.yaml

The 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.yaml

Either 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.yaml

The 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.

Configuration

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-secret

Create 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:

Extending The Pod

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.

Ingress

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.com

Set 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.

Markdown Pages

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.md
pages:
  existingSecret: fitpub-pages

Secret updates do not automatically restart the pod. Restart the Deployment if the application does not pick up changed files.

Persistence

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: 10Gi

You can also reuse an existing claim:

persistence:
  enabled: true
  existingClaim: fitpub-uploads

Back up the uploads PVC and the external PostGIS database regularly. It's important!

Application Logs

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: 512Mi

Replicas And Scaling

Default 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.

Memory And JVM

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: 3072Mi

OOMKilled 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"

NetworkPolicy

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 UnknownHostException on 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: 443

Adjust this to your actual PostgreSQL, DNS, SMTP, HTTPS federation and peer egress model.

Graceful Shutdown

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.

Debugging

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 -- sh

Disable it again when done:

helm upgrade fitpub fitpub/fitpub \
  --reuse-values \
  --set diagnosticMode.enabled=false

Global Labels And Annotations

Add labels and annotations to every resource created by this chart:

commonLabels:
  environment: production
  team: platform

commonAnnotations:
  reloader.stakater.com/auto: "true"

Monitoring

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: password

Without basicAuth, scrapes return HTTP 401 and the target stays empty. More in docs/troubleshooting.md.

Security Notes

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.

Pod Security Standards (restricted)

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: true

Upgrade

Use the same adapted values file you installed with (commonly copied from examples/production-values.yaml):

helm upgrade fitpub fitpub/fitpub -f production-values.yaml

When 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.

Design Notes

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.

GitOps

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.

Troubleshooting

See docs/troubleshooting.md for common Kubernetes deployment problems: PostGIS issues, missing secrets, failing health probes, PVC permissions and federation URL mistakes.

Production Checklist

A condensed version of the sections above:

  • PostgreSQL with PostGIS, not plain PostgreSQL
  • productionChecks.enabled=true so bad values fail at install rather than at runtime
  • Strong FITPUB_DATABASE_PASSWORD, FITPUB_JWT_SECRET, FITPUB_EMAIL_SECRET, FITPUB_ACTUATOR_PASSWORD (generate with openssl rand -base64 48)
  • FITPUB_ACTUATOR_PASSWORD is 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_URL public, canonical, no trailing slash
  • FitPub behind HTTPS
  • Back up PostgreSQL and /app/uploads (the parts you cannot regenerate)
  • Ship /app/logs somewhere if you want history - emptyDir does not survive rescheduling
  • FITPUB_PUSH_ENABLED=false unless VAPID keys and FITPUB_VAPID_SUBJECT are set
  • Scraping metrics? Set serviceMonitor.basicAuth against the actuator credentials - /actuator/prometheus is behind basic auth on 1.2.0
  • Want more than one replica? Switch uploads to ReadWriteMany first

This chart grew out of the Kubernetes manifests discussion in FitPub issue #301.

Contributing

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.

About

(Unofficial WIP) Helm chart for Fitpub — federated fitness tracking platform https://codeberg.org/fitpub/fitpub

Topics

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages