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
- Explicit
valuefield (set programmatically) env_fileentry (later files override earlier ones)- Environment-specific field (
development,staging,production) 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.
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:
type: secretwithgenerate: generated once, persisted to secrets file. Not regenerated unless an input dependency changes.generatewithouttype: secret: recomputed on everyfed start. Not persisted.type: secretwithoutgenerate: random 32-char alphanumeric (default behavior).
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
httpGet— HTTP request from the host.command— for directimage:services, runs inside the container viadocker exec. For process and Compose-backed services, runs on the host.
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.