portler.yml reference
Portler looks for portler.yml (or portler.yaml) in the current directory; -f/--file <path> selects another file. The file must contain a top-level services: mapping with at least one service.
Still changing
The portler.yml format is being improved and can change in breaking ways in upcoming releases. Check the release notes when you upgrade.
YAML subset
Portler uses its own hand-rolled YAML parser covering a practical subset: mappings, block and inline lists, scalars, quoting, and comments. Anchors, multi-document files, and other exotic features are not supported.
Top-level options
| Key | Type | Default | Description |
|---|---|---|---|
setup | string | list of commands and steps | — | Host shell commands run from the project root; a step object adds if_missing, always or watch. See Setup and prod |
name | string | — | Stable project name. Under --prod it names the Docker network, the containers and the managed volumes instead of the directory hash; [a-z0-9][a-z0-9_.-]* ending in a letter or digit, no --, up to 40 characters. See Stable identity on a server |
prod | object | — | Production setup replacement, env overrides, require (keys the deployment must supply) and services (which services run), used with --prod |
services | mapping | — (required) | The services to run; see Service options |
use_env | string | list | [] | .env file(s) loaded for all services, later files win. Alias: env_file |
env | mapping | list | {} | Env values shared by all services; references resolve |
host | string | 127.0.0.1 | Default bind/probe host for all services |
url_host | string | localhost | Default host used in generated URLs |
protocol | string | http | Default URL scheme |
port_range | {start, end} | 51000–59999 | Range scanned for free ports |
port_start / port_end | integer | 51000 / 59999 | Flat alternative to port_range |
prefer_declared_port | boolean | false | Try each service's declared port: first when free |
docker_network | string | portler-<project>-<hash> | Docker network shared by the project's containers |
k8s_namespace | string | portler-<project>-<hash> | Namespace for Kubernetes mode; must be a valid Kubernetes name |
proxy | object | — | Reverse proxy block; see Proxy |
Service options
Each entry under services: accepts the options below. Service names must be 1–63 ASCII letters, digits, _, or -, starting with a letter or digit. Names must also produce distinct upper-snake-case environment prefixes: for example, api-main and api_main cannot coexist because both would generate PORTLER_API_MAIN_*.
| Key | Type | Default | Description |
|---|---|---|---|
setup | string | list of commands and steps | — | Host shell commands in service cwd, with resolved service env; steps as at the top level |
prod | object | — | Production setup, command replacements, env overrides and require |
command | string | — | Shell command that starts the service (local mode) |
cwd | string | . | Working directory, relative to the project root: the folder Portler runs in, which --file does not change. A folder that does not exist stops up, restart, setup and start before anything runs |
port | integer | — | Declared port (local; tried first only with prefer_declared_port, see Ports) / internal container port (Docker). Services without a port get no assignment and cannot be referenced |
port_env | string | list | [] | Env variable name(s) that receive the port |
env | mapping | list | {} | Service env; wins over root env and .env files in development. Alias: environment |
host | string | top-level host | Bind/probe host override |
url_host | string | top-level url_host | URL host override |
protocol | string | top-level protocol | URL scheme override |
prefer_declared_port | boolean | top-level value | Try the declared port: first when free |
depends_on | string | list | mapping | [] | Dependencies; see depends_on |
healthcheck | string | object | none/false | tcp when port is set | Readiness check; see healthcheck. Alias: ready |
image | string | — | Docker image; makes this a Docker service |
build | string | object | — | Docker build config; makes this a Docker service. See build |
dockerfile | string | — | Shorthand for build: {context: ., dockerfile: ...} |
volumes | list | [] | Docker volumes; makes this a Docker service. See volumes |
container_name | string | portler-<project>-<hash>-<service> | Docker container name; must be unique across the project's services in a run mode. Alias: containerName |
type | docker | — | Force Docker service without other Docker keys |
docker | true | string | object | — | Docker-mode override, used only by portler up docker. See docker |
k8s | true | object | — | Kubernetes-mode config, used only by portler up k8s. See k8s |
A service becomes a Docker service (runs in Docker even under plain portler up) when it has image, build, dockerfile, volumes, or type: docker.
Reserved service names
docker and k8s are always reserved (they select run modes on the command line). proxy is reserved when a proxy: block exists.
Setup and prod
setup accepts a command or a list; each entry runs sequentially through the shell, stopping on the first non-zero exit. Root setup runs from the project root; service setup runs in its cwd. Setup is always on the host, even for Docker services.
setup may also be a single step object. A list entry is a command string or a step object with run and one of three modifiers:
setup:
- pnpm install # once per checkout; again when the line or cwd changes
- run: pnpm install
if_missing: node_modules # only while nothing exists at this path
- {run: pnpm run codegen, always: true} # on every up and setup
- run: pnpm run db:generate
watch: [prisma/schema.prisma] # once per checkout; again when the schema changes| Step | When it runs |
|---|---|
<command> or {run: <command>} | Once per checkout. A receipt in .portler/setup.json records the success; the step runs again when its command or cwd changes, with up --setup, or on portler setup. Identical commands in one list are separate steps, told apart by their position among all entries with that command; adding or removing one of them runs every entry with that command again. A step that is removed loses its receipt before the next up or setup, even when its service is not part of that run, and so does every step of a service that leaves the config. |
{run, if_missing: <path>} | Only while nothing exists at <path>, resolved against the step's cwd. A file, a folder, or a link whose target exists all count. No receipt: the path is the receipt, so deleting what the step produced runs it again, and --setup does not override it. |
{run, always: true} | On every up (unless --no-setup) and every setup. No receipt. |
{run, watch: [files]} | Like a plain step, with the content of the listed files (relative to the step's cwd) folded into its receipt: the step runs again when one of them changes, appears or disappears. The files are read when the step is reached, so a change an earlier step made in the same run counts, and recorded as they are once the step succeeds, so a step that rewrites its own input (an install updating its lockfile) does not run again for that. A listed path that exists but cannot be read (no permission, a directory) stops setup with an error instead of counting as absent. |
Every command, bare or as run, must be non-empty; if_missing, always and watch cannot be combined with each other; other keys are rejected.
Top-level prod accepts setup, env, require and services; service prod accepts setup, env, command and require. Unknown keys are rejected. With --prod, setup and command replace their ordinary definitions, while env merges over env at the same level. Omitted overrides inherit ordinary values; setup: [] disables an inherited setup.
prod.env wins over Docker-mode env. prod.command only replaces a container command that comes from command (top-level image/build services); a docker: block keeps docker.command or the image CMD. With --prod and in portler start, the deployment wins over config: process env, then the use_env file (a missing one is skipped), then config env. Kubernetes does not apply production overrides or setup; production/setup flags are rejected in up k8s. prod is a config key but not a reserved service name or positional mode; use --prod. See Setup & production.
prod.require lists variables the deployment must supply: with --prod, up, setup, start and restart fail before anything runs or is stopped when one of them is missing or empty, naming it. A required key is always taken from the deployment (process env or the use_env file); its config value is not used, and neither is a value Portler sets itself (PORTLER_*, a port_env name such as PORT), so a blank prod.env entry and a require entry mean the same, and required keys reach containers whether or not an env: names them. A blank top-level prod.env entry belongs to the env each service and the top-level setup steps see: it is required by each service that does not set the key itself, and by the project only while its top-level setup steps run. A top-level prod.require holds always. Top-level prod.services lists the services that run under --prod; the rest are external to the deployment: they are not started, dependencies on them are dropped, a proxy route to one is an error, and naming one on the command line is an error. Without it every service runs.
depends_on
depends_on: postgres # single dependency
depends_on: # list form
- postgres
- redis
depends_on: # mapping form with conditions
postgres:
condition: ready # default; aliases: healthy, service_healthy
worker:
condition: started # don't wait for readinessUnknown dependencies and cycles are rejected at load time. See Dependencies & readiness.
healthcheck
healthcheck: none # or false: ready immediately
healthcheck: tcp # type as a plain string
healthcheck: # object form
type: http # none | tcp | http | command; inferred when omitted
url: http://localhost:${backend.port}/healthz # for type: http
command: ./scripts/check.sh # for type: command; alias: test
timeout_ms: 60000 # default 60000
interval_ms: 500 # default 500
accept: [200, 401] # http only: statuses that mean ready (default: any 2xx or 3xx)Type inference: command/test present → command; url present → http; otherwise tcp. Default for a service with a port is a TCP check on the assigned port. For services that run in a container, the connection must also stay open for a moment (200 ms) or the peer must send something, because Docker's port proxy accepts connections even while nothing listens inside the container and never sends data itself; a timeout_ms shorter than that can never pass for a container whose listener stays silent. An explicit command check requires a command, an explicit tcp check requires a service port, and an http check requires either a URL or a service port. An http check counts a 2xx or 3xx response as ready; a 401 or 404 is not (the server is up, but the route or the credentials are not), unless accept: lists the statuses that count, for example accept: [200, 404]. ready: is an alias for healthcheck:. Timings must be positive and no greater than 2147483647ms (Node clamps larger timers to 1ms).
build
build: apps/api # string: the build context
build: # object form
context: . # default .
dockerfile: apps/api/Dockerfile # resolved relative to context
target: dev # multi-stage build target
args: # build args (mapping or KEY=value list)
NODE_VERSION: 22A dockerfile: on the service itself takes precedence over build.dockerfile. Built images default to the name portler-<project>-<hash>-<service>.
volumes
volumes:
- ./data:/data # bind mount, resolved from the project dir
- '@postgres-data:/var/lib/postgresql/data' # Portler-managed named volume
- myvolume:/somewhere # plain Docker named volumeNormal Docker syntax is supported. A source starting with @ declares a Portler-managed volume named portler-<project>-<hash>-<name>; managed names are slugged (a-z0-9_.-), must contain an ASCII letter or digit, and must not contain --. Two differently written names that normalize to the same slug are rejected rather than silently sharing data.
docker (mode override)
Configures how the service runs under portler up docker only — plain up ignores it:
docker: true # containerize with the top-level Docker config (or a default image)
docker: node:22-alpine # string: an image
docker: # object: full Docker config
image: node:22-alpine
build: {context: ., dockerfile: Dockerfile}
container_name: my-api
volumes: ['@data:/data']
command: node server.js
env: # merged OVER the service env in Docker mode
DATABASE_URL: postgres://app:app@postgres:5432/appIn Docker mode, containers share the project network and reach each other by service name (postgres:5432).
k8s
Configures the service under portler up k8s / portler k8s render only:
k8s: true # defaults: replicas 1, no overrides
k8s:
replicas: 2 # Positive Deployment replica count, default 1
env: # in-cluster overrides (k8s DNS names); alias: environment
DATABASE_URL: postgres://app:app@postgres:5432/app
volume: 1Gi # string shorthand for size
k8s:
volume:
size: 1Gi # required (PVC size)
mount_path: /var/lib/postgresql/data # default: first Docker volume's container path, or /dataSee Kubernetes mode.
proxy
proxy:
host: 127.0.0.1 # optional bind override; default: root host
trusted_proxies: [] # exact IPv4/IPv6 source IPs of trusted edges; no implicit loopback trust
port: auto # "auto" (or omitted) = Portler picks; a number is preferred when free
allowed_hosts: # optional: extra Host headers the proxy answers for
- myapp.test
routes: # at least one route required
/api: backend # prefix -> service name
/: frontend- Prefixes must start with
/; a trailing slash is ignored (/api/≡/api). - Longest prefix wins, respecting path segment boundaries; the prefix is not stripped before forwarding.
- Every route must target a known service that declares a
port. - Duplicate prefixes are an error.
hostoverrides only the proxy bind address (default: roothost), not backend binds or advertised URLs. Use roothost: 127.0.0.1withproxy.host: 0.0.0.0to expose one app endpoint while retaining loopback backend assignments. The listener remains plain HTTP; terminate TLS at a separate edge.trusted_proxiesis an array of exact IPv4/IPv6 peer addresses, default[]. No hostnames, wildcards, CIDRs, ports, or scope identifiers; loopback is not implicitly trusted. Equivalent IPv6 and IPv4-mapped IPv6 identities match. Only allowlist controlled edges that sanitize client forwarding headers. Trusted peers may supply a singlehttp/httpsscheme, a valid allowed hostname authority, and an IP-only client chain. Portler validates each independently, appends the socket IP to a valid chain, and otherwise falls back to socket/checkedHostmetadata. Untrusted incoming metadata is discarded.Forwarded,X-Real-IP, and unsupportedX-Forwarded-*headers are always removed, identically for HTTP and WebSocket upgrades. See the full forwarding/trust policy before enabling trust.allowed_hostsextends theHostheaders the proxy accepts. Loopback names (localhost,*.localhost,127.0.0.0/8,::1) and the project'surl_hostare always allowed; everything else gets a403. See DNS-rebinding protection.
Service references
Env values (top-level and per-service) can reference other services, either as the whole value (backend.url) or interpolated (${postgres.port}). Valid properties: url, port, host, url_host, protocol, desired_port, container, image, name. See Services & environment.
Complete example
setup: npm install
prod:
setup:
- npm ci
- npm run build
env:
NODE_ENV: production
use_env: .env
prefer_declared_port: false
port_range: {start: 51000, end: 59999}
proxy:
port: auto
routes:
/api: backend
/: frontend
services:
postgres:
image: postgres:16-alpine
port: 5432
env:
POSTGRES_USER: app
POSTGRES_PASSWORD: app
POSTGRES_DB: app
volumes:
- '@postgres-data:/var/lib/postgresql/data'
healthcheck:
command: docker exec $PORTLER_POSTGRES_CONTAINER pg_isready -h 127.0.0.1 -U app -d app
k8s:
volume:
size: 1Gi
mount_path: /var/lib/postgresql/data
backend:
command: npm run dev
setup: npm run generate
prod:
command: npm start
setup: []
env:
LOG_LEVEL: info
cwd: backend
port: 4000
port_env: PORT
depends_on:
- postgres
env:
DATABASE_URL: postgres://app:app@localhost:${postgres.port}/app
PUBLIC_ORIGIN: proxy.url
docker:
build:
context: .
dockerfile: apps/api/Dockerfile
env:
DATABASE_URL: postgres://app:app@postgres:5432/app
k8s:
env:
DATABASE_URL: postgres://app:app@postgres:5432/app
frontend:
command: npm run dev
cwd: frontend
port: 3000
port_env: PORT
depends_on:
- backend
env:
VITE_API_URL: backend.url