Workflow examples
Complete GitHub Actions workflow files for AWS Lightsail and Hetzner Cloud.
Copy one of the workflows below into .github/workflows/pullpreview.yml in your repository. Adjust instance_type, region, compose_files, and proxy_tls to match your app.
Both workflows use the same structure:
on: schedule— runs every four hours to clean up closed and TTL-expired previews. It only does cleanup; it does not deploy or keep previews alive.on: pull_request— the primary trigger for opening, updating, and destroying previews.if:guard on the job — lets the job run onschedule, when thepullpreviewlabel is added or removed (github.event.label.name), and on any PR event while the label is present (contains(...)). This is what makes teardown on label removal and PR close/merge work.
The permissions block grants only what the action needs: read the repo code, write PR comments. PullPreview v6 is PR-driven, so there is no push trigger — see Migrating from v5 to v6 to keep a long-lived branch preview.
AWS Lightsail
The default provider. Instances land in the Lightsail region you specify via AWS_REGION.
name: PullPreviewon: schedule: - cron: "30 */4 * * *" pull_request: types: [labeled, unlabeled, synchronize, reopened, opened, closed]
permissions: contents: read pull-requests: write
jobs: deploy: if: github.event_name == 'schedule' || github.event.label.name == 'pullpreview' || contains(github.event.pull_request.labels.*.name, 'pullpreview') runs-on: ubuntu-latest timeout-minutes: 30 steps: - uses: actions/checkout@v5 - uses: pullpreview/action@v6 with: admins: "@collaborators/push" provider: lightsail app_path: . instance_type: nano default_port: 80 compose_files: docker-compose.yml proxy_tls: web:80 ttl: 1h env: AWS_ACCESS_KEY_ID: "${{ secrets.AWS_ACCESS_KEY_ID }}" AWS_SECRET_ACCESS_KEY: "${{ secrets.AWS_SECRET_ACCESS_KEY }}" AWS_REGION: "us-east-1"Required secrets: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY. For production, prefer OIDC short-lived credentials.
The AWS_REGION value is set inline — change it to the region nearest your team. Common choices: us-east-1, eu-west-1, ap-southeast-1.
instance_type: nano is the smallest, cheapest Lightsail bundle. Note the action’s default is small, so set instance_type explicitly if you want the cheapest size. Upgrade to micro, small, or medium if your app needs more resources.
Hetzner Cloud
An alternative to AWS. Hetzner is often cheaper for European teams. Instances land in the Hetzner location you specify via region.
name: PullPreviewon: schedule: - cron: "30 */4 * * *" pull_request: types: [labeled, unlabeled, synchronize, reopened, opened, closed]
permissions: contents: read pull-requests: write
jobs: deploy: if: github.event_name == 'schedule' || github.event.label.name == 'pullpreview' || contains(github.event.pull_request.labels.*.name, 'pullpreview') runs-on: ubuntu-latest timeout-minutes: 30 steps: - uses: actions/checkout@v5 - uses: pullpreview/action@v6 with: admins: "@collaborators/push" provider: hetzner app_path: . region: nbg1 image: ubuntu-24.04 instance_type: cpx22 default_port: 80 compose_files: docker-compose.yml proxy_tls: web:80 ttl: 1h env: HCLOUD_TOKEN: "${{ secrets.HCLOUD_TOKEN }}" HETZNER_CA_KEY: "${{ secrets.HETZNER_CA_KEY }}"Required secrets: HCLOUD_TOKEN, HETZNER_CA_KEY (see Providers for generating the CA key).
region: nbg1 is Nuremberg, Germany. Hetzner has several other locations (e.g. Falkenstein, Helsinki, and US sites) — check the current list in the Hetzner Cloud console ↗, as the value is passed through unchanged.
instance_type: cpx22 is the Hetzner default (and what small/micro normalize to). Pass any valid Hetzner server type, e.g. cpx11 for a smaller box or cpx31 for more resources.
Note that proxy_tls: web:80 here points at the internal port your web service listens on (Caddy terminates TLS in front of it) — it is not a provider-specific value. Adjust it to your service’s port.
Tips
- TTL.
ttl: 1hmarks the preview for teardown one hour after the PR was last updated; the scheduled run enforces it, so actual destruction can lag until the next scheduled run. Onlyh/dsuffixes are recognized. Setttl: infiniteto keep environments alive until the label is removed. - Multiple services. If your app exposes multiple ports, list them all:
ports: "80/tcp,8080/tcp,443/tcp". Usedefault_portto choose which one becomes the clickable URL (ignored whenproxy_tlsis set — the URL is then HTTPS on 443). - Private access. Set
cidrsto your office IP range to restrict who can reach the preview (port 22 stays open for SSH). - Custom runner. Replace
ubuntu-latestwithubuntu-slimfrom runs-on.com ↗ for faster, cheaper builds.
For more patterns, see Deployment targets (Helm), Multiple environments per PR, and HTTPS and custom domains.