Services
Every Holden app has a services: block. Each service becomes one container. An app can have one service or many - a web server alone, or a web server plus workers and schedulers grouped together with shared networking and needs.
The Services Block
Section titled “The Services Block”services: web: image: myapp:latest domain: app.example.com port: 3000 env: DATABASE_URL: ${needs.postgres.url}
worker: image: myapp:latest command: ["bun", "worker.ts"] env: DATABASE_URL: ${needs.postgres.url}
needs: postgres:This creates two containers:
myapp-web- serves HTTP trafficmyapp-worker- runs background jobs
Both share the same network (holden-myapp) and the same Postgres instance.
Service Types
Section titled “Service Types”Web Services
Section titled “Web Services”Services with domain: get Traefik routing. They need a port: to tell Traefik where to forward traffic.
services: web: image: myapp:latest domain: app.example.com port: 3000Workers
Section titled “Workers”Services without domain: are workers. They run in the background, typically processing queues or running scheduled tasks.
services: worker: image: myapp:latest command: ["bun", "worker.ts"]Workers can still connect to needs and other services on the app network.
Multiple Domains
Section titled “Multiple Domains”A service can have multiple domains:
services: web: image: myapp:latest domain: - app.example.com - www.example.com port: 3000By default, all domains redirect to the first one. See Networking for more options.
Shared Needs
Section titled “Shared Needs”Needs are defined at the app level, not per-service. All services in an app share the same needs.
services: web: image: myapp:latest env: DATABASE_URL: ${needs.postgres.url} REDIS_URL: ${needs.valkey.url}
worker: image: myapp:latest env: DATABASE_URL: ${needs.postgres.url} # Same database REDIS_URL: ${needs.valkey.url} # Same Valkey
needs: postgres: valkey:Service-to-Service Communication
Section titled “Service-to-Service Communication”Services in the same app can reach each other by name:
services: web: image: myapp:latest env: WORKER_URL: http://worker:8080
worker: image: myapp:latest port: 8080The hostname is the service name (web, worker). No full container name needed.
Volumes
Section titled “Volumes”Volumes are stored at the app level, so services can share them:
services: web: volumes: - ./uploads:/app/uploads # → /appdata/myapp/uploads
worker: volumes: - ./uploads:/app/uploads # Same directory - ./cache:/worker/cache # → /appdata/myapp/cacheSee Volumes for more details on paths and permissions.
Resource Limits
Section titled “Resource Limits”Set memory and CPU limits per service:
services: web: image: myapp:latest memory: 512m cpu: 0.5If not set, containers run without limits. See the holden.yml reference for all options.
Example: Full Stack App
Section titled “Example: Full Stack App”services: web: image: ghcr.io/myorg/myapp:latest domain: app.example.com port: 3000 env: DATABASE_URL: ${needs.postgres.url} REDIS_URL: ${needs.valkey.url} S3_ENDPOINT: ${needs.garage.url}
worker: image: ghcr.io/myorg/myapp:latest command: ["bun", "run", "worker"] env: DATABASE_URL: ${needs.postgres.url} REDIS_URL: ${needs.valkey.url}
scheduler: image: ghcr.io/myorg/myapp:latest command: ["bun", "run", "scheduler"] env: DATABASE_URL: ${needs.postgres.url}
needs: postgres: valkey: garage:This deploys:
- Web server at
app.example.com - Background worker processing Valkey queues
- Scheduler running cron jobs
- PostgreSQL, Valkey, and Garage as supporting infrastructure
All containers share one network and can communicate freely.