v2.31.0-dev9 062dcae

preview.md RawHistoryFolder

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
source
1# Preview Environments
2
3Push a branch, get a URL. Preview environments give every branch or pull request its own isolated deployment — automatically torn down when it expires.
4
5Gordon 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.
6
7## Configuration
8
9```toml
10[auto.preview]
11enabled = true
12ttl = "48h"
13separator = "--"
14tag_patterns = ["preview-*", "pr-*"]
15data_copy = true
16env_copy = true
17```
18
19## Options
20
21| Option | Type | Default | Description |
22|--------|------|---------|-------------|
23| `enabled` | bool | `false` | Enable automatic preview environment creation |
24| `ttl` | duration string | `"48h"` | How long a preview environment lives before automatic teardown |
25| `separator` | string | `"--"` | String inserted between the base domain and the branch slug |
26| `tag_patterns` | string array | `[]` | Glob patterns matched against image tags to trigger preview creation |
27| `data_copy` | bool | `true` | Clone the production route's volumes into the preview environment on creation |
28| `env_copy` | bool | `true` | Inherit environment variables from the base route's secret store into the preview container |
29
30### `ttl` Format
31
32The `ttl` field accepts Go duration strings:
33
34| Value | Meaning |
35|-------|---------|
36| `"24h"` | 24 hours |
37| `"48h"` | 48 hours (default) |
38| `"168h"` | 7 days |
39| `"0"` | Never expire (manual deletion only) |
40
41### `tag_patterns` Matching
42
43Patterns use standard glob syntax. A tag must match at least one pattern in the list for a preview environment to be created.
44
45| Pattern | Matches |
46|---------|---------|
47| `"preview-*"` | `preview-my-feature`, `preview-fix-123` |
48| `"pr-*"` | `pr-42`, `pr-123` |
49| `"feat/*"` | `feat/new-api`, `feat/dark-mode` |
50
51## Naming Scheme
52
53Preview 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.
54
55```text
56<app><separator><preview-name><rest-of-domain>
57```
58
59For example, with `separator = "--"` and base route `myapp.example.com`:
60
61| Image Tag | Preview Domain |
62|-----------|----------------|
63| `preview-my-feature` | `myapp--my-feature.example.com` |
64| `pr-42` | `myapp--42.example.com` |
65| `preview-fix-login` | `myapp--fix-login.example.com` |
66
67The tag prefix matched by `tag_patterns` is stripped to keep domains short. Slashes in tags are replaced with hyphens.
68
69## CLI Usage
70
71### List Preview Environments
72
73```bash
74gordon preview list
75```
76
77Shows all active preview environments, their domains, TTL remaining, and source route.
78
79### Create or Refresh a Preview
80
81Previews are created automatically on push. To manually create or reset the TTL of an existing preview:
82
83```bash
84gordon preview create app.example.com --tag preview-my-feature
85```
86
87### Extend a Preview
88
89Reset the TTL on an existing preview without redeploying:
90
91```bash
92gordon preview extend myapp--my-feature.example.com
93gordon preview extend myapp--my-feature.example.com --ttl 24h
94```
95
96Without `--ttl`, the configured default TTL is applied from the current time.
97
98### Delete a Preview
99
100```bash
101gordon preview delete myapp--my-feature.example.com
102```
103
104Stops the container, removes the route, and (unless `volumes.preserve = true`) removes any cloned volumes.
105
106## CI Usage
107
108### GitHub Actions
109
110Use tag-based deploys to trigger preview environments automatically.
111
112```yaml
113name: Preview Environment
114
115on:
116  pull_request:
117    types: [opened, synchronize]
118
119jobs:
120  deploy-preview:
121    runs-on: ubuntu-latest
122    steps:
123      - uses: actions/checkout@v4
124
125      - name: Log in to Gordon registry
126        run: echo "${{ secrets.GORDON_TOKEN }}" | docker login ${{ vars.GORDON_REGISTRY }} -u deploy --password-stdin
127
128      - name: Build and push preview image
129        env:
130          TAG: pr-${{ github.event.pull_request.number }}
131        run: |
132          docker build -t ${{ vars.GORDON_REGISTRY }}/myapp:$TAG .
133          docker push ${{ vars.GORDON_REGISTRY }}/myapp:$TAG
134
135      - name: Comment preview URL
136        uses: actions/github-script@v7
137        with:
138          script: |
139            github.rest.issues.createComment({
140              issue_number: context.issue.number,
141              owner: context.repo.owner,
142              repo: context.repo.repo,
143              body: `Preview deployed: https://app--${{ github.event.pull_request.number }}.example.com`
144            })
145```
146
147Configure Gordon to match the `pr-*` tag convention:
148
149```toml
150[auto.preview]
151enabled = true
152ttl = "72h"
153tag_patterns = ["pr-*"]
154```
155
156### Cleanup on PR Close
157
158```yaml
159name: Teardown Preview
160
161on:
162  pull_request:
163    types: [closed]
164
165jobs:
166  teardown:
167    runs-on: ubuntu-latest
168    steps:
169      - name: Delete preview environment
170        env:
171          PR_NUMBER: ${{ github.event.pull_request.number }}
172        run: |
173          gordon --server ${{ vars.GORDON_SERVER }} preview delete app--${PR_NUMBER}.example.com
174```
175
176## Lifecycle
177
178### Creation
179
180When a push matches a `tag_patterns` entry:
181
1821. Gordon creates a new route: `<app><separator><preview-name><rest-of-domain>` → pushed image
1832. If `data_copy = true`, volumes from the base route are cloned into the preview
1843. If `env_copy = true`, environment variables from the base route's secret store are loaded and passed to the preview container (without persisting them)
1854. The preview TTL timer starts
1865. The container is deployed with zero-downtime rules disabled (previews are always cold-starts)
187
188### TTL and Automatic Teardown
189
190Gordon checks preview TTLs on a background schedule. When a preview expires:
191
1921. In-flight connections are drained
1932. The container is stopped and removed
1943. The route is removed from the proxy
1954. Cloned volumes are removed (unless `volumes.preserve = true`)
196
197Use `gordon preview extend` to reset the timer without redeploying.
198
199### Volume Cloning
200
201When `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.
202
203- Cloned volumes are prefixed with the preview slug
204- Cloned volumes are removed on preview teardown unless `volumes.preserve = true`
205- Set `data_copy = false` to start previews with empty volumes (faster creation, no production data)
206
207### Environment Variable Inheritance
208
209Preview containers receive environment variables from three sources, merged in this order (last wins):
210
2111. **Dockerfile `ENV`** — baked into the image at build time
2122. **Base route secrets** — inherited from the production domain's secret store on first deploy (when `env_copy = true`)
2133. **Preview domain secrets** — set directly on the preview domain, picked up on redeploy
214
215On 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.
216
217#### Overriding variables for a preview
218
219To change a variable for a specific preview, set it on the preview domain:
220
221```bash
222gordon secrets set gordon--pr42.example.com API_URL=https://staging-api.example.com
223```
224
225This 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.
226
227#### Disabling env inheritance
228
229To start previews with a clean environment (Dockerfile `ENV` only):
230
231```toml
232[auto.preview]
233env_copy = false
234```
235
236## Example Config
237
238```toml
239[server]
240gordon_domain = "registry.example.com"
241
242[auto_route]
243enabled = true
244
245[auto.preview]
246enabled = true
247ttl = "48h"
248separator = "--"
249tag_patterns = ["preview-*", "pr-*"]
250data_copy = true
251env_copy = true
252
253[routes]
254"app.example.com" = "myapp:latest"
255"api.example.com" = "myapi:latest"
256
257[volumes]
258auto_create = true
259preserve = false
260```
261
262With this config:
263- Pushing `myapp:pr-99` creates `myapp--99.example.com` with cloned volumes and inherited env vars
264- Pushing `myapi:preview-auth-refactor` creates `myapi--auth-refactor.example.com`
265- Both previews expire after 48 hours and volumes are removed on teardown
266- Env vars are loaded at deploy time from the base route's secret store and are not persisted for the preview domain
267
268## Related
269
270- [Routes](./routes.md)
271- [Auto Route](./auto-route.md)
272- [Volumes](./volumes.md)
273- [Configuration Overview](./index.md)

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close