Preview Environments

Push a branch, get a URL. Preview environments give every branch or pull request its own isolated deployment — automatically torn down when it expires.

Gordon provisions a preview environment when a matching image tag is pushed to the registry. Each preview gets its own subdomain derived from the base route, lives for a configurable TTL, and is cleaned up automatically when the TTL expires.

Configuration

[auto.preview]
enabled = true
ttl = "48h"
separator = "--"
tag_patterns = ["preview-*", "pr-*"]
data_copy = true
env_copy = true

Options

Option Type Default Description
enabled bool false Enable automatic preview environment creation
ttl duration string "48h" How long a preview environment lives before automatic teardown
separator string "--" String inserted between the base domain and the branch slug
tag_patterns string array [] Glob patterns matched against image tags to trigger preview creation
data_copy bool true Clone the production route's volumes into the preview environment on creation
env_copy bool true Inherit environment variables from the base route's secret store into the preview container

ttl Format

The ttl field accepts Go duration strings:

Value Meaning
"24h" 24 hours
"48h" 48 hours (default)
"168h" 7 days
"0" Never expire (manual deletion only)

tag_patterns Matching

Patterns use standard glob syntax. A tag must match at least one pattern in the list for a preview environment to be created.

Pattern Matches
"preview-*" preview-my-feature, preview-fix-123
"pr-*" pr-42, pr-123
"feat/*" feat/new-api, feat/dark-mode

Naming Scheme

Preview domains are derived from the base route for the deployed image. Gordon combines the base domain, the separator, and a URL-safe slug of the image tag.

<app><separator><preview-name><rest-of-domain>

For example, with separator = "--" and base route myapp.example.com:

Image Tag Preview Domain
preview-my-feature myapp--my-feature.example.com
pr-42 myapp--42.example.com
preview-fix-login myapp--fix-login.example.com

The tag prefix matched by tag_patterns is stripped to keep domains short. Slashes in tags are replaced with hyphens.

CLI Usage

List Preview Environments

gordon preview list

Shows all active preview environments, their domains, TTL remaining, and source route.

Create or Refresh a Preview

Previews are created automatically on push. To manually create or reset the TTL of an existing preview:

gordon preview create app.example.com --tag preview-my-feature

Extend a Preview

Reset the TTL on an existing preview without redeploying:

gordon preview extend myapp--my-feature.example.com
gordon preview extend myapp--my-feature.example.com --ttl 24h

Without --ttl, the configured default TTL is applied from the current time.

Delete a Preview

gordon preview delete myapp--my-feature.example.com

Stops the container, removes the route, and (unless volumes.preserve = true) removes any cloned volumes.

CI Usage

GitHub Actions

Use tag-based deploys to trigger preview environments automatically.

name: Preview Environment

on:
  pull_request:
    types: [opened, synchronize]

jobs:
  deploy-preview:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Log in to Gordon registry
        run: echo "${{ secrets.GORDON_TOKEN }}" | docker login ${{ vars.GORDON_REGISTRY }} -u deploy --password-stdin

      - name: Build and push preview image
        env:
          TAG: pr-${{ github.event.pull_request.number }}
        run: |
          docker build -t ${{ vars.GORDON_REGISTRY }}/myapp:$TAG .
          docker push ${{ vars.GORDON_REGISTRY }}/myapp:$TAG

      - name: Comment preview URL
        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 deployed: https://app--${{ github.event.pull_request.number }}.example.com`
            })

Configure Gordon to match the pr-* tag convention:

[auto.preview]
enabled = true
ttl = "72h"
tag_patterns = ["pr-*"]

Cleanup on PR Close

name: Teardown Preview

on:
  pull_request:
    types: [closed]

jobs:
  teardown:
    runs-on: ubuntu-latest
    steps:
      - name: Delete preview environment
        env:
          PR_NUMBER: ${{ github.event.pull_request.number }}
        run: |
          gordon --server ${{ vars.GORDON_SERVER }} preview delete app--${PR_NUMBER}.example.com

Lifecycle

Creation

When a push matches a tag_patterns entry:

  1. Gordon creates a new route: <app><separator><preview-name><rest-of-domain> → pushed image
  2. If data_copy = true, volumes from the base route are cloned into the preview
  3. If env_copy = true, environment variables from the base route's secret store are loaded and passed to the preview container (without persisting them)
  4. The preview TTL timer starts
  5. The container is deployed with zero-downtime rules disabled (previews are always cold-starts)

TTL and Automatic Teardown

Gordon checks preview TTLs on a background schedule. When a preview expires:

  1. In-flight connections are drained
  2. The container is stopped and removed
  3. The route is removed from the proxy
  4. Cloned volumes are removed (unless volumes.preserve = true)

Use gordon preview extend to reset the timer without redeploying.

Volume Cloning

When data_copy = true, Gordon copies the named volumes attached to the base route into fresh volumes for the preview. This gives the preview a realistic dataset without sharing state with production.

  • Cloned volumes are prefixed with the preview slug
  • Cloned volumes are removed on preview teardown unless volumes.preserve = true
  • Set data_copy = false to start previews with empty volumes (faster creation, no production data)

Environment Variable Inheritance

Preview containers receive environment variables from three sources, merged in this order (last wins):

  1. Dockerfile ENV — baked into the image at build time
  2. Base route secrets — inherited from the production domain's secret store on first deploy (when env_copy = true)
  3. Preview domain secrets — set directly on the preview domain, picked up on redeploy

On initial creation, Gordon loads the base route's secrets and passes them to the container. The variables are not persisted under the preview domain — they are resolved at deploy time.

Overriding variables for a preview

To change a variable for a specific preview, set it on the preview domain:

gordon secrets set gordon--pr42.example.com API_URL=https://staging-api.example.com

This writes the secret to the preview domain's store and triggers an automatic redeploy (~60s debounce). On redeploy, the container picks up its own domain secrets instead of the inherited base route ones.

Disabling env inheritance

To start previews with a clean environment (Dockerfile ENV only):

[auto.preview]
env_copy = false

Example Config

[server]
gordon_domain = "registry.example.com"

[auto_route]
enabled = true

[auto.preview]
enabled = true
ttl = "48h"
separator = "--"
tag_patterns = ["preview-*", "pr-*"]
data_copy = true
env_copy = true

[routes]
"app.example.com" = "myapp:latest"
"api.example.com" = "myapi:latest"

[volumes]
auto_create = true
preserve = false

With this config:

  • Pushing myapp:pr-99 creates myapp--99.example.com with cloned volumes and inherited env vars
  • Pushing myapi:preview-auth-refactor creates myapi--auth-refactor.example.com
  • Both previews expire after 48 hours and volumes are removed on teardown
  • Env vars are loaded at deploy time from the base route's secret store and are not persisted for the preview domain