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(orcompose.yml,docker-compose.yaml,docker-compose.yml; the first one found wins). What is supported. - A
Dockerfilefor every service withbuild:. 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 adeployer.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 /upanswering200once 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.
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.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.
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.
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.
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.Domains & DNS
Add the domain(s); the page lists the exact
A/AAAArecords to create. Check DNS verifies them from this server, and the certificate follows automatically.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.
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
curlwith 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
- Fetch the branch into a fresh checkout and plan it (as in the repository check).
- Build the images (
podman build, low priority, inside the project's limits). Images are tagged per release. - Release command (e.g. migrations) in the new image, if configured.
- Start the new web container next to the running one and health-check it.
- Switch nginx to the new container (a graceful reload), then watch it briefly. If it fails right after the switch, traffic goes back automatically.
- 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.
- 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
| Symptom | Cause and fix |
|---|---|
Permission denied (publickey) in the access test | The deploy key is not on this repository, or was added to a user account instead of the repository. |
| "no web service" in the repository check | Add [web] service/port to deployer.toml, or the deployer.expose label. |
| An unsupported compose key | The check names the key and its line. See supported keys; ports: and container_name: are ignored, not errors. |
| The health check times out | The 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 host | Allow your domain in the framework's host list: the check sends Host: <primary domain>. |
| "variable not set" during the deploy | Set it on the project's Variables page; the repository check lists every required one. |
| No certificate | Run Check DNS: the domain must resolve to this server and port 80 must be reachable from the internet. |
| Pushes do not deploy | Check the webhook page (verified? rejected deliveries?), the branch name, and whether the project is CI-only. |