fedService Federation

Configuration

Everything fed does is driven by one file: service-federation.yaml in your project root. Run fed init to generate a starter config.

The canonical shape

Declare every host or process port that must vary as a type: port parameter, then interpolate it with {{...}} wherever it appears. Literal listener and host-mapping ports can't be remapped by isolation; fixed container-internal ports such as Postgres's 5432 can stay literal.

parameters:
  API_PORT:
    type: port
    default: 8080
  DB_PORT:
    type: port
    default: 5432

services:
  database:
    image: postgres:16-alpine
    ports: ["{{DB_PORT}}:5432"]
    environment:
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: app

  api:
    process: npm start -- --port {{API_PORT}}
    depends_on: [database]
    startup_message: 'http://localhost:{{API_PORT}}'
    environment:
      DATABASE_URL: 'postgres://postgres:postgres@localhost:{{DB_PORT}}/app'
    healthcheck:
      httpGet: 'http://localhost:{{API_PORT}}/health'

entrypoint: api

Services

Every service has one type-defining field or pair: process, image, or composeFile + composeService — or it consists of lifecycle hooks alone.

Process

Run any command on the host:

services:
  api:
    process: npm start
    cwd: ./api

Hook-only (setup nodes)

A service can be nothing but its lifecycle hooks. You want this when several services need the same prepared state — a pushed schema, an installed workspace — because install/migrate on a normal service gate only that service. Declare the preparation once, and let everyone depend on it:

services:
  db-schema:
    migrate: npx prisma db push
    depends_on: [postgres]
    environment:
      DATABASE_URL: '{{DATABASE_URL}}'

  api:
    process: npm start
    depends_on: [db-schema]   # waits for the push to finish

The node runs after its dependencies are healthy; dependents start only after its hooks complete. A failed hook aborts fed start with the node's name. healthcheck and restart are rejected — completion is its readiness — and fed status shows it as completed. Since 6.0.

Docker container

parameters:
  REDIS_PORT:
    type: port
    default: 6379

services:
  redis:
    image: redis:7-alpine
    ports: ["{{REDIS_PORT}}:6379"]

Command override

command overrides the image's CMD — a string (split shell-style) or an array:

services:
  nats:
    image: nats:2
    command: "--jetstream --store_dir /data"
    # or: command: ["--jetstream", "--store_dir", "/data"]

Docker Compose service

Reuse services from an existing docker-compose.yml:

services:
  postgres:
    composeFile: ./docker-compose.yml
    composeService: postgres

Volumes

Docker services can mount named volumes and bind mounts:

services:
  postgres:
    image: postgres:16
    volumes:
      - postgres_data:/var/lib/postgresql/data      # Named volume
      - ./init-scripts:/docker-entrypoint-initdb.d  # Bind mount

Named volumes with a fed- prefix are automatically cleaned up by fed clean.

Tags

Flexible grouping for service selectors. Reference with @tag:

services:
  api:
    process: npm start
    tags: [backend, critical]

  worker:
    process: npm run worker
    tags: [backend, async]
fed start @backend        # Start all services tagged "backend"
fed stop @async           # Stop all services tagged "async"
fed install @critical     # Install only critical services

Watch

File paths for auto-restart when used with fed start --watch:

services:
  api:
    process: npm start
    watch:
      - ./src/**/*.ts
      - ./package.json

Restart policy

services:
  worker:
    process: npm run worker
    restart: always           # Always restart on failure

  api:
    process: npm start
    restart: !onfailure       # Restart on failure, with a retry limit
      max_retries: 3          # Restart up to 3 times on failure

Use no (the default) or always for the simple forms. The retry-limited form uses the !onfailure mapping shown above. A nested on_failure mapping is not accepted by v5.2.1.

Grace period

Graceful shutdown timeout before SIGKILL:

services:
  api:
    process: npm start
    grace_period: "30s"       # Default: 10s

Accepts duration strings: "10s", "1m", "500ms".

Startup timeout

Per-service cap on the complete start attempt, including install and migrate hooks, process or container startup, and health-check polling. It overrides the orchestrator default (120s) for one service. A health check's own timeout can end polling sooner.

services:
  search:
    process: ./bin/search-svc
    startup_timeout: "5m"     # Default: orchestrator-wide (120s)
    healthcheck:
      httpGet: "http://localhost:{{ES_PORT}}/health"

Accepts the same duration strings as grace_period.

Circuit breaker

Crash loop detection. Requires restart: always or the !onfailure form:

services:
  api:
    process: npm start
    restart: always
    circuit_breaker:
      restart_threshold: 5    # Trips after 5 restarts... (default: 5)
      window_secs: 60         # ...within 60 seconds (default: 60)
      cooldown_secs: 300      # Wait 5 minutes before retrying (default: 300)

States: closed (normal, restarts allowed), open (tripped, restarts blocked), half-open (after cooldown, one retry allowed).

Expose

Other projects can only import a service from yours (via packages) if you mark it expose: true — everything else stays private to this config:

services:
  api:
    process: npm start
    expose: true

Parameters

Values referenced throughout your config with {{PARAM}}:

parameters:
  API_PORT:
    type: port
    default: 8080  # See resolution order below

  DB_PORT:
    type: port     # No default — allocates a random available port

  API_KEY:
    default: "dev-key"  # String parameter

Environment-specific values

parameters:
  API_URL:
    default: "http://localhost:8080"
    development: "http://localhost:8080"     # Used with -e development (default)
    staging: "https://staging.example.com"   # Used with -e staging
    production: "https://api.example.com"    # Used with -e production

develop is accepted as an alias for development.

Resolution priority

  1. Explicit value field (set programmatically)
  2. env_file entry (later files override earlier ones)
  3. Environment-specific field (development, staging, production)
  4. default

Shell environment variables are not consulted — configuration lives in the file, not in whatever happens to be exported.

Port parameters add a persisted layer: once allocated, a port is cached and reused across restarts. In normal mode an available default wins over the cache (so editing the config takes effect); under isolation the cache wins and fresh parameters skip the default entirely — random ports are the point. fed isolate rotate re-rolls every allocated port.

Validation with either

parameters:
  LOG_LEVEL:
    default: "info"
    either: [debug, info, warn, error]    # Validated after resolution

Built-in: FED_PROJECT_ID

Every config resolves {{FED_PROJECT_ID}} without declaring it: a stable, cookie-safe identifier for the running stack — the checkout directory's name plus a short hash, with the isolation ID appended when isolation is enabled (myapp-3f2a9c01, myapp-3f2a9c01-rev2).

Use it to namespace anything parallel stacks would otherwise share. The classic case is cookies: localhost cookies are port-agnostic, so two stacks of the same app on different ports log each other out. Suffix the cookie name and they stop colliding:

services:
  web:
    process: npm run dev
    environment:
      SESSION_COOKIE: 'session.{{FED_PROJECT_ID}}'

Declaring your own parameter named FED_PROJECT_ID is a validation error — the name is reserved. Available since 5.3.0.

variables removed in 4.0

variables was an alias for parameters at the top level. It was removed in 4.0 — use parameters. A config still using variables: fails validation with a message telling you to rename it. The schema is identical, so renaming the key is the only change needed.

Note: this is unrelated to per-parameter environment overrides (development/staging/production), which are still supported under parameters.

Secrets

Parameters with type: secret are auto-generated on first fed start and stored in a file you gitignore:

generated_secrets_file: .env.secrets

parameters:
  DB_PASSWORD:
    type: secret
  SESSION_KEY:
    type: secret

services:
  database:
    image: postgres:15
    environment:
      POSTGRES_PASSWORD: '{{DB_PASSWORD}}'

On fed start, if .env.secrets doesn't contain a value for DB_PASSWORD or SESSION_KEY, fed generates 32-character random alphanumeric strings and writes them to .env.secrets. Values are never overwritten once generated — they're stable across restarts.

The generated_secrets_file must be in .gitignore. Inside a git repository, fed checks this and refuses to generate if it isn't:

# .gitignore
.env.secrets

In a terminal, fed asks for confirmation before generating. In CI or non-interactive contexts, it generates and logs a single line saying where the values went. The file is written mode 0600 — readable by you alone.

Manual secrets

For secrets you provide yourself (API keys, OAuth credentials), add source: manual. Teams can share these through the vault — see Team secrets:

parameters:
  GITHUB_CLIENT_SECRET:
    type: secret
    source: manual
    description: "GitHub OAuth client secret"

  STRIPE_SECRET_KEY:
    type: secret
    source: manual
    optional: true
    description: "From https://dashboard.stripe.com/apikeys"

Fed will not generate a value for manual secrets. It first checks local secret and environment files, then a linked team vault. If the value is still missing, startup fails with a message listing what's needed. The description field is shown in this error message.

Add optional: true for secrets that not every team member needs (e.g., a Stripe key only some developers have). Optional manual secrets resolve to an empty string when not provided, instead of failing at startup.

Manual secrets don't require generated_secrets_file — you can use them standalone when you only need to enforce that certain values are provided.

Constraints

Secret parameters cannot have default, environment-specific values (development, staging, production), or either constraints. Secrets are provided externally, not baked into config.

Resolution priority

generated_secrets_file is prepended to the env_file list at runtime, giving it the lowest priority. This means your .env or .env.local files can override generated values when needed.

Custom generation

Use generate to run a shell command that produces the parameter value:

parameters:
  # Generate an Ed25519 keypair for JWT signing.
  JWT_PRIVATE_KEY:
    type: secret
    generate: openssl genpkey -algorithm ed25519 -outform PEM 2>/dev/null | base64 -w0

  JWT_PUBLIC_KEY:
    type: secret
    generate: echo '{{JWT_PRIVATE_KEY}}' | base64 -d | openssl pkey -pubout -outform PEM 2>/dev/null | base64 -w0

  # Computed parameter (not a secret — recomputed every start).
  DB_URL:
    generate: echo "postgres://app:{{DB_PASSWORD}}@localhost:{{DB_PORT}}/mydb"

The command runs via sh -c. Stdout is captured as the value (trimmed). Stderr is discarded on success, shown on failure.

{{PARAM}} references create dependencies. Fed resolves parameters in topological order (DAG). Circular dependencies are detected and rejected.

Invalidation: when a secret with generate is missing and gets generated, all secrets that depend on it (via {{PARAM}} references) are regenerated — even if they already have values. This ensures derived secrets stay in sync with their sources.

Persistence rules:

Dependencies & health checks

Services declare dependencies with depends_on. Fed starts dependencies before dependents and polls configured health checks during startup.

Simple form

services:
  api:
    process: npm start
    depends_on: [database, cache]

Structured form

Control behavior when a dependency fails:

services:
  api:
    process: npm start
    depends_on:
      - database                           # Simple: stop if database fails
      - service: cache
        on_failure: ignore                 # Keep running if cache fails
      - service: worker
        on_failure: restart                # Restart if worker fails

on_failure values: stop (default), restart, ignore.

Health check types

services:
  database:
    image: postgres:15
    healthcheck:
      command: pg_isready -U postgres  # Runs INSIDE the container
      timeout: 10s

  api:
    process: npm start
    healthcheck:
      httpGet: 'http://localhost:{{API_PORT}}/health'
      timeout: 5s  # Optional, default 5s

The health-check timeout is also the startup polling window. If the process or container stays alive but the check never passes, v5.2.1 logs a warning and lets startup continue with the service in Running state; it does not fail fed start solely because the check timed out.

Simple string form (uses default 5s timeout):

healthcheck: "curl -f http://localhost:{{API_PORT}}/health"

Environment files

.env files set parameter values (not service environment directly):

parameters:
  API_KEY:
    default: ""

env_file:
  - .env         # API_KEY=secret123
  - .env.local   # Later files override earlier

All .env variables must be declared as parameters.

A missing env_file is not a fatal error: fed logs a warning and continues without it. Parameters that depend on values from the missing file fall back to their defaults (or fail later with a clearer "missing parameter value" error if they have no default). Parse errors and other I/O failures (e.g. permission denied) still abort startup.

Startup messages

Show where to access services after startup:

services:
  api:
    startup_message: "API docs: http://localhost:{{API_PORT}}/docs"
  frontend:
    startup_message: "App: http://localhost:{{NEXT_PORT}}"
╭──────────────────────────────────────────────────╮
│ API docs: http://localhost:8081/docs             │
├──────────────────────────────────────────────────┤
│ App: http://localhost:3000                       │
╰──────────────────────────────────────────────────╯

Entrypoint services sort last. A warning is emitted if an entrypoint has no startup_message — particularly useful when ports are randomized.

Templates

Reusable base configurations:

parameters:
  API_PORT:
    type: port

templates:
  java-service:
    image: openjdk:17-slim
    environment:
      JAVA_OPTS: '-Xmx512m'
    healthcheck:
      httpGet: 'http://localhost:{{API_PORT}}/actuator/health'

services:
  auth-service:
    extends: java-service
    ports: ["{{API_PORT}}:8080"]

Placeholders in templates resolve against the same global parameters as everywhere else — a template can't rename them per service.

Profiles

Conditionally include services:

services:
  api:
    process: npm start       # No profiles = always included

  worker:
    profiles: [worker]       # Only with -p worker

  debug-tools:
    profiles: [debug]        # Only with -p debug
fed start                    # Starts profileless services only
fed -p worker start          # Starts api + worker
fed -p worker -p debug start # Starts api + worker + debug-tools

Packages

Import service configurations across projects:

packages:
  - source: "github:org/repo@v1.0"
    as: "infra"

services:
  database:
    extends: "infra.postgres"
    environment:
      POSTGRES_DB: "myapp"

Packages are cached locally. fed package refresh to re-fetch, fed --offline start to skip git.

Lifecycle hooks

services:
  backend:
    process: npm start
    cwd: ./backend
    install: npm ci                       # Before first start (offline prep)
    migrate: npx prisma migrate deploy    # After deps healthy, before start
    build: npm run build                  # Runs with `fed build`
    clean: rm -rf node_modules dist       # Runs with `fed clean`

Two of these answer the question "when should this run?":

install — once per checkout. Runs before the service's first start, then never again (until fed clean, or on demand with fed install). Put dependency installation here — work that's done until you wipe it.

migrate — every start. Runs after the service's dependencies are healthy and before the service itself starts, on every fed start. Write it idempotent — migration tools already are: a current schema is a no-op. This is why a migration you pulled this morning is applied the next time you start, with no extra step. The service's full resolved environment is available, and dependents wait for it to complete.

Need the prepared state before other services, not just this one? That's a hook-only service. Need it only when you ask for it? That's a script.

build runs with fed build. clean runs with fed clean.

fed clean also removes Docker volumes with fed- prefix and clears install state.

Resource limits

services:
  api:
    process: npm start
    resources:
      memory: "512m"              # Hard memory limit
      memory_reservation: "256m"  # Soft limit (Docker only)
      memory_swap: "1g"           # Memory + swap limit (Docker only)
      cpus: "0.5"                 # CPU limit (0.5 = 50% of one core)
      cpu_shares: 512             # Relative CPU weight (default: 1024)
      pids: 100                   # Max processes/threads
      nofile: 65536               # Max open file descriptors
      strict_limits: false        # Fail startup if limits can't be set (default: false)

For Docker services, these map to docker run flags. For host process services on Unix, v5.2.1 enforces memory and nofile with rlimits, plus pids on Linux. memory_reservation, memory_swap, cpus, and cpu_shares are Docker-only.

For host processes, strict_limits controls whether a failed setrlimit aborts startup. The default, false, warns and continues. Docker reports invalid or unsupported flags as a normal container-start failure.

Docker image builds

services:
  web:
    cwd: ./apps/web
    build:
      image: my-app
      # dockerfile: Dockerfile  (default)
      # args:                    (optional build arguments)
      #   NODE_ENV: production

fed build builds all services with a build field. fed docker build builds only Docker images. Images are tagged with the git short hash by default.

fed build                          # Build all (shell + Docker)
fed build --tag v1.0.0             # Custom tag
fed build --build-arg KEY=VALUE    # Extra build args
fed docker build                   # Build Docker images only
fed docker build --tag v1.0.0      # Custom tag
fed docker build --json            # Machine-readable output
fed docker push                    # Push images to registry
fed docker push --tag v1.0.0       # Push specific tag

Entrypoint

Declare the main service(s). When no service names are passed, fed start starts these entrypoints and their dependencies. Entrypoints also sort last in startup messages:

entrypoint: backend           # Single entrypoint

# Or multiple:
entrypoints: [frontend, backend]

Cannot specify both entrypoint and entrypoints.

Scripts

Custom commands with dependencies and environment:

scripts:
  test:
    script: npm test
    cwd: ./api
    depends_on: [database]
    environment:
      NODE_ENV: test
    timeout: "5m"             # Library execution only; see the Scripts page

  integration:
    script: npm run test:integration
    depends_on: [database, redis]
    isolated: true            # Fresh ports and direct image-backed resources

  scenario:
    script: ./seed-scenario.sh
    depends_on: [web]
    keep_services: true       # Leave started services running after the script exits
fed run test                  # Start deps, run script, stop deps
fed run integration           # Runs service deps in an isolated child context
fed run scenario              # Start deps, run script, leave deps running

isolated: true allocates fresh random ports, scopes direct image: containers and named volumes, and cleans up after completion. Compose-backed services keep Compose's path-derived project namespace; the isolated script context does not give them a second Compose project.

keep_services: true skips borrow-or-own cleanup so the services the script starts persist (like fed start) until fed stop. It can't be combined with isolated: true — an isolated stack is always torn down. See Scripts.

See also Isolation for how directory scoping works.

Next: Commands →