All posts
expresspreview-environmentstutorial

Express.js Preview Environments in Under 5 Minutes

PreviewDrop Team·August 24, 2026·6 min read

You push a branch with a new /api/orders endpoint to your Express app. The PR comment shows a preview URL. You click it, hit the endpoint, and it returns a 502 — because the "preview" is a static build with no Node runtime and no database behind it.

That's the failure mode of frontend-only preview tools applied to a backend. Express is a long-running Node process. It listens on a port, holds a connection pool, and serves WebSockets. None of that works in a serverless function or a static host. To preview an Express app you need a real container running your actual code — not a mock.

Here's how to get that on every pull request, in under five minutes.

What an Express preview actually needs

An Express app is three things a static preview can't provide:

  1. A Node runtime. Your app.listen(PORT) call has to actually run. That means a process, not a build artifact.
  2. A database. Most Express apps connect to Postgres through pg or Prisma. A preview that can't reach a database will fail on the first route that queries it.
  3. Long-lived connections. If you use socket.io or Server-Sent Events, the preview has to keep sockets open. Serverless functions time out; a container doesn't.

The common workaround — pointing every preview at your shared staging database — breaks the moment two PRs run conflicting migrations. PR A adds a column, PR B's code doesn't expect it, and both reviewers see a half-migrated schema.

The manual way: a GitHub Action that builds a container

You can assemble this yourself with a GitHub Action. The shape is roughly:

name: preview
on:
  pull_request:
    types: [opened, synchronize, reopened, closed]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Build image
        run: docker build -t preview:${{ github.event.pull_request.head.sha }} .
      - name: Deploy container
        run: |
          # spin up a container, wire a database, post the URL,
          # and tear it down when the PR closes
          ./scripts/deploy-preview.sh ${{ github.event.pull_request.number }}

This works, but it's a lot of moving parts. You own the Dockerfile, the database provisioning, the teardown on PR close, the HTTPS certificate, and the cleanup when someone forgets to close a PR. Every one of those is a place a preview silently stops working.

What PreviewDrop does instead

PreviewDrop treats every branch as a full-stack environment. It detects your stack from the repository, builds your application, and gives it a live URL — no render.yaml, no vercel.json, no deploy script.

For an Express app, that means:

git push origin feature/orders-endpoint

# URL posted to your PR within ~60 seconds:
# https://prv-a1f3.previews.previewdrop.dev

PreviewDrop detects package.json with Express, runs your build, and starts node server.js on a managed port. Your app.listen(PORT) picks up the port PreviewDrop assigns. The container has a writable filesystem for the preview's lifetime, so file uploads and background jobs behave as they do locally.

Isolated database per branch

When PreviewDrop detects a database dependency, you set one DATABASE_URL per project. Each preview gets its own isolated database, so two PRs can run conflicting migrations simultaneously without stepping on each other:

PR #412 (feature/orders-endpoint)
  ├── App container: node server.js
  ├── Database: orders_endpoint_db (isolated)
  └── Migrations: prisma migrate deploy (applied automatically)

When the branch merges or closes, the preview and its database are torn down.

WebSockets and long-lived connections

Because your Express app runs in a container rather than a serverless function, socket.io and Server-Sent Events work without workarounds. The preview behaves like your local environment — because it is one, just hosted.

What you get on each plan

PreviewDrop charges per workspace, not per seat, and the preview lifetime varies by plan:

  • Free — 2 concurrent previews, 4-hour lifetime, 256MB container memory, up to 3 projects.
  • Starter ($19/mo) — 5 concurrent previews, 24-hour lifetime, 512MB, up to 10 projects, 3 team members, API access.
  • Pro ($79/mo) — 20 concurrent previews, 3-day lifetime, 1GB, unlimited projects, 10 team members, password-protected previews.
  • Team ($149/mo) — 40 concurrent previews, 7-day lifetime, 2GB, up to 25 team members.

The 4-hour lifetime on Free is enough to review a PR in a single sitting. If you need a preview to survive the weekend for client review, that's the 3-day lifetime on Pro or the 7-day lifetime on Team.

When the manual approach still makes sense

If your team already runs a mature container platform and you have the headcount to maintain a preview pipeline, a hand-rolled GitHub Action gives you full control. You decide the image, the networking, and the database strategy. That's a legitimate choice for teams that already own that infrastructure.

The trade-off is maintenance. Every preview tool you assemble yourself — the Dockerfile, the DB provisioning, the teardown, the certs — is code you own and debug. PreviewDrop removes that layer so the preview is the thing you think about, not the pipeline that builds it.

The bottom line

Express previews fail when they're treated as static builds. The app is a process with a database and long-lived connections — it needs a real runtime. PreviewDrop gives you that on every pull request: a container running your actual code, an isolated database per branch, and a URL posted to the PR. No render.yaml, no deploy script, no teardown job to maintain.

If you're wiring up Express previews by hand today, that's the part you can stop maintaining.

Start with 2 free concurrent previews →

Ready to give every branch a live URL?

Free tier — 2 concurrent previews, no credit card required.

Start free