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:
- Gordon creates a new route:
<app><separator><preview-name><rest-of-domain>→ pushed image - If
data_copy = true, volumes from the base route are cloned into the preview - If
env_copy = true, environment variables from the base route's secret store are loaded and passed to the preview container (without persisting them) - The preview TTL timer starts
- 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:
- In-flight connections are drained
- The container is stopped and removed
- The route is removed from the proxy
- 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 = falseto 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):
- Dockerfile
ENV— baked into the image at build time - Base route secrets — inherited from the production domain's secret store on first deploy (when
env_copy = true) - 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-99createsmyapp--99.example.comwith cloned volumes and inherited env vars - Pushing
myapi:preview-auth-refactorcreatesmyapi--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