Module 3 · Lesson 6

Multi-Component Apps

Wire a frontend and backend together with dependency ordering.

What you'll learn
  • Using the components map for multi-service apps
  • Inter-component dependency ordering with depends_on
  • Cross-component expressions like $components.backend.url
  • The exposed flag — only frontends need it

When to go multi-component

Everything we've built so far has been a single-component app — one runtime, one start command, one process. That covers most applications. But when your project has a separate frontend and backend (or background workers, or a websocket server), you need the components map.

Instead of a flat Launchfile, you nest each service under a key in components:. Each component gets its own runtime, commands, ports, and health checks. The provider launches them together and wires them up.

Components can depend on each other using depends_on — the provider ensures the backend is healthy before starting the frontend.

Build it up

Let's build HedgeDoc — a collaborative markdown editor with a Node backend and a separate frontend — step by step.

1
Start with a single backend component. Everything from flat Launchfiles works inside components: — it's the same fields, just nested.
name: hedgedoc

components:
  backend:
    runtime: node
    commands:
      start: "node dist/main.js"
2
Add provides to expose the API on port 3000, and requires to declare the Postgres dependency. Same fields you already know.
name: hedgedoc

components:
  backend:
    runtime: node
    provides:
      - name: api
        protocol: http
        port: 3000
    requires:
      - type: postgres
        set_env:
          HD_DATABASE_URL: $url
    commands:
      start: "node dist/main.js"
3
Add a health check so the provider knows when the backend is ready. The release command runs migrations before each deploy.
name: hedgedoc

components:
  backend:
    runtime: node
    provides:
      - name: api
        protocol: http
        port: 3000
    requires:
      - type: postgres
        set_env:
          HD_DATABASE_URL: $url
    commands:
      start: "node dist/main.js"
      release: "node dist/migrations.js"
    health:
      path: /api/private/config
      start_period: 30s
4
Add the frontend. It depends_on the backend (waits until healthy), exposes port 3001 to the internet, and uses $components.backend.url to discover the backend's address.
name: hedgedoc

components:
  backend:
    runtime: node
    provides:
      - name: api
        protocol: http
        port: 3000
    requires:
      - type: postgres
        set_env:
          HD_DATABASE_URL: $url
    commands:
      start: "node dist/main.js"
      release: "node dist/migrations.js"
    health:
      path: /api/private/config
      start_period: 30s

  frontend:
    runtime: node
    depends_on:
      - component: backend
        condition: healthy
    provides:
      - protocol: http
        port: 3001
        exposed: true
    env:
      HD_BASE_URL:
        default: $components.backend.url
5
The complete HedgeDoc Launchfile with build steps, environment variables, and full wiring between components.
version: launch/v1
name: hedgedoc
description: "Collaborative markdown editor"

components:
  backend:
    runtime: node
    build:
      dockerfile: ./docker/Dockerfile.backend
    provides:
      - name: api
        protocol: http
        port: 3000
        bind: "0.0.0.0"
        spec:
          openapi: file:docs/openapi.yaml
    requires:
      - type: postgres
        version: ">=15"
        set_env:
          HD_DATABASE_URL: $url
          HD_DATABASE_USERNAME: $user
          HD_DATABASE_PASSWORD: $password
          HD_DATABASE_NAME: $name
    env:
      HD_AUTH_SESSION_SECRET:
        generator: secret
        sensitive: true
      HD_BACKEND_BIND_IP:
        default: "0.0.0.0"
    commands:
      start: "node dist/main.js"
      release: "node dist/migrations.js"
    health:
      path: /api/private/config
      start_period: 30s

  frontend:
    runtime: node
    build:
      dockerfile: ./docker/Dockerfile.frontend
    depends_on:
      - component: backend
        condition: healthy
    provides:
      - protocol: http
        port: 3001
        exposed: true
    env:
      HD_BASE_URL:
        default: $components.backend.url
        description: "Backend URL for SSR API calls"

Cross-component wiring

How components talk to each other

The expression $components.backend.url resolves to the backend's internal URL at deploy time. No hardcoded ports, no guessing — the provider handles the wiring.

Notice that only the frontend has exposed: true. That's because the frontend is the public-facing entry point — users hit the frontend, which talks to the backend internally. The backend stays on the private network, inaccessible from the outside.

This pattern scales to any number of components. A backend, a frontend, a websocket server, a background worker — each gets a key in components:, declares its own ports and dependencies, and uses expressions to discover the others.

In the wild

Here's the full HedgeDoc Launchfile from the spec examples. This is the same file we built step by step — now with all the production details.

hedgedoc/Launchfile View on GitHub
version: launch/v1
name: hedgedoc
description: "Collaborative markdown editor"

components:
  backend:
    runtime: node
    build:
      dockerfile: ./docker/Dockerfile.backend
    provides:
      - name: api
        protocol: http
        port: 3000
        bind: "0.0.0.0"
        spec:
          openapi: file:docs/openapi.yaml
    requires:
      - type: postgres
        version: ">=15"
        set_env:
          HD_DATABASE_URL: $url
          HD_DATABASE_USERNAME: $user
          HD_DATABASE_PASSWORD: $password
          HD_DATABASE_NAME: $name
    env:
      HD_AUTH_SESSION_SECRET:
        generator: secret
        sensitive: true
      HD_BACKEND_BIND_IP:
        default: "0.0.0.0"
    commands:
      start: "node dist/main.js"
      release: "node dist/migrations.js"
    health:
      path: /api/private/config
      start_period: 30s

  frontend:
    runtime: node
    build:
      dockerfile: ./docker/Dockerfile.frontend
    depends_on:
      - component: backend
        condition: healthy
    provides:
      - protocol: http
        port: 3001
        exposed: true
    env:
      HD_BASE_URL:
        default: $components.backend.url
        description: "Backend URL for SSR API calls"
depends_on:
The frontend waits for the backend to pass its health check before starting.
$components.backend.url
The provider resolves this to the backend's internal URL — no hardcoded addresses.
exposed: true
Only the frontend is public-facing. The backend stays on the internal network.

Check your understanding

Why does only the frontend have `exposed: true`?
Key takeaways
  • Use components: when your app has multiple services (frontend, backend, workers).
  • depends_on controls startup order — a component waits until its dependency is healthy.
  • $components.<name>.url lets components discover each other without hardcoded addresses.
  • Only public-facing components need exposed: true — keep backends internal.
esc
Type to search the docs