Skip to content

Guides

CI/CD Preview Builds

Serve a pull request's build from your CI runner on a public HTTPS address, so reviewers open the running app without a staging server.

Summary

A CI job builds the app, starts it on the runner and opens a tunnel to it. Reviewers open the tunnel's address and use the running build. The address belongs to the tunnel, so it is known before the job starts, and the preview is up for as long as the job keeps running.

How it works#

1. The job builds the app and starts it on a local port of the runner.

2. The agent opens a tunnel from that port to the tunnel's public address.

3. The job posts the address on the pull request and stays alive while reviewers use it.

4. The job ends and the tunnel closes.

A tunnel has one address. Create one tunnel in the dashboard for previews and set its subdomain. In your CI settings, store the token as a secret named LOCALPORT_TOKEN and the address as a variable named PREVIEW_URL.

You need a Localport account and an HTTP tunnel created for this purpose.

GitHub Actions#

name: Preview
on:
  pull_request:
    types: [opened, synchronize]

concurrency:
  group: preview
  cancel-in-progress: true

jobs:
  preview:
    runs-on: ubuntu-latest
    permissions:
      pull-requests: write
    env:
      LOCALPORT_TOKEN: ${{ secrets.LOCALPORT_TOKEN }}
      PREVIEW_URL: ${{ vars.PREVIEW_URL }}
    steps:
      - uses: actions/checkout@v4

      - name: Build
        run: |
          npm ci
          npm run build

      - name: Start the app
        run: npm run preview -- --port 3000 &

      - name: Install the Localport agent
        run: curl -fsSL https://localport.io/install.sh | sh

      - name: Open the tunnel
        run: localport http 3000 --region eu &

      - name: Post the address
        uses: actions/github-script@v7
        with:
          script: |
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: 'Preview: ' + process.env.PREVIEW_URL
            })

      - name: Keep the preview up for 30 minutes
        run: sleep 1800

The agent reads the token from LOCALPORT_TOKEN, so it never appears on a command line. With no terminal attached it prints plain log lines, one per event.

The concurrency block lets one preview run at a time. A new push cancels the run before it and takes the address over.

GitLab CI#

preview:
  stage: test
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
  script:
    - npm ci && npm run build
    - npm run preview -- --port 3000 &
    - curl -fsSL https://localport.io/install.sh | sh
    - localport http 3000 --region eu &
    - echo "Preview at $PREVIEW_URL"
    - sleep 1800

Add LOCALPORT_TOKEN as a masked variable and PREVIEW_URL as a plain one in the project's CI/CD settings.

Several previews at once#

One tunnel serves one build at a time. When a second job connects with the same token, the newer one takes the address and the older preview stops.

To review several pull requests side by side, create one tunnel per slot and give each job its own token and address.

Limit who can open a preview#

A preview is public while the job runs. Add Basic Auth or an IP allowlist to the tunnel and only your reviewers can open it.

Frequently asked questions#

How long does a preview stay up?

For as long as the CI job runs. The examples hold the job for 30 minutes with sleep 1800. Change that number to suit your reviews, within your CI provider's job time limit.

Does every pull request get its own URL?

Each tunnel has one address. With one tunnel, the newest job holds it. Create several tunnels to keep several previews up at once.

How do I keep the token out of the logs?

Store it as a CI secret named LOCALPORT_TOKEN. The agent reads that variable, so the token is not passed as a flag and is not printed.

Does the agent need a terminal?

No. When its output is not a terminal the agent switches to plain log lines on its own. --noui forces the same mode.

Can a pipeline reach private devices instead of publishing a build?

Yes. That is Remote Access, and a job can authenticate with the token its CI platform mints, with no stored secret. See CI/CD with OIDC.

Is this a replacement for a staging environment?

It suits review and manual testing of one build. For load tests or an environment that stays up between jobs, deploy to a server.