deployer

How to deploy

You bring a git repository with a Dockerfile and a compose file. The deployer builds it on this server, runs it with rootless Podman, puts nginx with HTTP/3 and a Let's Encrypt certificate in front, and deploys every change without downtime.

Before you start: the repository

  • A compose file at the root: compose.yaml (or compose.yml, docker-compose.yaml, docker-compose.yml; the first one found wins). What is supported.
  • A Dockerfile for every service with build:. Compose decides how to build (context, dockerfile, target); the deployer builds on this server with low priority.
  • One web service that speaks plain HTTP/1.1 inside its container and listens on 0.0.0.0. TLS, HTTP/2 and HTTP/3 are handled by nginx. Tell the deployer which service and port with a deployer.toml, or with one label on the service:
services:
  web:
    build: .
    labels:
      deployer.expose: "8080"       # this service, port 8080: no deployer.toml needed
  • A health endpoint, ideally GET /up answering 200 once the app can serve requests. Why it matters.
  • No secrets, domains or limits in the repository. Anyone who can push must not be able to claim a domain or raise limits; these live in the deployer.
  • Stateless apps. Keep databases on an external managed service. Named volumes work, but the deployer does not back them up.

Register the project: eight steps

Open New project. Every step shows ✓ or ✗ and stays on the project page until all are green.

  1. Repository

    Name (the slug, e.g. shop), repository URL, branch, and for self-hosted servers the provider. The deployer creates the project's directory and resource limits, an SSH deploy key, two local ports for zero-downtime switches and a webhook secret.

  2. Host key (self-hosted git servers only)

    Scan the server's SSH host key and compare the fingerprint with what its admin publishes. GitHub, GitLab.com and Bitbucket keys are built in.

  3. Deploy key & access

    Add the shown public key as a read-only deploy key of the repository (the page links to the right settings page), then click Test access. On failure you see git's exact error and the usual cause.

  4. Repository check

    The deployer fetches the branch and plans the deploy without building: compose file, web service and port, how each service is replaced, the health check, every rejected or ignored compose key with its line, and the variables the compose file needs.

  5. Environment variables

    Fill in the required variables and any others. Mark secrets as secret (write-only, redacted in logs), build-time secrets as build, multi-line values (keys, certificates) as file: those are mounted read-only at /run/secrets/NAME.

  6. Domains & DNS

    Add the domain(s); the page lists the exact A/AAAA records to create. Check DNS verifies them from this server, and the certificate follows automatically.

  7. Webhook

    Paste the URL and secret into your git provider (the page has instructions for GitHub, GitLab, Gitea, Forgejo and Bitbucket). It turns ✓ when the first correctly signed delivery arrives.

  8. First deploy

    Click Deploy now and watch the live log: fetch, build, release command, start, health check, switch.

How deploys happen afterwards

git push
A push to the configured branch reaches the webhook; the deployer re-reads the branch from git (it never trusts the payload) and deploys its tip. Pushes to other branches and tags are ignored. A burst of pushes results in at most one running and one queued deploy.
Deploy button
Deploy now on the project page builds and deploys the branch tip, also to pick up newer base images ([build] pull = "newer").
From CI
One curl with a project token after your tests pass. Set the project to CI only so that pushes alone never deploy. Details.
Variable changes
Saving a variable creates a new encrypted version; Apply now restarts the live release with it (no rebuild, still zero-downtime).
Rollback
The live release and the 5 previous ones are kept. Releases → Roll back activates an old build in seconds, with today's variables (the dialog lists what changed).

What a deploy does

  1. Fetch the branch into a fresh checkout and plan it (as in the repository check).
  2. Build the images (podman build, low priority, inside the project's limits). Images are tagged per release.
  3. Release command (e.g. migrations) in the new image, if configured.
  4. Start the new web container next to the running one and health-check it.
  5. Switch nginx to the new container (a graceful reload), then watch it briefly. If it fails right after the switch, traffic goes back automatically.
  6. Restart the other built services on their new images. Image-only services (databases, caches) keep running; only when their definition or environment changed are they restarted, before the new web container starts.
  7. The old web container drains its open requests and stops.

If anything fails before the switch, the old version simply keeps serving and the deploy page shows why.

Troubleshooting

SymptomCause and fix
Permission denied (publickey) in the access testThe deploy key is not on this repository, or was added to a user account instead of the repository.
"no web service" in the repository checkAdd [web] service/port to deployer.toml, or the deployer.expose label.
An unsupported compose keyThe check names the key and its line. See supported keys; ports: and container_name: are ignored, not errors.
The health check times outThe app must listen on 0.0.0.0 (not 127.0.0.1) on the configured port, and answer the health path within 1m (deploy_timeout).
The app rejects the health check's hostAllow your domain in the framework's host list: the check sends Host: <primary domain>.
"variable not set" during the deploySet it on the project's Variables page; the repository check lists every required one.
No certificateRun Check DNS: the domain must resolve to this server and port 80 must be reachable from the internet.
Pushes do not deployCheck the webhook page (verified? rejected deliveries?), the branch name, and whether the project is CI-only.