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
services:
redis:
image: redis:7-alpine
port: 6379Built services
Similar to Docker Compose — dockerfile is resolved relative to context:
services:
api:
build:
context: .
dockerfile: apps/api/Dockerfile
port: 4000
port_env: PORTbuild: also accepts a plain string (the context directory), plus target: for multi-stage builds and args: for build arguments:
build:
context: .
dockerfile: apps/api/Dockerfile
target: dev
args:
NODE_VERSION: 22A 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:
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:/dataare 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:
| Resource | Default name |
|---|---|
| Container | portler-<project>-<hash>-<service> |
| Image (built) | portler-<project>-<hash>-<service> |
| Network | portler-<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):
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/appdocker: 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:
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.