Skip to content

Docker services ​

Services can run in Docker instead of as local processes. A service becomes a Docker service when it has image, build, dockerfile, volumes, or type: docker.

Image services ​

yaml
services:
  redis:
    image: redis:7-alpine
    port: 6379

Built services ​

Similar to Docker Compose — dockerfile is resolved relative to context:

yaml
services:
  api:
    build:
      context: .
      dockerfile: apps/api/Dockerfile
    port: 4000
    port_env: PORT

build: also accepts a plain string (the context directory), plus target: for multi-stage builds and args: for build arguments:

yaml
build:
  context: .
  dockerfile: apps/api/Dockerfile
  target: dev
  args:
    NODE_VERSION: 22

A top-level dockerfile: on the service (without build:) is shorthand for build: { context: ., dockerfile: ... }.

Ports in Docker services ​

For Docker services, port: is the internal container port. Portler maps a dynamically assigned host port to it, so from the outside the service behaves exactly like a local one: references (api.url, ${api.port}) and PORTLER_* variables use the assigned host port. Inside the container, port_env receives the internal container port.

Environment ​

You can use either env: or the Docker-Compose-style environment: for environment variables — mapping or KEY=value list form. Portler forwards exactly the variables it composed (env files, config env, generated values, port_env) into the container; your shell's unrelated variables don't leak in. An inherited PORTLER_* prefix does not make a host variable explicit, so host paths, Kubernetes safety overrides, arbitrary Portler secrets, and Portler's internal package-manager defaults stay outside the container. Generated PORTLER_* values and keys you deliberately declare are still forwarded.

Volumes ​

Docker volumes support normal Docker syntax:

yaml
services:
  postgres:
    image: postgres:16-alpine
    port: 5432
    volumes:
      - ./data:/docker-entrypoint-initdb.d       # bind mount, resolved from the project dir
      - '@postgres-data:/var/lib/postgresql/data' # Portler-managed named volume
  • Relative bind mounts like ./data:/data are resolved from the project directory.
  • A source starting with @ creates a Portler-managed, project-scoped Docker named volume — recommended for database data on Docker Desktop. See Volumes for naming, forking, and management.

Naming and networking ​

Portler derives stable, project-scoped default names so projects never collide:

ResourceDefault name
Containerportler-<project>-<hash>-<service>
Image (built)portler-<project>-<hash>-<service>
Networkportler-<project>-<hash>

<project> is the project directory basename, <hash> a short hash of its full path. Override with container_name: per service or a top-level docker_network:. Explicit container names must remain unique across the project's services in each run mode; Portler rejects a collision before starting anything.

Under --prod a top-level name: replaces the directory part: containers are portler-<name>--<service>, the network portler-<name>, and managed volumes portler-<name>--<volume>; the double dash keeps project foo with service bar-api apart from project foo-bar with service api. An explicit container_name: or docker_network: is kept, and built images keep the hashed default. That is what lets a release that unpacks into a new folder find its volumes again, and what makes Docker refuse a second release while the previous one is still up. See Stable identity on a server.

All Docker services of a project share the project network, with the service name as network alias — so in full Docker mode containers can reach each other as postgres:5432.

Docker mode: portler up docker ​

Plain portler up runs Docker services in Docker and everything else locally. portler up docker runs the project completely in Docker: every service with any Docker configuration runs in a container.

The per-service docker: key configures how a service runs only in Docker mode (plain up ignores it):

yaml
services:
  api:
    command: npm run dev        # plain "up": local process
    port: 4000
    port_env: PORT
    docker:                      # "up docker": containerized
      build:
        context: .
        dockerfile: apps/api/Dockerfile
      env:
        DATABASE_URL: postgres://app:app@postgres:5432/app

docker: accepts:

  • true — reuse the service's top-level Docker config (or a default image name),
  • a string — an image name,
  • an object — full Docker config (image, build, container_name, volumes, env/environment, command).

Env values under docker: are merged over the service's env in Docker mode, which is how the example above swaps localhost:${postgres.port} for the in-network postgres:5432 address.

WARNING

docker is a reserved word on the command line (portler up docker) and cannot be used as a service name.

Prebuilt images ​

portler up builds a build: service's image every time. When the image already exists, for example because CI built and pushed it, pin it instead of building:

bash
portler up docker -d --image api=ghcr.io/org/api@sha256:3dc17e…

--image <service>=<image> replaces that service's image and drops its build, so nothing is built and Docker pulls the reference when it is not present locally. Everything else still comes from portler.yml: the command, env, ports, volumes, dependencies and healthchecks. The flag is repeatable, works with up, up docker, up k8s and restart, and can also pin a plain image: service to a digest. The service must run in Docker in the chosen mode, so a service whose only Docker configuration is a docker: block takes the flag under up docker, not under plain up. The target also has to be a service the command starts: restart api leaves an already-running dependency untouched, and plain up never starts over a running service, so pinning that dependency there is an error rather than a flag that changes nothing (restart it as well, or stop it first), and restart checks this before it stops anything.

This is how a server runs exactly what CI built, with no toolchain on the host: check out the commit the images were built from, so portler.yml matches, then start the stack from the pinned references. A digest reference is immutable, so the same command starts the same bytes every time. Docker pulls with the login of the user running Portler, so docker login to a private registry first. Under up k8s a pinned image is not loaded into the cluster; the cluster pulls it, so it must be reachable from there.

A pin is recorded in pids.json and replayed: a restart of that service without --image runs it from the same pinned reference, ps shows the pin in an IMAGE column, a new --image for the service replaces it, and down clears it. A pin that does not apply in the run mode a restart is given (the service is not a container there) is dropped with a notice.

up --prod still runs the setup: steps of portler.yml on the host, so a server with no toolchain adds --no-setup. That skips every setup step, including a migration declared there, so keep such steps in the image or run them separately.

Requirements & failure behavior ​

Docker services need the Docker daemon; when it is not running, commands that need it fail with a clear error. portler down stops and removes the project's containers.

Released under the MIT License.