Skip to content

Lifecycle hooks ​

Hooks let you run commands at stack boundaries. Before any service starts, after the stack is ready, before tearing down. The most common use is after_up for migrations and seeds.

Top-level hooks ​

Three phases at the stack level:

yaml
lifecycle:
  before_up:                       # runs before any service starts
    - ./scripts/check-prereqs.sh
  after_up:                        # runs once all services are ready
    - psql "$DATABASE_URL" -f db/migrations/01_init.sql
    - cmd: ./scripts/seed.sh
      env_group: stack-plus-test   # optional — use a named env bundle
  before_down:                     # runs before any service stops
    - ./scripts/dump-state.sh

There's also after_down (runs AFTER all services have stopped), for external resource cleanup that's only safe once services are fully torn down.

Entries are either a string (the cmd) or an object: { cmd: ..., env_group?: ..., cwd?: ... }.

Per-service hooks ​

Each services.<name> and owned.<name> can also declare hooks scoped to that service:

yaml
owned:
  api:
    cmd: bun run dev
    lifecycle:
      before_start:                # runs before this service starts
        - ./scripts/api-prebuild.sh
      after_ready:                 # runs when this service's ready_when fires
        - ./scripts/api-warm-cache.sh
      before_down:                 # runs before this service stops
        - ./scripts/api-flush.sh

Per-service hooks run in the context of one service. Use them for service-specific setup, teardown that doesn't belong at the stack boundary, or to optimize the startup speed of your stack through parallelization.

Phase ordering ​

For a typical lich up:

  1. lifecycle.before_up (top-level)
  2. Compose services start
  3. Per-service lifecycle.before_start for owned services
  4. Owned services start (in depends_on order)
  5. Per-service lifecycle.after_ready as each service becomes ready
  6. lifecycle.after_up (top-level) runs once every service is ready

For lich down:

  1. lifecycle.before_down (top-level) services still alive
  2. Per-service lifecycle.before_down
  3. Services stop
  4. lifecycle.after_down (top-level) services fully torn down

Choosing after_up vs after_down vs oneshot ​

Use caseRight tool
Migration / seed (no teardown needed)lifecycle.after_up
Dump live-service state for debugginglifecycle.before_down
Clean up external resources (scratch dirs, sockets)lifecycle.after_down
External CLI that spawns side-effects you must tear down laterowned: { oneshot: true, stop_cmd: ... }. See Oneshot services

The trap: don't use lifecycle.before_up for something like supabase start or any other launcher that spawns long-lived side-effects. The spawned containers stay running after lich down, the second lich up collides on container names, and you end up with a graveyard of orphan stacks unless you build custom tooling to detect and tear them down. Use oneshot: + stop_cmd: instead. It's purpose designed to solve this problem.

Hook logs ​

Hook stderr is captured to <LICH_HOME>/stacks/<id>/hooks/<phase>-<idx>.log and surfaced inline on completion. Useful for debugging hooks that swallow errors with || true. The inline tail prints regardless of exit code, and the full combined stdout+stderr lives in the per-hook log file (rolling ~1 MB cap).

Path shape: before_up-0.log, after_down-2.log, etc. (phase + 0-based index within the composed entries array).

Profile lifecycle merge ​

When a profile declares its own lifecycle: block, it merges with the top-level. It does NOT replace. See Profiles → Profile lifecycle merge for the merge order and an example.

See the full lifecycle: section in the lich.yaml reference for the schema and edge cases.

Released under the MIT License.