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 |
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 = 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):
- 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-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)