2026-03-12 | PreviewProof Team
How to Add Preview Environments to Your GitHub Actions Pipeline
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: appThe 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/shset -enpx prisma migrate deploynpx prisma db seednode 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 seedThe 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: 5432This 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 automaticallyPreviewProof 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.