docker-compose.yml starts Postgres and the service together with a single command, and the built-in healthcheck ensures the application never starts before the database is ready. The compose file is designed for local trials and proof-of-concept work — if you want a production-grade deployment with rolling updates, resource limits, and network policies, use the Helm chart instead. If you do choose to run Compose in production, this page covers the minimum changes required to do so safely.
Production checklist
Before you expose MeetCost to real users, verify each of the following. The first five are enforced at startup — MeetCost refuses to serve traffic if any of them are wrong.1
Set MEETCOST_ENVIRONMENT=production
Switches the service into production mode, which activates all startup guards and disables development-only commands such as
meetcost token and seed-demo.2
Supply MEETCOST_ENCRYPTION_KEY from your secret manager
Every pay figure is encrypted with this key using AES-256-GCM. Generate it with
meetcost genkey (see below), store it in your secret manager, and never commit it to git. Without the key, pay data is unreadable — losing it means losing the data.3
Change MEETCOST_SECRET_KEY from its default
Signs admin sessions and other internal tokens. Set it to a random 48-byte base64 string:
openssl rand -base64 48.4
Set MEETCOST_AUTH_MODE=oidc with MEETCOST_OIDC_ISSUER
Development auth (
dev mode) signs tokens locally and is rejected when environment=production. Configure OIDC against your identity provider — see the Identity guide for details.5
Ensure MEETCOST_ENFORCE_EGRESS_ALLOWLIST=true
This is the default. MeetCost allows outbound connections only to hosts derived from your configuration. Set it to
false only to debug, and never leave it off in production.6
Use Postgres, not SQLite
SQLite is the default for trials. Money columns are float-backed in SQLite, so long-running aggregates can drift by fractions of a cent. Postgres 14 or newer is required for production.
7
Terminate TLS in front of the service
MeetCost binds to
0.0.0.0:8000 and speaks plain HTTP. Put a reverse proxy (nginx, Caddy, Traefik) or a load balancer in front that handles TLS termination and redirects HTTP to HTTPS.8
Set MEETCOST_FALLBACK_ANNUAL_PAY to a real number
Anyone without a pay record is priced at this figure. Leaving it at zero means uncovered people cost nothing and every meeting estimate will be silently understated. MeetCost shows a warning on screen, but set a real blended rate.
9
Schedule digests outside the process
Do not rely on background threads for digest delivery. A thread that dies quietly produces missing reports that nobody notices. Use the
scheduled compose profile (or, better, Kubernetes CronJobs) to run meetcost digest as a separate process.10
Back up the database and encryption key — separately
Together they constitute a payroll file. Backed up to the same place, they offer no protection. Back up the database to one location and the encryption key to a different one (your secret manager). Test your restore procedure.
The docker-compose.yml
The bundled compose file defines two services:postgres and meetcost.
- Postgres is not published to the host. It uses
exposerather thanports, so it is only reachable within the compose network. Nothing outside the network can connect to the database directly. - Development mode is hardcoded.
MEETCOST_ENVIRONMENT: developmentandMEETCOST_AUTH_MODE: devare set directly in the compose file. To use this file for production you must override both. env_file: .envis loaded before the inlineenvironmentblock, so the inline values win. Put your secrets in.envand leave the compose file untouched.- The service binds only to loopback (
127.0.0.1:8000:8000). This means it is not accessible from other machines on your network without a reverse proxy.
The scheduled profile
A digest service is included but only starts if you opt in:
meetcost digest --period weekly on a loop with a 24-hour sleep. It is adequate for a trial. For production, run the digest as a proper CronJob so failures are visible.
Environment variables before first boot
Copy the example file and fill in the required values:docker compose up:
See the Configuration reference for the full list.
Generating the encryption key
The encryption key is a base64-encoded 32-byte value. Generate it with the built-in CLI command:.env file:
Quick start (local trial)
seed-demo is refused when MEETCOST_ENVIRONMENT=production. It exists only to populate the quickstart with realistic numbers.Switching to Postgres for production
The compose file already uses Postgres. The hardcoded passwordmeetcost-local-only must be changed before production use. Either:
- Update the password in both the
postgresservice environment and theMEETCOST_DATABASE_URL, or - Remove the bundled Postgres service and point
MEETCOST_DATABASE_URLat an external managed database.
TLS termination
MeetCost speaks plain HTTP on port 8000. For production, terminate TLS in front of the service. A minimal nginx snippet:Observability endpoints
The Docker image configures a
HEALTHCHECK against /readyz:
Upgrading
Pull the new image and restart:Uninstalling
meetcost-data persists by default. To remove it: