DocsApplications
Pull request previews
Every pull request gets its own container and subdomain, created when the PR opens and removed when it closes.
- Per PR
- One container + one subdomain
- Lifetime
- From PR opened to closed/merged
- Default
- Off — opt-in per application
Prerequisites
- The application's webhook is registered with the Pull requests (GitHub) or Merge request events (GitLab) event, in addition to push.
- The reverse proxy is running with
PROXY_ACME_EMAILif you want HTTPS. - A wildcard DNS record pointing to the server:
*.preview.example.com. A 203.0.113.10With a Cloudflare connection and DNS-01 on the proxy you can also use a single wildcard certificate for all previews — see Cloudflare DNS. You still create this wildcard DNS record yourself.
- The application runs on the aoox host (not a remote server).
Lifecycle
- 01PR openeda preview row is created, build from the PR branch
- 02runningcontainer <app>-pr<N> served at <app>-pr<N>.<domain>
- 03push to PRrebuilt, container replaced
- 04PR closed / mergedcontainer & row deleted
container : aoox-app-shop-pr42
router : shop-pr42
host : https://shop-pr42.preview.example.comEnabling it
Application settings → Pull request previews
Turn on the toggle and fill in the Preview domain, e.g.
preview.example.com. Leaving it blank falls back to the API'sPREVIEW_DOMAINenv var (if set). To set it once for every application, putPREVIEW_DOMAIN=preview.example.comin.env.distand rundocker compose up -d. An install created before this variable was forwarded to the API container won't receive it untilaoox reinstallis run —aoox update paneldoesn't rewrite the compose file.Open a pull request at the provider
The webhook receives the event, and aoox builds the PR branch the same way as a normal build (Dockerfile/Nixpacks) with a preview-specific tag.
Watch it under the Webhook tab → Pull request previews
The list of previews shows: PR number, status (
building→running|failed), host + an open link, build logs, and a manual delete button. The list refreshes every 5 seconds while anything isbuilding.
Events handled
| Provider | Creates / refreshes | Deletes | Ignored |
|---|---|---|---|
| GitHub (pull_request) | opened, synchronize, reopened | closed | PRs from a fork (head repo ≠ base repo) |
| GitLab (merge_request) | open, update, reopen | close, merge | MRs from a fork (source project ≠ target project) |
The webhook response for these events: preview, preview-closed, or ignored with a reason (previews disabled, fork, preview limit).
What's inherited from the application
| Aspect | Preview |
|---|---|
| Build method, Dockerfile path, build args | Same |
| Environment variables (+ project/database references) | Same — including the production database if referenced |
| Health check path | Same; the preview deployment fails if it isn't healthy |
| CPU / memory limits | Same |
| Mounts (volume/bind/file) | No — previews get no mounts |
| Host port | No — only reachable through Traefik |
| Application domain | No — has its own preview host |
| Blue/green | No — a plain replace |
| Metrics & notifications | Not shown in the UI |
${{database.<slug>.url}}, the PR branch writes to the same database as production. For isolation, create a separate database and use a different env, or don't enable previews on applications that modify data.Security rules
- Opt-in per application (off by default) — a PR branch is arbitrary code that will run on your server.
- PRs from a fork are always ignored, even with previews enabled.
- A maximum of 5 open previews per application (
PREVIEW_MAX); the sixth PR getsignoreduntil one is closed. - A PR closed while its build is still running doesn't leave a container behind — it's re-checked after the build and after start.
- Deleting the application also deletes all of its PR preview containers (on a remote server too).
Common pitfalls
| Symptom | Cause & fix |
|---|---|
| Webhook 200 ignored: previews disabled | The toggle hasn't been turned on in the application's settings. |
| Preview host doesn't resolve | The wildcard DNS record is missing, or the preview domain is misspelled. |
| Certificate error / slow to issue | ACME per host is requested on first access. For many PRs, use PROXY_ACME_STAGING=true first to avoid hitting the rate limit. |
| Preview is running but Traefik returns 404 | The container isn't healthy yet, or the health check is failing — check the build/preview logs. |
| No preview appears for a given PR | The PR is from a fork, or there are already 5 open previews. |
Next steps
Found a mistake? Edit this page on GitHub ↗