detached 9f0ec73

docs: add preview environments configuration guide

Documents the [auto.preview] config section, naming scheme, CLI commands, GitHub Actions CI example, and lifecycle (TTL, volume cloning, teardown).
commit
9f0ec73fe7b373dd8ac078d0f205fd3e31a3c776
author
brice · · 2026-03-19 05:12 UTC
committer
brice · · 2026-03-19 05:13 UTC
parent
d9532c9
signature
unsigned
2 files · +240 -0
A docs/config/preview.md +239 -0
@@ -0,0 +1,239 @@
1+# Preview Environments
2+
3+Push a branch, get a URL. Preview environments give every branch or pull request its own isolated deployment — automatically torn down when it expires.
4+
5+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.
6+
7+## Configuration
8+
9+```toml
10+[auto.preview]
11+enabled = true
12+ttl = "48h"
13+separator = "--"
14+tag_patterns = ["preview-*", "pr-*"]
15+data_copy = true
16+```
17+
18+## Options
19+
20+| Option | Type | Default | Description |
21+|--------|------|---------|-------------|
22+| `enabled` | bool | `false` | Enable automatic preview environment creation |
23+| `ttl` | duration string | `"48h"` | How long a preview environment lives before automatic teardown |
24+| `separator` | string | `"--"` | String inserted between the base domain and the branch slug |
25+| `tag_patterns` | string array | `[]` | Glob patterns matched against image tags to trigger preview creation |
26+| `data_copy` | bool | `true` | Clone the production route's volumes into the preview environment on creation |
27+
28+### `ttl` Format
29+
30+The `ttl` field accepts Go duration strings:
31+
32+| Value | Meaning |
33+|-------|---------|
34+| `"24h"` | 24 hours |
35+| `"48h"` | 48 hours (default) |
36+| `"7d"` | 7 days |
37+| `"0"` | Never expire (manual deletion only) |
38+
39+### `tag_patterns` Matching
40+
41+Patterns use standard glob syntax. A tag must match at least one pattern in the list for a preview environment to be created.
42+
43+| Pattern | Matches |
44+|---------|---------|
45+| `"preview-*"` | `preview-my-feature`, `preview-fix-123` |
46+| `"pr-*"` | `pr-42`, `pr-123` |
47+| `"feat/*"` | `feat/new-api`, `feat/dark-mode` |
48+
49+## Naming Scheme
50+
51+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.
52+
53+```
54+<base-domain><separator><tag-slug>
55+```
56+
57+For example, with `separator = "--"` and base route `app.example.com`:
58+
59+| Image Tag | Preview Domain |
60+|-----------|----------------|
61+| `preview-my-feature` | `my-feature--app.example.com` |
62+| `pr-42` | `pr-42--app.example.com` |
63+| `preview-fix-login` | `fix-login--app.example.com` |
64+
65+The tag prefix matched by `tag_patterns` is stripped to keep domains short. Slashes in tags are replaced with hyphens.
66+
67+## CLI Usage
68+
69+### List Preview Environments
70+
71+```bash
72+gordon preview list
73+```
74+
75+Shows all active preview environments, their domains, TTL remaining, and source route.
76+
77+### Create or Refresh a Preview
78+
79+Previews are created automatically on push. To manually create or reset the TTL of an existing preview:
80+
81+```bash
82+gordon preview create app.example.com --tag preview-my-feature
83+```
84+
85+### Extend a Preview
86+
87+Reset the TTL on an existing preview without redeploying:
88+
89+```bash
90+gordon preview extend my-feature--app.example.com
91+gordon preview extend my-feature--app.example.com --ttl 24h
92+```
93+
94+Without `--ttl`, the configured default TTL is applied from the current time.
95+
96+### Delete a Preview
97+
98+```bash
99+gordon preview delete my-feature--app.example.com
100+```
101+
102+Stops the container, removes the route, and (unless `volumes.preserve = true`) removes any cloned volumes.
103+
104+## CI Usage
105+
106+### GitHub Actions
107+
108+Use tag-based deploys to trigger preview environments automatically.
109+
110+```yaml
111+name: Preview Environment
112+
113+on:
114+ pull_request:
115+ types: [opened, synchronize]
116+
117+jobs:
118+ deploy-preview:
119+ runs-on: ubuntu-latest
120+ steps:
121+ - uses: actions/checkout@v4
122+
123+ - name: Log in to Gordon registry
124+ run: echo "${{ secrets.GORDON_TOKEN }}" | docker login ${{ vars.GORDON_REGISTRY }} -u deploy --password-stdin
125+
126+ - name: Build and push preview image
127+ env:
128+ TAG: pr-${{ github.event.pull_request.number }}
129+ run: |
130+ docker build -t ${{ vars.GORDON_REGISTRY }}/myapp:$TAG .
131+ docker push ${{ vars.GORDON_REGISTRY }}/myapp:$TAG
132+
133+ - name: Comment preview URL
134+ uses: actions/github-script@v7
135+ with:
136+ script: |
137+ github.rest.issues.createComment({
138+ issue_number: context.issue.number,
139+ owner: context.repo.owner,
140+ repo: context.repo.repo,
141+ body: `Preview deployed: https://pr-${{ github.event.pull_request.number }}--app.example.com`
142+ })
143+```
144+
145+Configure Gordon to match the `pr-*` tag convention:
146+
147+```toml
148+[auto.preview]
149+enabled = true
150+ttl = "72h"
151+tag_patterns = ["pr-*"]
152+```
153+
154+### Cleanup on PR Close
155+
156+```yaml
157+name: Teardown Preview
158+
159+on:
160+ pull_request:
161+ types: [closed]
162+
163+jobs:
164+ teardown:
165+ runs-on: ubuntu-latest
166+ steps:
167+ - name: Delete preview environment
168+ env:
169+ TAG: pr-${{ github.event.pull_request.number }}
170+ run: |
171+ gordon --server ${{ vars.GORDON_SERVER }} preview delete ${TAG}--app.example.com
172+```
173+
174+## Lifecycle
175+
176+### Creation
177+
178+When a push matches a `tag_patterns` entry:
179+
180+1. Gordon creates a new route: `<slug>--<base-domain>` → pushed image
181+2. If `data_copy = true`, volumes from the base route are cloned into the preview
182+3. The preview TTL timer starts
183+4. The container is deployed with zero-downtime rules disabled (previews are always cold-starts)
184+
185+### TTL and Automatic Teardown
186+
187+Gordon checks preview TTLs on a background schedule. When a preview expires:
188+
189+1. In-flight connections are drained
190+2. The container is stopped and removed
191+3. The route is removed from the proxy
192+4. Cloned volumes are removed (unless `volumes.preserve = true`)
193+
194+Use `gordon preview extend` to reset the timer without redeploying.
195+
196+### Volume Cloning
197+
198+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.
199+
200+- Cloned volumes are prefixed with the preview slug
201+- Cloned volumes are removed on preview teardown unless `volumes.preserve = true`
202+- Set `data_copy = false` to start previews with empty volumes (faster creation, no production data)
203+
204+## Example Config
205+
206+```toml
207+[server]
208+gordon_domain = "registry.example.com"
209+
210+[auto_route]
211+enabled = true
212+
213+[auto.preview]
214+enabled = true
215+ttl = "48h"
216+separator = "--"
217+tag_patterns = ["preview-*", "pr-*"]
218+data_copy = true
219+
220+[routes]
221+"app.example.com" = "myapp:latest"
222+"api.example.com" = "myapi:latest"
223+
224+[volumes]
225+auto_create = true
226+preserve = false
227+```
228+
229+With this config:
230+- Pushing `myapp:pr-99` creates `pr-99--app.example.com` with cloned volumes
231+- Pushing `myapi:preview-auth-refactor` creates `preview-auth-refactor--api.example.com`
232+- Both previews expire after 48 hours and volumes are removed on teardown
233+
234+## Related
235+
236+- [Routes](./routes.md)
237+- [Auto Route](./auto-route.md)
238+- [Volumes](./volumes.md)
239+- [Configuration Overview](./index.md)
@@ -0,0 +1,239 @@
1+# Preview Environments
2+
3+Push a branch, get a URL. Preview environments give every branch or pull request its own isolated deployment — automatically torn down when it expires.
4+
5+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.
6+
7+## Configuration
8+
9+```toml
10+[auto.preview]
11+enabled = true
12+ttl = "48h"
13+separator = "--"
14+tag_patterns = ["preview-*", "pr-*"]
15+data_copy = true
16+```
17+
18+## Options
19+
20+| Option | Type | Default | Description |
21+|--------|------|---------|-------------|
22+| `enabled` | bool | `false` | Enable automatic preview environment creation |
23+| `ttl` | duration string | `"48h"` | How long a preview environment lives before automatic teardown |
24+| `separator` | string | `"--"` | String inserted between the base domain and the branch slug |
25+| `tag_patterns` | string array | `[]` | Glob patterns matched against image tags to trigger preview creation |
26+| `data_copy` | bool | `true` | Clone the production route's volumes into the preview environment on creation |
27+
28+### `ttl` Format
29+
30+The `ttl` field accepts Go duration strings:
31+
32+| Value | Meaning |
33+|-------|---------|
34+| `"24h"` | 24 hours |
35+| `"48h"` | 48 hours (default) |
36+| `"7d"` | 7 days |
37+| `"0"` | Never expire (manual deletion only) |
38+
39+### `tag_patterns` Matching
40+
41+Patterns use standard glob syntax. A tag must match at least one pattern in the list for a preview environment to be created.
42+
43+| Pattern | Matches |
44+|---------|---------|
45+| `"preview-*"` | `preview-my-feature`, `preview-fix-123` |
46+| `"pr-*"` | `pr-42`, `pr-123` |
47+| `"feat/*"` | `feat/new-api`, `feat/dark-mode` |
48+
49+## Naming Scheme
50+
51+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.
52+
53+```
54+<base-domain><separator><tag-slug>
55+```
56+
57+For example, with `separator = "--"` and base route `app.example.com`:
58+
59+| Image Tag | Preview Domain |
60+|-----------|----------------|
61+| `preview-my-feature` | `my-feature--app.example.com` |
62+| `pr-42` | `pr-42--app.example.com` |
63+| `preview-fix-login` | `fix-login--app.example.com` |
64+
65+The tag prefix matched by `tag_patterns` is stripped to keep domains short. Slashes in tags are replaced with hyphens.
66+
67+## CLI Usage
68+
69+### List Preview Environments
70+
71+```bash
72+gordon preview list
73+```
74+
75+Shows all active preview environments, their domains, TTL remaining, and source route.
76+
77+### Create or Refresh a Preview
78+
79+Previews are created automatically on push. To manually create or reset the TTL of an existing preview:
80+
81+```bash
82+gordon preview create app.example.com --tag preview-my-feature
83+```
84+
85+### Extend a Preview
86+
87+Reset the TTL on an existing preview without redeploying:
88+
89+```bash
90+gordon preview extend my-feature--app.example.com
91+gordon preview extend my-feature--app.example.com --ttl 24h
92+```
93+
94+Without `--ttl`, the configured default TTL is applied from the current time.
95+
96+### Delete a Preview
97+
98+```bash
99+gordon preview delete my-feature--app.example.com
100+```
101+
102+Stops the container, removes the route, and (unless `volumes.preserve = true`) removes any cloned volumes.
103+
104+## CI Usage
105+
106+### GitHub Actions
107+
108+Use tag-based deploys to trigger preview environments automatically.
109+
110+```yaml
111+name: Preview Environment
112+
113+on:
114+ pull_request:
115+ types: [opened, synchronize]
116+
117+jobs:
118+ deploy-preview:
119+ runs-on: ubuntu-latest
120+ steps:
121+ - uses: actions/checkout@v4
122+
123+ - name: Log in to Gordon registry
124+ run: echo "${{ secrets.GORDON_TOKEN }}" | docker login ${{ vars.GORDON_REGISTRY }} -u deploy --password-stdin
125+
126+ - name: Build and push preview image
127+ env:
128+ TAG: pr-${{ github.event.pull_request.number }}
129+ run: |
130+ docker build -t ${{ vars.GORDON_REGISTRY }}/myapp:$TAG .
131+ docker push ${{ vars.GORDON_REGISTRY }}/myapp:$TAG
132+
133+ - name: Comment preview URL
134+ uses: actions/github-script@v7
135+ with:
136+ script: |
137+ github.rest.issues.createComment({
138+ issue_number: context.issue.number,
139+ owner: context.repo.owner,
140+ repo: context.repo.repo,
141+ body: `Preview deployed: https://pr-${{ github.event.pull_request.number }}--app.example.com`
142+ })
143+```
144+
145+Configure Gordon to match the `pr-*` tag convention:
146+
147+```toml
148+[auto.preview]
149+enabled = true
150+ttl = "72h"
151+tag_patterns = ["pr-*"]
152+```
153+
154+### Cleanup on PR Close
155+
156+```yaml
157+name: Teardown Preview
158+
159+on:
160+ pull_request:
161+ types: [closed]
162+
163+jobs:
164+ teardown:
165+ runs-on: ubuntu-latest
166+ steps:
167+ - name: Delete preview environment
168+ env:
169+ TAG: pr-${{ github.event.pull_request.number }}
170+ run: |
171+ gordon --server ${{ vars.GORDON_SERVER }} preview delete ${TAG}--app.example.com
172+```
173+
174+## Lifecycle
175+
176+### Creation
177+
178+When a push matches a `tag_patterns` entry:
179+
180+1. Gordon creates a new route: `<slug>--<base-domain>` → pushed image
181+2. If `data_copy = true`, volumes from the base route are cloned into the preview
182+3. The preview TTL timer starts
183+4. The container is deployed with zero-downtime rules disabled (previews are always cold-starts)
184+
185+### TTL and Automatic Teardown
186+
187+Gordon checks preview TTLs on a background schedule. When a preview expires:
188+
189+1. In-flight connections are drained
190+2. The container is stopped and removed
191+3. The route is removed from the proxy
192+4. Cloned volumes are removed (unless `volumes.preserve = true`)
193+
194+Use `gordon preview extend` to reset the timer without redeploying.
195+
196+### Volume Cloning
197+
198+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.
199+
200+- Cloned volumes are prefixed with the preview slug
201+- Cloned volumes are removed on preview teardown unless `volumes.preserve = true`
202+- Set `data_copy = false` to start previews with empty volumes (faster creation, no production data)
203+
204+## Example Config
205+
206+```toml
207+[server]
208+gordon_domain = "registry.example.com"
209+
210+[auto_route]
211+enabled = true
212+
213+[auto.preview]
214+enabled = true
215+ttl = "48h"
216+separator = "--"
217+tag_patterns = ["preview-*", "pr-*"]
218+data_copy = true
219+
220+[routes]
221+"app.example.com" = "myapp:latest"
222+"api.example.com" = "myapi:latest"
223+
224+[volumes]
225+auto_create = true
226+preserve = false
227+```
228+
229+With this config:
230+- Pushing `myapp:pr-99` creates `pr-99--app.example.com` with cloned volumes
231+- Pushing `myapi:preview-auth-refactor` creates `preview-auth-refactor--api.example.com`
232+- Both previews expire after 48 hours and volumes are removed on teardown
233+
234+## Related
235+
236+- [Routes](./routes.md)
237+- [Auto Route](./auto-route.md)
238+- [Volumes](./volumes.md)
239+- [Configuration Overview](./index.md)
M docs/index.md +1 -0
@@ -50,6 +50,7 @@ Gordon runs on your VPS and provides:
5050 - [Authentication](./config/auth.md) - Registry auth plus remote CLI login/token workflows
5151 - [Secrets](./config/secrets.md) - Secure credential storage
5252 - [Network Isolation](./config/network-isolation.md) - Per-app network isolation
53+- [Preview Environments](./config/preview.md) - Ephemeral per-branch deployments
5354 - [Attachments](./config/attachments.md) - Service dependencies
5455 - [Logging](./config/logging.md) - Log collection and rotation
5556 - [Environment Variables](./config/env.md) - Per-route environment configuration
@@ -50,6 +50,7 @@ Gordon runs on your VPS and provides:
50 - [Authentication](./config/auth.md) - Registry auth plus remote CLI login/token workflows50 - [Authentication](./config/auth.md) - Registry auth plus remote CLI login/token workflows
51 - [Secrets](./config/secrets.md) - Secure credential storage51 - [Secrets](./config/secrets.md) - Secure credential storage
52 - [Network Isolation](./config/network-isolation.md) - Per-app network isolation52 - [Network Isolation](./config/network-isolation.md) - Per-app network isolation
53+- [Preview Environments](./config/preview.md) - Ephemeral per-branch deployments
53 - [Attachments](./config/attachments.md) - Service dependencies54 - [Attachments](./config/attachments.md) - Service dependencies
54 - [Logging](./config/logging.md) - Log collection and rotation55 - [Logging](./config/logging.md) - Log collection and rotation
55 - [Environment Variables](./config/env.md) - Per-route environment configuration56 - [Environment Variables](./config/env.md) - Per-route environment configuration

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