Module 2 · Lesson 5

Lifecycle Commands

Build, release, start, seed — plus running from source with install and dev.

What you'll learn
  • The six command types: build, release, start, seed, test, bootstrap
  • When each command runs in the deployment lifecycle
  • What happens when each command fails — and which failures stop a deploy
  • Timeouts and the duration grammar (30s, 5m, 2h)
  • Running from source with install / dev and launchfile dev
  • Where a provider gets the source: attached checkout vs. the repository origin
  • Command capture for post-start output
  • The build block for Dockerfile-based builds

The deployment lifecycle

Every deployment follows the same arc: compile the code, prepare the environment, then run the app. Launchfile gives you a hook for each stage via the commands block.

Here's the order, and when each command fires:

  1. build — Compile source, install dependencies, generate assets. Runs at build time.
  2. release — Runs once per deployment, before start. Migrations, asset uploads, cache warming. If it fails, the deploy stops.
  3. start — The long-running process. This is your app.
  4. seed — Populate the database with sample or default data. Typically run on demand, not on every deploy.
  5. test — Run the test suite. Used by CI and during deploys that validate before promoting.
  6. bootstrap — First-time setup. Create the admin user, generate invite links, initialize config. Runs on user request, not automatically.
release vs. bootstrap

Think of release as "every deploy" (migrations) and bootstrap as "first deploy ever" (admin setup). Both run against a live app, but release gates the deploy while bootstrap is fire-and-forget.

When a command fails

Every command has exactly one failure disposition, decided by the slot it fills rather than its name — because one command can fill a slot in either mode. Source mode resolves prepare as install ?? build and run asdev ?? start, so build and start each show up in two modes.

SlotFilled byOn failure
prepareartifact: build
source: install ?? build
Fails the invocation — your deploy if you're deploying, your session if you're running from source. Nothing to run either way.
releasereleaseFails the deploy. Runs after resources are ready and before the run slot, so a failed migration never serves traffic.
runartifact: start
source: dev ?? start
Fails the invocation — the component didn't come up.
bootstrapbootstrapReported to whoever invoked it — never affects deploy status.
on-demandseed, test, customReported to whoever invoked it — never affects deploy status.

"Fails the invocation" is what keeps this unambiguous. If you declare only start: — no dev, no image — it fills the run slot in source mode, and it's covered exactly once.

Commands run in a shell

A command string is handed to a POSIX shell, so &&, pipes, redirection and variable expansion all work the way you'd write them at a terminal:

commands:
  release: "rails db:migrate && rails db:seed"

A provider that can't offer a shell has to say the command went unhonored rather than guess at it — splitting on whitespace and running the first word would quietly execute half of what you wrote.

Timeouts and durations

Any command in the expanded form can declare a timeout:

1
A timeout caps how long the command may run. Without one, the provider applies its own documented default.
commands:
  release:
    command: "npx prisma migrate deploy"
    timeout: "5m"

Every duration in a Launchfile uses one grammar: an integer immediately followed by exactly one unit — ms, s, m, or h. Valid: 500ms, 30s, 5m, 2h. Not valid: 5 m (no spaces), 1m30s (no compounds), 1.5h (no fractions). The same grammar covers the health check durations (interval, timeout, start_period).

Timeouts follow the failure table

A timeout that expires is a failure of that stage: a release that overruns fails the deploy; a bootstrap that overruns is reported. And launchfile validate warns on any duration it can't parse — providers surface the error rather than silently substituting a default.

Build it up

1
The minimum: a start command — this is what launches your app.
commands:
  start: "node server.js"
2
Add build to compile and release to run migrations before every deploy.
commands:
  build: "npm install && npm run build"
  release: "npx prisma migrate deploy"
  start: "node dist/server.js"
3
Add seed for sample data and test for the test suite.
commands:
  build: "npm install && npm run build"
  release: "npx prisma migrate deploy"
  start: "node dist/server.js"
  seed: "node scripts/seed.js"
  test: "npm test"
4
The capture pattern extracts values from stdout via regex. Useful for apps that print a generated token or URL on first start.
commands:
  start:
    run: "my-app serve"
    capture:
      ADMIN_TOKEN: "Admin token: (.+)"

The build block

There's a subtle but important distinction: commands.build runs shell commands (like npm run build), while a top-level build: block configures a Dockerfile-based container build. They serve different purposes.

commands.build (shell)
commands:
  build: "npm install && npm run build"
  start: "node dist/server.js"
build: (Dockerfile)
build:
  dockerfile: ./Dockerfile

commands:
  start: "node server.js"
Don't confuse the two

commands.build runs inside an already-built container (or on the host). The top-level build: block describes how to build the container image itself from a Dockerfile. If you're using a prebuilt image (like image: node:20), you only need commands.build. If you have a Dockerfile, use the top-level build: block.

Running from source: install and dev

The commands above run the app as a built artifactbuild compiles it, start runs it. But on a dev machine you often want to run the app from source instead: hot reload, the repo's own dev server, no container. That's a different execution mode, and it varies exactly one thing — the command lines.

Two source-mode keys pair with the artifact ones:

  • install — source-mode prepare (the counterpart of build). Runs on demand: first launch, or when dependencies change.
  • dev — source-mode run (the counterpart of start). Your dev server.
launchfile up (artifact)
commands:
  build: "bun install && bun run build"
  start: "node dist/server.js"
launchfile dev (source)
source: ./apps/api        # working dir for install/dev
commands:
  install: "bun install"
  dev: "bun run dev"

launchfile up runs the artifact (buildstart); launchfile dev runs from source (installdev), in the optional source directory. Run precedence is dev > image > start: a prebuilt image stays an artifact unless dev overrides it. Only prepare and run are mode-aware — release, bootstrap, seed, and test are the same command in either mode.

Only declare what differs

An app whose dev command equals its production command needs nothing extra — a plain start works in both modes. Add dev / install only when running from source needs a different command line than the built artifact (a compiled binary vs. bun run dev, an image entrypoint vs. a repo script).

Where the source comes from

Locally, the source is the checkout your Launchfile sits in. A remote provider building from source has to acquire the tree first, and resolves it in order: a source the orchestrator hands it always wins (a fork, a mirror, a per-environment ref); otherwise the checkout the file was read from is shipped as the build context; otherwise — for a standalone file, like a catalog entry — it clones the repository field, the app's canonical origin.

On a git-hosted URL, a # fragment names the baseline ref the declaration describes — repository: https://github.com/hedgedoc/hedgedoc#develop is how an edge variant pins its baseline while the stable variant tracks a release. It's a default, never a lock: an orchestrator-supplied source always overrides it. A bare URL means the default branch.

In the wild

This spec example shows a real-world pattern: a post-start bootstrap command that creates an admin user and captures the one-time invite link from stdout.

my-app/LaunchfileView on GitHub
version: launch/v1
name: my-app

build:
  dockerfile: ./Dockerfile

provides:
  - protocol: http
    port: 9999
    exposed: true

env:
  APP_SECRET:
    generator: secret
    sensitive: true
  PUBLIC_URL:
    default: $app.url
    description: "Public URL the app is reachable on (auth callbacks, email links)"

health: /health
restart: always

commands:
  start: "node server.js"

  # Post-start setup: create the first admin and capture the one-time invite link.
  # Run by the user via `launchfile bootstrap my-app` (or the provider's equivalent)
  # after the app is up. Re-runnable; failures are reported, not deploy-failing.
  bootstrap:
    command: "my-app-cli create-invite --name admin --url $app.url"
    capture:
      invite_link:
        pattern: "https?://\\S+"
        description: "One-time invite link — open in a browser to register your account"
        sensitive: true
build:
Top-level build block — this app uses a Dockerfile, not a prebuilt image.
commands:
The commands block defines start and bootstrap. No release or build command needed since the Dockerfile handles compilation.
bootstrap:
Runs on user request after the app is up. Creates an admin invite and captures the URL from stdout.
capture:
Extracts the invite link via regex — it's captured and surfaced to the user.

Check your understanding

When does the release command run?
Key takeaways
  • The deployment lifecycle is: build, release, start. Seed, test, and bootstrap are on-demand.
  • Failure semantics: the prepare and run slots fail the invocation — your deploy when deploying, your session when running from source; release fails the deploy; bootstrap and on-demand commands are reported. Timeout expiry counts as a failure of its slot.
  • Durations are an integer plus one unit (30s, 5m) — for timeout and the health check timings. validate warns on anything else.
  • install / dev run the app from source (launchfile dev) — the source-mode pair of build / start. Only these two are mode-aware.
  • release runs once per deploy (migrations). bootstrap runs once ever (initial setup).
  • Commands can use the capture pattern to extract values from stdout via regex.
  • Top-level build: configures Docker image builds. commands.build runs shell commands inside a container.
esc
Type to search the docs