deployer

The compose file

The deployer reads your compose file itself and turns it into systemd units for rootless Podman (Quadlet). It supports the parts that make sense on a single server and rejects the rest with a clear message, the YAML path and the line, so nothing is silently dropped. The repository check (setup step ④) shows the result before anything is built.

A typical project

services:
  app:
    build: .                        # ./Dockerfile
    environment:
      DATABASE_URL: ${DATABASE_URL} # set in the deployer UI (Variables)
      NODE_ENV: production
    depends_on:
      - redis

  worker:
    build: .
    command: ["node", "worker.js"]
    environment:
      DATABASE_URL: ${DATABASE_URL}

  redis:
    image: docker.io/library/redis:7

With [web] service = "app" and port = 8080 in deployer.toml: app gets the traffic and zero-downtime switches, worker is restarted on its new image after each successful switch, and redis keeps running across deploys.

Supported

KeyNotes
imageShort names are expanded Docker-style (redis:7 → docker.io/library/redis:7). Pinning a version beats :latest.
build (context, dockerfile, args, target)Built on this server. The context must stay inside the repository. Build args end up in the image: no secrets there.
environment, env_fileMerged; values you set in the deployer win over both.
${VAR}, ${VAR:-default}, ${VAR:?message}Filled from the project's variables, then from a .env next to the compose file. $$ is a literal $.
volumesNamed volumes (kept across deploys, not backed up), and read-only bind mounts of files inside the repository (./config:/etc/app). Absolute host paths are rejected.
depends_onIncluding service_healthy (needs a healthcheck) and service_completed_successfully (for one-off jobs).
healthcheckBecomes a container health check; a service with one counts as started only once healthy.
command, entrypoint, user, working_dir, hostname, init, read_only, tmpfs, shm_size, ulimits, stop_signal, stop_grace_period, extra_hosts, labels, cap_dropMapped one to one.
cap_addOnly a small allow-list (e.g. NET_BIND_SERVICE, CHOWN, SETUID, SETGID).
sysctlsOnly net.*.
mem_limit, cpus, pids_limit, deploy.resources.limitsCapped by the project's own limits.
networks (with aliases)Each project gets its own isolated network; services find each other by name.
restart, profiles, x-* fields, YAML anchorsSupported. Profiles are enabled in deployer.toml.

Ignored, with a warning

  • ports: nginx is the only public entry point; the deployer picks local ports itself.
  • container_name: the deployer names containers (two copies of the web service run during a switch).
  • expose, logging (logs always go to the server's journal), resource reservations.

Rejected

privileged, network_mode, host pid/ipc/uts/userns_mode, devices, volumes_from, links, secrets/configs (use the deployer's variables, optionally as files), extends, include, scale or more than one replica, security_opt, external volumes and networks, volume drivers, and any unknown key.

Good to know

  • The web service must listen on 0.0.0.0 inside the container and speak plain HTTP/1.1.
  • Containers run rootless; ports below 1024 inside the container are fine.
  • Containers may connect to external hosts (your managed database); services on this server's host network are not reachable from them.
  • All projects share one Podman user, and every name is prefixed with the project's name, so two projects can both have a service called app.