Configuration Reference (epd.yml)
Complete annotated schema for epd.yml.
Full Annotated Schema
# ==============================================================================
# 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 hostsLimits 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.