Skip to content

Kubernetes mode

portler up k8s runs the whole project in a local Kubernetes cluster. It targets local development clusters only: kind, minikube, and Docker Desktop Kubernetes.

bash
portler up k8s        # build, load, apply, port-forward
portler down k8s      # delete everything, stop port-forwards
portler k8s render    # only generate and print the manifests

What up k8s does

  1. Builds Docker images for services with a build config (same resolution as up docker: the service's docker: key wins over top-level image/build).
  2. Detects the cluster from the active kubectl context (kind-*, minikube, docker-desktop) and loads locally built images into it (kind load docker-image / minikube image load; Docker Desktop shares the host Docker daemon). No registry push is needed. Registry images like postgres:16-alpine are pulled by the cluster itself.
  3. Generates manifests into .portler/k8s/: a Namespace, plus a Deployment, a Service (when the service has a port), and a PersistentVolumeClaim (when k8s.volume is set) per service.
  4. Applies one dependency level at a time: a service's manifests are not applied until every dependency's pods have rolled out, stayed stable (ready with no new restarts for a few seconds), and answered through their port-forward — mirroring local and Docker startup ordering.
  5. Port-forwards each in-cluster Service to its Portler-assigned localhost port, so generated URLs and env values keep working exactly like local and Docker modes. The supervisor re-establishes the forward when a pod restart kills it (capped backoff, a few retries) and keeps running after up k8s -d exits. Before reporting the stack up, Portler re-probes every forwarded port over TCP and fails loudly if one refuses connections.

Namespace

Everything lives in a project-scoped namespace (portler-<project>-<hash>, override with a top-level k8s_namespace:), so portler down k8s can delete it all and stop the port-forwards.

down k8s waits (up to 60s) for the namespace to finish terminating, and up k8s waits out a still-terminating namespace instead of failing.

Per-service k8s: options

yaml
services:
  postgres:
    image: postgres:16-alpine
    port: 5432
    env:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: app
      POSTGRES_DB: app
    k8s:
      volume:
        size: 1Gi
        mount_path: /var/lib/postgresql/data

  api:
    docker:
      build:
        context: .
        dockerfile: apps/api/Dockerfile
    port: 4000
    port_env: PORT
    depends_on:
      - postgres
    k8s:
      replicas: 1
      env:
        DATABASE_URL: postgres://app:app@postgres:5432/app
OptionMeaning
replicasDeployment replica count (default 1)
envEnv overrides for in-cluster consumption; layered over the service's Docker-mode env
volumeCreates a PersistentVolumeClaim of the given size, mounted at mount_path
  • env: inside the cluster, services reach each other through Kubernetes DNS service names (postgres:5432), just like Docker network aliases in Docker mode. Host-facing URLs (backend.url, PORTLER_*) keep using the port-forwarded localhost ports.
  • volume: mount_path defaults to the container path of the first Docker volume, or /data. A plain string is shorthand for the size: volume: 1Gi.
  • k8s: true is shorthand for the defaults (replicas: 1, no overrides).

Ports and readiness

As with Docker services, port: is the internal container port; the in-cluster Service exposes it and Portler forwards a dynamic localhost port to it.

command healthchecks are ignored in Kubernetes mode (they usually shell into Docker containers); readiness uses the Deployment rollout plus a TCP check through the port-forward.

WARNING

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

Released under the MIT License.