Skip to content

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 ​

KeyTypeDefaultDescription
setupstring | 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
namestring—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
prodobject—Production setup replacement, env overrides, require (keys the deployment must supply) and services (which services run), used with --prod
servicesmapping— (required)The services to run; see Service options
use_envstring | list[].env file(s) loaded for all services, later files win. Alias: env_file
envmapping | list{}Env values shared by all services; references resolve
hoststring127.0.0.1Default bind/probe host for all services
url_hoststringlocalhostDefault host used in generated URLs
protocolstringhttpDefault URL scheme
port_range{start, end}51000–59999Range scanned for free ports
port_start / port_endinteger51000 / 59999Flat alternative to port_range
prefer_declared_portbooleanfalseTry each service's declared port: first when free
docker_networkstringportler-<project>-<hash>Docker network shared by the project's containers
k8s_namespacestringportler-<project>-<hash>Namespace for Kubernetes mode; must be a valid Kubernetes name
proxyobject—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_*.

KeyTypeDefaultDescription
setupstring | list of commands and steps—Host shell commands in service cwd, with resolved service env; steps as at the top level
prodobject—Production setup, command replacements, env overrides and require
commandstring—Shell command that starts the service (local mode)
cwdstring.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
portinteger—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_envstring | list[]Env variable name(s) that receive the port
envmapping | list{}Service env; wins over root env and .env files in development. Alias: environment
hoststringtop-level hostBind/probe host override
url_hoststringtop-level url_hostURL host override
protocolstringtop-level protocolURL scheme override
prefer_declared_portbooleantop-level valueTry the declared port: first when free
depends_onstring | list | mapping[]Dependencies; see depends_on
healthcheckstring | object | none/falsetcp when port is setReadiness check; see healthcheck. Alias: ready
imagestring—Docker image; makes this a Docker service
buildstring | object—Docker build config; makes this a Docker service. See build
dockerfilestring—Shorthand for build: {context: ., dockerfile: ...}
volumeslist[]Docker volumes; makes this a Docker service. See volumes
container_namestringportler-<project>-<hash>-<service>Docker container name; must be unique across the project's services in a run mode. Alias: containerName
typedocker—Force Docker service without other Docker keys
dockertrue | string | object—Docker-mode override, used only by portler up docker. See docker
k8strue | 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:

yaml
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
StepWhen 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 ​

yaml
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 readiness

Unknown dependencies and cycles are rejected at load time. See Dependencies & readiness.

healthcheck ​

yaml
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 ​

yaml
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: 22

A dockerfile: on the service itself takes precedence over build.dockerfile. Built images default to the name portler-<project>-<hash>-<service>.

volumes ​

yaml
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 volume

Normal 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:

yaml
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/app

In 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:

yaml
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 /data

See Kubernetes mode.

proxy ​

yaml
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.
  • host overrides only the proxy bind address (default: root host), not backend binds or advertised URLs. Use root host: 127.0.0.1 with proxy.host: 0.0.0.0 to expose one app endpoint while retaining loopback backend assignments. The listener remains plain HTTP; terminate TLS at a separate edge.
  • trusted_proxies is 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 single http/https scheme, 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/checked Host metadata. Untrusted incoming metadata is discarded. Forwarded, X-Real-IP, and unsupported X-Forwarded-* headers are always removed, identically for HTTP and WebSocket upgrades. See the full forwarding/trust policy before enabling trust.
  • allowed_hosts extends the Host headers the proxy accepts. Loopback names (localhost, *.localhost, 127.0.0.0/8, ::1) and the project's url_host are always allowed; everything else gets a 403. See DNS-rebinding protection.

See Proxy: one project URL.

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 ​

yaml
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

Released under the MIT License.