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 on schedule, when the pullpreview label 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: PullPreview
on:
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: PullPreview
on:
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: 1h marks 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. Only h/d suffixes are recognized. Set ttl: infinite to 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". Use default_port to choose which one becomes the clickable URL (ignored when proxy_tls is set — the URL is then HTTPS on 443).
  • Private access. Set cidrs to your office IP range to restrict who can reach the preview (port 22 stays open for SSH).
  • Custom runner. Replace ubuntu-latest with ubuntu-slim from runs-on.com for faster, cheaper builds.

For more patterns, see Deployment targets (Helm), Multiple environments per PR, and HTTPS and custom domains.