Build Preview Environments for Every Pull Request with the Sliplane API

Build Preview Environments for Every Pull Request with the Sliplane API

Jonas Scholz - Co-Founder von sliplane.ioJonas Scholz
7 min

Preview environments are one of those things that sound fancy but are surprisingly simple to set up. The idea: every pull request gets its own live URL, so reviewers can click around and test the actual running app instead of pulling the branch locally.

In this tutorial you'll build a GitHub Actions workflow that uses the Sliplane API to automatically create a preview environment when a PR is opened, post the URL as a comment, and tear it down when the PR is closed.

How it works

The workflow has two main jobs:

  1. PR opened → create a Sliplane service from the PR branch, wait for it to come up, post the URL as a PR comment
  2. PR closed → find the service by name and delete it

All of this happens through the Sliplane API. Sliplane gives every public service a managed domain out of the box, so you'll get a live URL immediately.

Prerequisites

  • A Sliplane account with a server and project already set up
  • Your app needs to be in a GitHub repository and deployable via Docker
  • Basic familiarity with GitHub Actions

Step 1: Create a Sliplane API Token

Go to your Sliplane team settings and create a new API token with read-write access. Copy it somewhere safe — you'll only see it once.

Step 2: Find Your Project and Server IDs

You'll need two IDs from Sliplane:

  • Project ID — visible in the URL when you open a project in the dashboard (e.g., project_abc123)
  • Server ID — visible in the URL when you open a server (e.g., server_xyz456)

You can also get them from the API:

# List your projects
curl https://ctrl.sliplane.io/v0/projects \
  -H "Authorization: Bearer YOUR_API_TOKEN"

# List servers in a project
curl https://ctrl.sliplane.io/v0/projects/PROJECT_ID/servers \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Step 3: Add Secrets to Your GitHub Repository

Go to your repository → SettingsSecrets and variablesActions and add:

SecretValue
SLIPLANE_API_TOKENYour Sliplane API token

Step 4: Create the Workflow

Create a file at .github/workflows/preview.yml in your repository:

.github/workflows/preview.yml
name: Preview Environment
on:
  pull_request:
    types: [opened, reopened, closed]

env:
  SLIPLANE_API: https://ctrl.sliplane.io/v0
  PROJECT_ID: your_project_id
  SERVER_ID: your_server_id

jobs:
  preview:
    runs-on: ubuntu-latest
    steps:
      - name: Create Preview
        if: github.event.action == 'opened' || github.event.action == 'reopened'
        id: create-preview
        run: |
          PREVIEW_NAME="preview-pr-${{ github.event.number }}"
          BRANCH="${{ github.head_ref }}"

          RESPONSE=$(curl -s -X POST "$SLIPLANE_API/projects/$PROJECT_ID/services" \
            -H "Authorization: Bearer ${{ secrets.SLIPLANE_API_TOKEN }}" \
            -H "Content-Type: application/json" \
            -d '{
              "name": "'"$PREVIEW_NAME"'",
              "serverId": "'"$SERVER_ID"'",
              "deployment": {
                "url": "https://github.com/${{ github.repository }}",
                "branch": "'"$BRANCH"'",
                "autoDeploy": true
              },
              "network": {
                "public": true,
                "protocol": "http"
              }
            }')

          echo "service_id=$(echo "$RESPONSE" | jq -r '.id')" >> $GITHUB_OUTPUT
          echo "domain=$(echo "$RESPONSE" | jq -r '.network.managedDomain')" >> $GITHUB_OUTPUT

      - name: Wait for Service to be Ready
        if: github.event.action == 'opened' || github.event.action == 'reopened'
        run: |
          SERVICE_ID="${{ steps.create-preview.outputs.service_id }}"

          for i in {1..60}; do
            STATUS=$(curl -s "$SLIPLANE_API/projects/$PROJECT_ID/services/$SERVICE_ID" \
              -H "Authorization: Bearer ${{ secrets.SLIPLANE_API_TOKEN }}" \
              | jq -r '.status')

            echo "Attempt $i: Status is $STATUS"

            if [ "$STATUS" = "live" ]; then
              echo "Service is ready!"
              break
            elif [ "$STATUS" = "failed" ]; then
              echo "Service failed to start"
              exit 1
            fi

            if [ $i -eq 60 ]; then
              echo "Timeout waiting for service to be ready"
              exit 1
            fi

            sleep 5
          done

      - name: Comment Preview URL
        if: github.event.action == 'opened' || github.event.action == 'reopened'
        uses: actions/github-script@v7
        with:
          script: |
            const url = 'https://${{ steps.create-preview.outputs.domain }}';
            const body = `Preview deployed at: ${url}`;

            const { data: comments } = await github.rest.issues.listComments({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: context.issue.number,
            });
            const botComment = comments.find(c => c.body.includes('Preview deployed at:'));

            if (botComment) {
              await github.rest.issues.updateComment({
                owner: context.repo.owner,
                repo: context.repo.repo,
                comment_id: botComment.id,
                body,
              });
            } else {
              await github.rest.issues.createComment({
                owner: context.repo.owner,
                repo: context.repo.repo,
                issue_number: context.issue.number,
                body,
              });
            }

      - name: Delete Preview
        if: github.event.action == 'closed'
        run: |
          SERVICE_ID=$(curl -s "$SLIPLANE_API/projects/$PROJECT_ID/services" \
            -H "Authorization: Bearer ${{ secrets.SLIPLANE_API_TOKEN }}" \
            | jq -r '.[] | select(.name == "preview-pr-${{ github.event.number }}") | .id')

          if [ -n "$SERVICE_ID" ]; then
            curl -X DELETE "$SLIPLANE_API/projects/$PROJECT_ID/services/$SERVICE_ID" \
              -H "Authorization: Bearer ${{ secrets.SLIPLANE_API_TOKEN }}"
          fi

Replace your_project_id and your_server_id in the env section with your actual IDs from Step 2.

What each part does

Create Preview — calls POST /v0/projects/{projectId}/services to spin up a new service from your PR branch. The autoDeploy: true flag means Sliplane will automatically redeploy whenever new commits are pushed to that branch. The response includes a managedDomain (e.g., preview-pr-42.sliplane.app) that we pass to the next steps.

You can also add environment variables or mount volumes to your preview service by including env and volumes fields in the service creation request. Check the API reference for the full schema.

Wait for Service — polls GET /v0/projects/{projectId}/services/{serviceId} every 5 seconds until the status is live. If it hits failed it exits immediately with an error, and after 5 minutes it times out.

Comment Preview URL — uses the GitHub API to post (or update) a comment on the PR with the live URL. Updating instead of creating a new comment keeps things tidy if the PR is closed and reopened.

Delete Preview — on PR close, lists all services in the project and finds the one matching the PR number by name, then calls DELETE /v0/projects/{projectId}/services/{serviceId} to remove it.

Step 5: Open a Pull Request

Push the workflow file to your main branch, then open a new PR. You should see the workflow kick off in the Actions tab and within a couple of minutes a bot comment like this will appear on the PR:

Preview deployed at: https://preview-pr-42.sliplane.app

When you merge or close the PR, the cleanup job runs and the service is deleted automatically.

Wrapping up

That's it — fully automatic preview environments with about 80 lines of YAML and the Sliplane API. Every reviewer gets a live URL to click through, and you don't have to think about cleanup.

The full Sliplane API reference has more you can do from here: environment variables per service, volume mounts, TCP/UDP services, and more. If you want custom domains on your previews instead of the managed ones, the API supports that too via POST /v0/projects/{projectId}/services/{serviceId}/domains.

Cheers,

Jonas

Welcome to the container cloud

Sliplane makes it simple to deploy containers in the cloud and scale up as you grow. Try it now and get started in minutes!