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
kubectlconfigured 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 The chart references this secret by name (
ConfigMap: the encryption key, the session signing key, and the database URL. Create a Kubernetes secret to hold them:meetcost-secrets) and mounts the values as environment variables. It never templates a secret value into a ConfigMap.3
Install the chart
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
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 documentedvalues.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.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:NetworkPolicy: defence in depth
The chart ships aNetworkPolicy 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).
NetworkPolicy are independent controls:
- The application guard is a code-level promise: the service refuses to open connections to unconfigured hosts.
- The
NetworkPolicyis enforced by the cluster regardless of what the application code does.
- Add its hostname to
config.allowedEgressHostsso the application allows the connection. - Add it to
networkPolicy.extraEgressso the cluster allows the packet.
CronJobs
Scheduled work is deliberately run as KubernetesCronJob 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. ThecollectGoogle 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:PodDisruptionBudget with minAvailable: 1 ensures at least one pod remains available during cluster maintenance.