Skip to main content
The bundled Helm chart at deploy/helm/meetcost is the recommended way to run MeetCost in production. It is opinionated on the things that make MeetCost’s data-residency guarantee concrete: the egress NetworkPolicy is on by default, the pod runs as a non-root user with a read-only root filesystem, secrets are referenced from a Kubernetes secret rather than templated into a ConfigMap, and all scheduled work runs as CronJob resources rather than background threads. Two replicas and a PodDisruptionBudget are included out of the box.

Prerequisites

  • Kubernetes 1.26 or newer
  • Helm 3.12 or newer
  • An ingress controller (the default configuration expects nginx)
  • A Postgres 14+ database reachable from within the cluster
  • kubectl configured to point at your target cluster

Step-by-step installation

1

Create the namespace

2

Create the secret

MeetCost needs three values that must never appear in a ConfigMap: the encryption key, the session signing key, and the database URL. Create a Kubernetes secret to hold them:
The chart references this secret by name (meetcost-secrets) and mounts the values as environment variables. It never templates a secret value into a ConfigMap.
Replace PASSWORD with your actual Postgres password. The encryption key is generated fresh by the genkey command — it is printed to stdout and captured directly into the secret, so it is never written to disk or a shell history.
3

Install the chart

Replace ingress.host, config.oidcIssuer, config.oidcAudience, and config.bootstrapAdmins with values for your environment. See the Identity guide for the correct oidcIssuer for your identity provider.
4

Verify the deployment

Wait for both pods to show Running and 1/1 ready. The readiness probe checks /readyz which touches the database, so a pod that cannot reach Postgres will stay 0/1.

Key values.yaml settings

The chart ships with a fully documented values.yaml. The most important settings to review before going live:

ingress.host

The public hostname for MeetCost. TLS is enabled by default; the chart expects a secret named meetcost-tls in the same namespace, or you can override ingress.tls.secretName.

config.oidcIssuer

The OIDC issuer URL for your identity provider. MeetCost uses OIDC discovery to fetch JWKS automatically from <issuer>/.well-known/openid-configuration.

config.oidcAudience

The client ID (audience) that tokens must be issued for. For Google this is your OAuth app’s client ID; for Entra it is your application (client) ID.

config.bootstrapAdmins

Comma-separated email addresses granted the admin role at first boot, before any role rows exist in the database. After the first admin logs in, use meetcost grant or the admin UI to manage roles.
Other settings that commonly need attention:
See the Configuration reference for the complete list of settings and their environment variable equivalents.

Secrets from an external secret manager

If you use an external secret manager (AWS Secrets Manager, GCP Secret Manager, HashiCorp Vault), create the Kubernetes secret via your preferred operator (External Secrets Operator, Vault Agent Injector, etc.) and point the chart at it:
Provider credentials (Zoom client secret, HRIS API key, etc.) can be mounted as additional environment variables:

NetworkPolicy: defence in depth

The chart ships a NetworkPolicy that restricts egress at the cluster level to the same set of destinations the application allows itself. It is enabled by default (networkPolicy.enabled: true).
The application-level egress guard and the NetworkPolicy are independent controls:
  • The application guard is a code-level promise: the service refuses to open connections to unconfigured hosts.
  • The NetworkPolicy is enforced by the cluster regardless of what the application code does.
Keep both in place. If you use an HTTP proxy or an egress gateway:
  1. Add its hostname to config.allowedEgressHosts so the application allows the connection.
  2. Add it to networkPolicy.extraEgress so the cluster allows the packet.
If you add it to only one place, requests will be blocked by the other. For example:

CronJobs

Scheduled work is deliberately run as Kubernetes CronJob resources rather than background threads. A background thread that dies quietly produces a missing report nobody notices. The chart defines five CronJobs: Enable the ones you need:

Notes on attendance collection

Google Meet has no end-of-meeting webhook, so attendance is polled via the Reports API. The collectGoogle CronJob runs hourly with a three-hour lookback window (--hours 3). The overlapping window is intentional — the Reports API is eventually consistent, and ingest is idempotent on (provider, provider_meeting_id), so re-reading is free. Zoom and Microsoft deliver attendance via webhooks at POST /webhooks/zoom and POST /webhooks/microsoft. Both endpoints verify the provider’s signature before taking any action.

Resource limits and autoscaling

The default resource requests and limits:
Horizontal autoscaling is off by default. Enable it if you expect significant concurrent usage:
A PodDisruptionBudget with minAvailable: 1 ensures at least one pod remains available during cluster maintenance.

Upgrading

Back up the database before every upgrade, and ensure the encryption key backup is stored separately from the database dump.
If you need to change values at upgrade time:
Tables are updated on first boot after an upgrade if the schema has changed.

Uninstalling

The database is not deleted by helm uninstall. If you are fully decommissioning MeetCost, connect to your Postgres instance and drop the database manually. Then destroy the encryption key — without the key, pay data is already unreadable, and destroying the key is a belt-and-braces erasure step alongside dropping the tables.
To remove the namespace as well: