Skip to content

Configuration Reference (epd.yml) ​

Complete annotated schema for epd.yml.


Full Annotated Schema ​

yaml
# ==============================================================================
# Global Application Settings
# ==============================================================================
name: myapp                    # App name (see Names below)
mode: docker                   # Runtime mode: "docker" | "process"

# ==============================================================================
# Docker Mode Settings (when mode: docker)
# ==============================================================================
image: ghcr.io/org/myapp       # Docker image repository
registry:                      # Optional: Container registry credentials
  server: ghcr.io
  username: deployer
  password: GITHUB_TOKEN       # Name of the env var holding the token (${GITHUB_TOKEN} and [GITHUB_TOKEN] also work); a literal token is rejected

build:
  dockerfile: Dockerfile       # Relative path to Dockerfile (default: Dockerfile)
  context: .                   # Build context directory (default: .)
  platform: linux/amd64        # Target architecture
  args:                        # Build arguments passed to docker build
    COMMIT_SHA: ${GIT_SHA:-dev}
  secrets:                     # BuildKit build secrets
    npm_token: NPM_TOKEN

# ==============================================================================
# Process Mode Settings (when mode: process)
# ==============================================================================
process:
  install: bun install         # Dependency install command on server
  build: bun run build         # Build command on server
  start: bun run start         # Start command (epd automatically injects $PORT)
  exclude:                     # Files excluded from rsync archive
    - ".git"
    - "node_modules"
    - ".env"
    - "dist"
  source: rsync                # Source transport: "rsync" | "git"

# ==============================================================================
# SSH Connection Details
# ==============================================================================
ssh:
  user: root                   # SSH username (default: root)
                               # Any non-root user needs passwordless sudo.
                               # Override it per server by writing the login
                               # into the host entry: "deploy@203.0.113.10".
  port: 22                     # SSH port (default: 22)
  key: ~/.ssh/id_ed25519       # Path to explicit private key
  sudo_password: ${EPD_SUDO_PASSWORD}  # Only if the user has no NOPASSWD rule;
                               # interactive runs simply prompt for it instead
  proxy_jump: bastion.net      # Optional jump host / bastion
  options:                     # Extra OpenSSH options
    - ServerAliveInterval=30

# ==============================================================================
# Server Groups & Service Definitions
# ==============================================================================
servers:
  web:
    hosts:                     # Target servers (IPs or Hetzner selectors)
      - 203.0.113.10
      - deploy@203.0.113.11    # Per-host ssh login, overrides ssh.user
      - hetzner:prod-web-1     # Dynamic Hetzner query by server name
      - hetzner:label:app=web  # Dynamic Hetzner query by label selector
    replicas: 2                # Container replicas per host (1-256; see Limits)
    port: 3000                 # Internal port the app listens on
    domains: [myapp.com]       # Public domains (automatic SSL)
    cpus: "2.0"                # Docker CPU limit
    memory: "1g"               # Docker RAM limit
    drain: 10                  # Graceful drain period (seconds) before old slot stops
    stop_timeout: 15           # Grace period before SIGKILL
    volumes:
      - /var/lib/epd/volumes/uploads:/app/uploads
    env:
      clear:
        APP_ENV: production
      secret:
        - APP_SECRET

  worker:                      # Background worker service (no exposed routes)
    hosts: [203.0.113.10]
    replicas: 1
    command: bun run worker.ts # Custom entrypoint command

# ==============================================================================
# Shared Reverse Proxy (Traefik)
# ==============================================================================
proxy:
  enabled: true                # Start & manage shared Traefik proxy (default: true)
  ssl: true                    # Auto-provision Let's Encrypt SSL (default: true)
  email: admin@myapp.com       # ACME registration email
  challenge: http              # SSL challenge type: "http" | "dns" | "tlsalpn"
  entrypoints:
    web: 80
    websecure: 443
  cross_host: false            # Route requests across all cluster nodes
  private_ips:                 # VPC IPs for cross-host load balancing
    203.0.113.10: 10.0.0.10

# ==============================================================================
# Automated DNS Synchronization (Cloudflare / Hetzner)
# ==============================================================================
dns:
  provider: cloudflare         # "cloudflare" | "hetzner"
  api_token: ${DNS_API_TOKEN}  # Provider API token
  proxied: true                # Cloudflare CDN orange-cloud (default: true)
  ttl: 300                     # DNS TTL in seconds
  auto_sync: true              # Sync on epd deploy and epd setup (default: true)

# ==============================================================================
# Hetzner Cloud Integration
# ==============================================================================
hetzner:
  api_token: ${HETZNER_API_TOKEN}

# ==============================================================================
# Health Check Probes
# ==============================================================================
healthcheck:
  path: /health                # HTTP endpoint to probe (default: "/")
  status: 200-399              # Acceptable HTTP status codes
  timeout: 60                  # Timeout in seconds before rolling back
  interval: 2                  # Seconds between probe attempts
  delay: 1                     # Initial seconds before first probe

# ==============================================================================
# Environment Variables & Secrets
# ==============================================================================
env:
  clear:                       # Public variables (safe in git)
    NODE_ENV: production
  secret:                      # Private secrets (read from .env or shell)
    - DATABASE_URL
    - STRIPE_KEY

# ==============================================================================
# Persistent Accessories (Databases, Caches)
# ==============================================================================
accessories:
  db:
    image: postgres:17-alpine
    host: 203.0.113.10
    env:
      clear:
        POSTGRES_DB: myapp
      secret:
        - POSTGRES_PASSWORD
    volumes:
      - /var/lib/epd/volumes/myapp-db:/var/lib/postgresql/data
    ports:
      - "127.0.0.1:5432:5432"

  redis:
    image: redis:7-alpine
    host: 203.0.113.10
    volumes:
      - /var/lib/epd/volumes/myapp-redis:/data

# ==============================================================================
# Lifecycle Hooks
# ==============================================================================
hooks:
  pre_build: ./scripts/lint.sh
  pre_deploy: ./scripts/db-migrate.sh
  post_deploy: ./scripts/notify-slack.sh
  on_failure: ./scripts/alert-ops.sh

# ==============================================================================
# General Strategy & Retention
# ==============================================================================
strategy: rolling              # Rollout strategy: "rolling" | "parallel"
keep_releases: 5               # Past images / releases to retain (1-1000)
remote_root: /var/lib/epd      # Root directory on remote hosts

Limits and Rules ​

epd checks these when it loads epd.yml. An error names the file that sets the offending key and the key path, for example epd.staging.yml: servers.web.replicas. Overlay files (epd.<destination>.yml) are checked the same way.

Names. App, service and accessory names are 1-63 characters: lowercase letters, digits, dashes and underscores, starting with a letter or digit. Upper-case names are rejected. The name becomes container and state names on the server, so renaming an app creates a new app there.

Host names. A route host is a DNS name, optionally *. wildcard, optionally with one trailing dot (example.com.), or an IPv4 address, or a bracketed IPv6 literal such as "[2001:db8::1]". A route host may add :port (1-65535). Server hosts take IPv4 or plain IPv6 addresses, and may take a user@ prefix.

Ports. Service, route, health check, accessory, ssh.port and proxy entrypoint ports are whole numbers from 1 to 65535. host_port is 1 to 65504, which leaves room for the replica ports after it. port_base is 1 to 65280, because each app reserves 256 consecutive ports from it.

Replicas. replicas is a whole number from 1 to 256 per server. When host ports are published (mode: process, proxy.cross_host: true, or proxy.enabled: false), a service may have at most 16 replicas per server.

Retention. keep_releases is a whole number from 1 to 1000.

Paths. A route path and a health check path start with /. They must not contain spaces, quotes, backticks, backslashes, control characters or .. segments. remote_root is an absolute path with no .. segments or control characters.

Environment variables. Variable names are letters, digits and underscores, and do not start with a digit. This applies to env.clear and env.secret keys, accessory env, proxy.dns_env, build.args keys and build.secrets values. Values in env.clear must not contain newlines, because each variable is one line in the env file. Wherever a mapping is accepted for env, a list of KEY=value strings is also accepted, and the same checks apply.

Released under the MIT License.