2026-03-12 | PreviewProof Team

How to Add Preview Environments to Your GitHub Actions Pipeline

GitHub Actionspreview environmentsCI/CDDockerDevOps

Most teams that want preview environments assume they need to adopt a new platform that owns their build and deployment process. The alternative — adding preview environments to your existing GitHub Actions pipeline — is simpler than it sounds and preserves the CI setup you already have. If your pipeline already builds Docker images, you’re one workflow step away from deploying a full-stack preview environment for every pull request.

The Integration Point Is After Your Build, Not Instead of It

The key architectural decision is where the preview platform sits in your pipeline. Platforms like Vercel and Netlify replace your build — they pull your source, compile it, and serve the output. Container-based preview environments work differently. They consume the artifacts your CI already produces.

Your existing workflow stays the same: checkout code, run tests, build Docker images, push to a registry. The preview deployment is a single step appended to the end:

name: CI
on:
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Your existing build steps — unchanged
- name: Build and push images
run: |
docker build -t ghcr.io/${{ github.repository }}/web:${{ github.sha }} ./web
docker build -t ghcr.io/${{ github.repository }}/api:${{ github.sha }} ./api
docker push ghcr.io/${{ github.repository }}/web:${{ github.sha }}
docker push ghcr.io/${{ github.repository }}/api:${{ github.sha }}
# One step to deploy the preview
- name: Deploy Preview Environment
uses: previewproof/deploy@v1
with:
services:
web:
image: ghcr.io/${{ github.repository }}/web:${{ github.sha }}
port: 3000
api:
image: ghcr.io/${{ github.repository }}/api:${{ github.sha }}
port: 8080
db:
image: postgres:16
env:
POSTGRES_DB: app

The preview platform pulls the images your CI just built and runs them together. It doesn’t clone your repo. It doesn’t run your build. It doesn’t need to know what language your application is written in or how your Dockerfile works.

What the Preview Environment Actually Runs

Each service defined in the deployment step becomes a running container in an isolated environment. The preview platform handles three things your CI doesn’t need to:

Service networking. The containers can reach each other by service name. Your web container talks to api on port 8080, and api talks to db on port 5432 — the same way they would in a Docker Compose setup or a Kubernetes pod.

Public routing. The preview platform assigns a unique URL and routes external traffic to the appropriate service. For a web application, this is typically the frontend service. The URL is deterministic — derived from the PR number or branch name — so it’s predictable and linkable from PR comments.

Lifecycle management. When the PR is updated, the preview redeploys with the new images. When the PR is closed or merged, the preview environment is torn down. There’s no cleanup step to add to your workflow — the platform manages the lifecycle based on PR state.

Handling Databases and Seed Data

The part that trips up most teams isn’t the application containers — it’s the database. A full-stack preview environment needs data to be useful. An empty database renders most features untestable.

There are two common patterns:

Migrate and seed on startup. Run migrations and seed data when the preview environment boots, either in the application entrypoint or as a separate init container that runs before the app starts.

# Option A: in the app entrypoint
#!/bin/sh
set -e
npx prisma migrate deploy
npx prisma db seed
node server.js
# Option B: as a separate job in your workflow, after the
# preview environment is up but before you notify reviewers
- name: Initialize database
run: |
# Run against the preview environment's database
DATABASE_URL="${{ steps.deploy.outputs.database_url }}" \
npx prisma migrate deploy && npx prisma db seed

The entrypoint approach is simpler — one container handles everything. The separate job approach keeps your app container clean and lets you retry or debug migrations independently. Either way, the tradeoff is boot time. Migrations are usually fast, but seed scripts that generate realistic data with faker or insert hundreds of records can add 10-30 seconds to every startup.

Prebuilt database image. Instead of migrating and seeding at boot time, you bake the fully initialized database into a Docker image during CI. The image contains a PostgreSQL data directory with migrations applied and seed data loaded — the database is ready the moment the container starts, with no scripts to run.

# Build the seeded database image in CI
- name: Build seeded database
run: |
docker build -t ghcr.io/${{ github.repository }}/db:${{ github.sha }} ./db
docker push ghcr.io/${{ github.repository }}/db:${{ github.sha }}

Then reference it in your preview deployment like any other service:

db:
image: ghcr.io/${{ github.repository }}/db:${{ github.sha }}
port: 5432

This is the same pattern many teams already use for local development with Docker Compose — everyone pulls the same prebuilt database image so docker compose up gives you a working environment immediately. For a deeper look at seed data strategies, faker configuration, and which tables to populate, see Database Migrations and Seed Data for Ephemeral Preview Environments.

The right approach depends on what your previews are for. If they’re primarily for engineering review, migrations on startup are usually sufficient. If PMs or stakeholders need to test features against realistic data, prebuilt database images are worth the investment.

Connecting the Preview to Your PR

The preview URL is only useful if people know it exists. The deployment step should post the URL back to the pull request — either as a comment or as a deployment status.

GitHub’s deployments API is the cleanest integration. When the preview deploys, it registers a deployment with a preview environment and an environment_url pointing to the running preview. This shows up in the PR UI as a clickable deployment link — no comment noise, no bot spam.

- name: Deploy Preview Environment
uses: previewproof/deploy@v1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
# Posts deployment status to the PR automatically

PreviewProof handles this automatically — each deployment creates a GitHub deployment status with the preview URL, and attaches structured testing checklists so reviewers know what to verify, not just where to click.

The result is a PR workflow where every push produces a testable environment, linked directly from the PR, running the exact images your CI built. No separate platform to configure. No second build system to maintain. One step added to the pipeline you already have.