$ aoox

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_EMAIL if you want HTTPS.
  • A wildcard DNS record pointing to the server:
DNS
*.preview.example.com.   A   203.0.113.10

With 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

  1. 01PR openeda preview row is created, build from the PR branch
  2. 02runningcontainer <app>-pr<N> served at <app>-pr<N>.<domain>
  3. 03push to PRrebuilt, container replaced
  4. 04PR closed / mergedcontainer & row deleted
Example: application shop, PR #42, preview domain preview.example.com
container : aoox-app-shop-pr42
router    : shop-pr42
host      : https://shop-pr42.preview.example.com

Enabling it

  1. 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's PREVIEW_DOMAIN env var (if set). To set it once for every application, put PREVIEW_DOMAIN=preview.example.com in .env.dist and run docker compose up -d. An install created before this variable was forwarded to the API container won't receive it until aoox reinstall is run — aoox update panel doesn't rewrite the compose file.

  2. 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.

  3. 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 is building.

Events handled

ProviderCreates / refreshesDeletesIgnored
GitHub (pull_request)opened, synchronize, reopenedclosedPRs from a fork (head repo ≠ base repo)
GitLab (merge_request)open, update, reopenclose, mergeMRs 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

AspectPreview
Build method, Dockerfile path, build argsSame
Environment variables (+ project/database references)Same — including the production database if referenced
Health check pathSame; the preview deployment fails if it isn't healthy
CPU / memory limitsSame
Mounts (volume/bind/file)No — previews get no mounts
Host portNo — only reachable through Traefik
Application domainNo — has its own preview host
Blue/greenNo — a plain replace
Metrics & notificationsNot shown in the UI
Env is shared with production
Previews use the application's env as-is. If the env references ${{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 gets ignored until 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

SymptomCause & fix
Webhook 200 ignored: previews disabledThe toggle hasn't been turned on in the application's settings.
Preview host doesn't resolveThe wildcard DNS record is missing, or the preview domain is misspelled.
Certificate error / slow to issueACME 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 404The container isn't healthy yet, or the health check is failing — check the build/preview logs.
No preview appears for a given PRThe PR is from a fork, or there are already 5 open previews.
Not available yet
Previews for applications on remote servers; automatic PR comments with the preview link; preview-specific env.

Next steps

Found a mistake? Edit this page on GitHub ↗