lifeos handbook
Operations

Env and secrets

Where configuration lives, and why a missing variable kills the app.

There is no root .env, ever

Configuration lives in exactly two places, each with a committed .env.example beside it (ADR-0018):

PathFor
tools/dev/env/*.envbackends, local development
apps/*/web/.envfrontends

A root .env gets loaded by whatever tool happens to look for one, which means the same variable name silently means different things to different processes.

Production infrastructure credentials live off the repository entirely, in ~/.lifeos-prod/, and are uploaded to Dokploy service environments by tools/deploy/infra/deploy.sh.

No :default on any placeholder

Every placeholder in application-platform.yaml is a bare ${VAR}. A missing variable kills the app at boot rather than letting it run degraded (ADR-0004).

The failure mode this prevents is specific: a defaulted mail host that silently drops every notification, or a defaulted database URL that points somewhere harmless while real writes go nowhere. Both look healthy. Neither is.

Fail loud, never silent

The same principle runs through everything operational here:

  • Expected-count checks that refuse a partial run rather than reporting success on half the work.
  • .partial → verify → mv, so an interrupted write never leaves a file that looks complete.
  • Test restores. A backup nobody has restored is a hypothesis.
  • A dead-man's-switch ping on success, so silence is the alarm. A job that stops running entirely produces no failure email — which is exactly why the absence of a success ping has to be what pages you.

Rotating a credential

When a secret is rotated, every place holding a copy has to move together. Today that means:

  • the GitHub Actions secret, where CI reads it;
  • ~/.lifeos-prod/*.env on the machine that runs the infra deploy script;
  • the Dokploy service environment for anything already running with the old value.

PROD_IAM_KEY_ENCRYPTION_KEY is the exception that cannot be rotated casually — changing it invalidates every token ever issued.

On this page