Skip to content

holden.yml

The holden.yml file defines your app’s services and infrastructure needs. It lives in your app’s git repo.

services:
web:
image: ghcr.io/you/myapp:latest
domain: myapp.example.com
port: 3000
memory: 512m
cpu: 1
env:
DATABASE_URL: ${needs.postgres.url}
volumes:
- ./uploads:/app/uploads
worker:
image: ghcr.io/you/myapp:latest
command: ["bun", "worker.ts"]
memory: 256m
cpu: 0.5
env:
DATABASE_URL: ${needs.postgres.url}
needs:
postgres:
valkey:
data_dir: /mnt/big-drive/myapp

Each key under services: defines a container. The key becomes the service name.

image: ghcr.io/you/myapp:latest

Docker image to run.

Required.


domain: myapp.example.com

Public domain(s) for this service. Holden configures Traefik routing automatically.

# Multiple domains
domain:
- myapp.example.com
- www.myapp.example.com

Optional. Services without domain: are internal-only (workers, schedulers).


domain_mode: redirect

How to handle multiple domains.

ValueBehavior
redirect (default)Redirect all domains to the first one
allServe content on all domains, no redirects

Optional. Default: redirect


port: 3000

Port your container listens on. Holden tells Traefik to route HTTP traffic to this port.

The port is only accessible inside the app’s private network (holden-{app-id}). External access requires domain: to be set, which configures Traefik routing.

Multiple services in the same app can use the same port number—they’re isolated on the network.

Required if domain: is set.


networks: ["backend"]

Join shared networks for cross-app communication. Holden creates networks as holden-{name}.

Services on the same network can reach each other as {app-id}-{service-name}.

networks: ["backend", "monitoring"] # join multiple networks

Optional. No shared networks if not specified.


startup_timeout: 5m

Max time to wait for a new container to become healthy during zero-downtime deployment. If the timeout expires (or Docker marks the container “unhealthy”), Holden removes the new container and keeps the old one running.

Only applies to containers with a Docker HEALTHCHECK in their image.

Optional. Default: 5m


drain_timeout: 30s

Time between SIGTERM and SIGKILL when stopping containers. This gives your app time to finish in-flight requests before being forcefully terminated.

Optional. Default: 10s


command: ["bun", "worker.ts"]

Override the container’s default command (CMD). The image’s ENTRYPOINT is preserved—this replaces only the CMD portion.

# If the image has: ENTRYPOINT ["node"] CMD ["server.js"]
# This runs: node worker.js
command: ["worker.js"]

Optional.


Not yet implemented. Will support cron syntax for scheduled tasks that run once and exit, rather than long-running services.


env:
NODE_ENV: production
DATABASE_URL: ${needs.postgres.url}
API_KEY: ${secret.API_KEY}

Environment variables for the container. Supports variable syntax.

Optional.


volumes:
- ./uploads:/app/uploads
- /mnt/nas/assets:/app/assets

Mount volumes into the container.

FormatBehavior
./path:/containerRelative: stored at [@base_data_dir]/{app-id}/path
/absolute:/containerAbsolute: passed through as-is

Optional.


memory: 512m

Maximum memory for the container. Uses Docker memory format: 128m, 1g, 2.5g.

Optional. No limit if not specified.


cpu: 0.5

Maximum CPU cores. Decimal value where 1.0 equals one full core, 0.5 is half a core.

Optional. No limit if not specified.


needs:
postgres:
valkey:
garage:

Managed infrastructure services. Holden creates these containers automatically with generated credentials.

See Needs for details.

Optional.

needs:
postgres:
version: 17
FieldDefaultDescription
version18PostgreSQL major version. Resolves to the Alpine variant (e.g. 17postgres:17-alpine).
imageCustom image (e.g. postgis/postgis:18-3.5). Overrides version.

Both fields are optional. Bare postgres: uses PostgreSQL 18 Alpine.


services:
web:
volumes:
- ./uploads:/app/uploads
- ./cache:/app/cache
backup_volumes:
- ./uploads

Volumes to include in backups during the maintenance window. Uses host paths (the left side of volume definitions). Needs containers are backed up automatically—this is for app-specific data.

Validation:

  • holden validate errors if a path doesn’t match any volume’s host path
  • At runtime, missing directories are skipped with a warning

Optional. No app volumes backed up if not specified.


data_dir: /mnt/big-drive/myapp

Override where this app’s data is stored.

By default, app data goes to [@base_data_dir]/{app-id}/. For example, if HOLDEN_BASE_DATA_DIR=/appdata and your app ID is myapp, volumes are stored under /appdata/myapp/.

Setting data_dir here replaces the entire path—volumes go directly under the specified directory.

Optional. Default: [@base_data_dir]/{app-id}/


update_policy: always

Controls when Holden checks for new upstream images (base image updates, security patches).

ValueBehavior
always (default)Poll timer checks continuously; maintenance also checks
during_maintenanceOnly check during the maintenance window—no surprise updates during the day
manualOnly update when explicitly triggered via webhook or CLI

Webhooks always trigger an image check regardless of this setting—update_policy controls background checking only.

Use during_maintenance for predictable restarts—your app only updates at 3am, not randomly throughout the day.

Use manual when you have CI/CD webhooks and want full control over when deploys happen.

Optional. Default: always