v3.3.0 b164fe9

getting-started.md RawHistoryFolder

Getting Started

Deploy your first app with Gordon in under 5 minutes.

Prerequisites

  • A Linux VPS with Docker or Podman installed
  • A domain pointing to your VPS (DNS A record)
  • A public TCP edge address for Gordon (for example external :443, mapped to your chosen entrypoints.edge.address)
  • pass (password manager) with GPG key initialized

1. Install Gordon

curl -fsSL https://bnema.dev/gordon/install | bash

This script automatically detects your OS and architecture, downloads the appropriate binary, and installs it to /usr/local/bin.

2. Initialize Configuration

# First run creates the default config
gordon serve
# Press Ctrl+C to stop

Config is created at ~/.config/gordon/gordon.toml.

3. Set Up Authentication

Gordon requires a token_secret for JWT authentication. You can store it in pass or provide it via the GORDON_AUTH_TOKEN_SECRET environment variable. We recommend using pass to store secrets securely.

Local development? If you just want to try Gordon locally, you can disable auth temporarily:

[auth]
enabled = false

Skip to Step 4. For production, continue below.

Initialize pass (if not already done):

# Generate a GPG key if you don't have one
gpg --gen-key

# Initialize pass with your GPG key ID
pass init <your-gpg-key-id>

Create the JWT token secret:

# Generate and store a random 32-character secret
openssl rand -base64 32 | pass insert -m gordon/auth/token_secret

Or set it via environment variable:

export GORDON_AUTH_TOKEN_SECRET="your-32-character-secret-here"

Update your config (~/.config/gordon/gordon.toml):

[auth]
enabled = true
secrets_backend = "pass"
token_secret = "gordon/auth/token_secret"

4. Configure Your Gordon Domain

Edit ~/.config/gordon/gordon.toml:

[server]
registry_port = 5000                     # Registry port
gordon_domain = "gordon.mydomain.com"    # Registry + Admin API domain

[entrypoints.edge]
address = ":443"                         # Public smart TCP edge (choose your bind/mapping)
protocol = "smart_tcp"

Application workloads are NOT declared in gordon.toml. Each app lives in its own standalone TOML file (see step 8). The old [routes], [attachments], [network_groups], [service_routes], [auto_route], and [previews] keys were removed in v3 — Gordon refuses to start when any of them is present. Installation-level [[services]] for standalone L4 workloads remains valid.

5. Set Up DNS (Including Wildcard)

gordon_domain is the single domain used by both the registry and admin API.

In Cloudflare (or your DNS provider), create:

Type Name Content Proxy
A gordon YOUR_SERVER_IP Yes
A/CNAME * YOUR_SERVER_IP or gordon.mydomain.com Yes

Why wildcard (*)?

  • It automatically covers app hosts like app.mydomain.com, api.mydomain.com, demo.mydomain.com, etc.
  • You can add new app HTTP hosts without creating DNS records one by one.

If your DNS provider supports wildcard CNAME flattening (Cloudflare does), * -> gordon.mydomain.com is usually the cleanest option.

Important for Cloudflare or other proxied setups: when Gordon serves HTTP paths behind Cloudflare or another proxy, add your proxy edge IPs to server.proxy_allowed_ips or proxied HTTP traffic can get 403 Forbidden. See Installation.

6. Start Gordon as a Service

# Create systemd user service
mkdir -p ~/.config/systemd/user
cat > ~/.config/systemd/user/gordon.service <<EOF
[Unit]
Description=Gordon Container Platform

[Service]
Type=simple
Restart=always
ExecStart=/usr/local/bin/gordon serve

[Install]
WantedBy=default.target
EOF

# Enable and start
systemctl --user daemon-reload
systemctl --user enable --now gordon
sudo loginctl enable-linger $USER

7. Generate a Deploy Token

Create a token for remote CLI use (skip if auth is disabled):

gordon auth token generate --subject deploy --scopes "push,pull,admin:apps:read,admin:apps:write" --expiry 90d

--expiry 0 creates a non-expiring token. Prefer a finite expiry and a rotation policy unless you explicitly need a long-lived deploy token.

Save this token securely -- you will use it to authenticate with your Gordon server from CI/CD pipelines and remote CLI sessions.

8. Deploy Your First App

On your local machine:

# Save and select your Gordon remote (one-time)
gordon remotes add prod https://gordon.mydomain.com --token <your-token>
gordon remotes use prod

# Build and push the image (OCI transfer only, never deploys)
gordon images push myapp --build --remote prod

Write the app file (blog.toml). The file is intended for Git: it must never contain secret values.

name = "blog"

[services.web]
image = "gordon.mydomain.com/myapp:latest"

[[services.web.http]]
host = "app.mydomain.com"
port = 3000

Apply the manifest, then deploy:

gordon apps apply --file blog.toml --remote prod
gordon apps deploy blog --remote prod

What these commands do:

  • gordon images push builds, uploads, and stores the image. It never deploys.
  • gordon apps apply validates the manifest and persists it as desired state.
  • gordon apps deploy activates the accepted revision: runs preflight, withdraws the service from traffic, stops and removes the old container, starts the new container, waits for readiness, publishes the new ACTIVE state, and routes traffic to it.

If the app needs secrets, register their names in the manifest ([services.<name>.secrets] maps ENV name to secret name), then write values — values stay in pass, never in the file:

gordon apps secrets set blog --service web APP_ENV=production --remote prod

Your app is now live at https://app.mydomain.com!

9. Update Your App

Push a new image, update the manifest tag if needed, apply, deploy:

# Make changes, then build + push
gordon images push myapp --build --remote prod

# Deploy the new tag (re-resolves mutable tags)
gordon apps deploy blog --remote prod

Gordon deploys services one at a time in sorted order. A stateless TCP or HTTP service (no volume, bind, device, or UDP interface) is replaced without downtime: the new container starts and becomes ready while the old one keeps serving, then traffic switches and the old one is stopped. A service that owns a volume, bind, device, or UDP interface is replaced sequentially, may be briefly interrupted, and never has two generations running at once. If preflight fails, the running service is left untouched.

Next Steps

source
1# Getting Started
2
3Deploy your first app with Gordon in under 5 minutes.
4
5## Prerequisites
6
7- A Linux VPS with Docker or Podman installed
8- A domain pointing to your VPS (DNS A record)
9- A public TCP edge address for Gordon (for example external `:443`, mapped to your chosen `entrypoints.edge.address`)
10- [pass](https://www.passwordstore.org/) (password manager) with GPG key initialized
11
12## 1. Install Gordon
13
14```bash
15curl -fsSL https://bnema.dev/gordon/install | bash
16```
17
18This script automatically detects your OS and architecture, downloads the appropriate binary, and installs it to `/usr/local/bin`.
19
20## 2. Initialize Configuration
21
22```bash
23# First run creates the default config
24gordon serve
25# Press Ctrl+C to stop
26```
27
28Config is created at `~/.config/gordon/gordon.toml`.
29
30## 3. Set Up Authentication
31
32Gordon requires a `token_secret` for JWT authentication. You can store it in `pass` or provide it via the `GORDON_AUTH_TOKEN_SECRET` environment variable. We recommend using `pass` to store secrets securely.
33
34> **Local development?** If you just want to try Gordon locally, you can disable auth temporarily:
35> ```toml
36> [auth]
37> enabled = false
38> ```
39> Skip to [Step 4](#4-configure-your-gordon-domain). For production, continue below.
40
41**Initialize pass (if not already done):**
42```bash
43# Generate a GPG key if you don't have one
44gpg --gen-key
45
46# Initialize pass with your GPG key ID
47pass init <your-gpg-key-id>
48```
49
50**Create the JWT token secret:**
51```bash
52# Generate and store a random 32-character secret
53openssl rand -base64 32 | pass insert -m gordon/auth/token_secret
54```
55
56**Or set it via environment variable:**
57
58```bash
59export GORDON_AUTH_TOKEN_SECRET="your-32-character-secret-here"
60```
61
62**Update your config** (`~/.config/gordon/gordon.toml`):
63```toml
64[auth]
65enabled = true
66secrets_backend = "pass"
67token_secret = "gordon/auth/token_secret"
68```
69
70## 4. Configure Your Gordon Domain
71
72Edit `~/.config/gordon/gordon.toml`:
73
74```toml
75[server]
76registry_port = 5000                     # Registry port
77gordon_domain = "gordon.mydomain.com"    # Registry + Admin API domain
78
79[entrypoints.edge]
80address = ":443"                         # Public smart TCP edge (choose your bind/mapping)
81protocol = "smart_tcp"
82```
83
84Application workloads are NOT declared in `gordon.toml`. Each app lives in its own standalone TOML file (see step 8). The old `[routes]`, `[attachments]`, `[network_groups]`, `[service_routes]`, `[auto_route]`, and `[previews]` keys were removed in v3 — Gordon refuses to start when any of them is present. Installation-level `[[services]]` for standalone L4 workloads remains valid.
85
86## 5. Set Up DNS (Including Wildcard)
87
88`gordon_domain` is the single domain used by both the registry and admin API.
89
90In Cloudflare (or your DNS provider), create:
91
92| Type | Name | Content | Proxy |
93|------|------|---------|-------|
94| A | `gordon` | `YOUR_SERVER_IP` | Yes |
95| A/CNAME | `*` | `YOUR_SERVER_IP` or `gordon.mydomain.com` | Yes |
96
97Why wildcard (`*`)?
98- It automatically covers app hosts like `app.mydomain.com`, `api.mydomain.com`, `demo.mydomain.com`, etc.
99- You can add new app HTTP hosts without creating DNS records one by one.
100
101If your DNS provider supports wildcard CNAME flattening (Cloudflare does), `* -> gordon.mydomain.com` is usually the cleanest option.
102
103> **Important for Cloudflare or other proxied setups:** when Gordon serves HTTP paths behind Cloudflare or another proxy, add your proxy edge IPs to `server.proxy_allowed_ips` or proxied HTTP traffic can get `403 Forbidden`. See [Installation](./installation.md#proxy-origin-allowlist).
104
105## 6. Start Gordon as a Service
106
107```bash
108# Create systemd user service
109mkdir -p ~/.config/systemd/user
110cat > ~/.config/systemd/user/gordon.service <<EOF
111[Unit]
112Description=Gordon Container Platform
113
114[Service]
115Type=simple
116Restart=always
117ExecStart=/usr/local/bin/gordon serve
118
119[Install]
120WantedBy=default.target
121EOF
122
123# Enable and start
124systemctl --user daemon-reload
125systemctl --user enable --now gordon
126sudo loginctl enable-linger $USER
127```
128
129## 7. Generate a Deploy Token
130
131Create a token for remote CLI use (skip if auth is disabled):
132
133```bash
134gordon auth token generate --subject deploy --scopes "push,pull,admin:apps:read,admin:apps:write" --expiry 90d
135```
136
137`--expiry 0` creates a non-expiring token. Prefer a finite expiry and a rotation policy unless you explicitly need a long-lived deploy token.
138
139Save this token securely -- you will use it to authenticate with your Gordon server from CI/CD pipelines and remote CLI sessions.
140
141## 8. Deploy Your First App
142
143On your local machine:
144
145```bash
146# Save and select your Gordon remote (one-time)
147gordon remotes add prod https://gordon.mydomain.com --token <your-token>
148gordon remotes use prod
149
150# Build and push the image (OCI transfer only, never deploys)
151gordon images push myapp --build --remote prod
152```
153
154Write the app file (`blog.toml`). The file is intended for Git: it must never contain secret values.
155
156```toml
157name = "blog"
158
159[services.web]
160image = "gordon.mydomain.com/myapp:latest"
161
162[[services.web.http]]
163host = "app.mydomain.com"
164port = 3000
165```
166
167Apply the manifest, then deploy:
168
169```bash
170gordon apps apply --file blog.toml --remote prod
171gordon apps deploy blog --remote prod
172```
173
174What these commands do:
175
176- `gordon images push` builds, uploads, and stores the image. It never deploys.
177- `gordon apps apply` validates the manifest and persists it as desired state.
178- `gordon apps deploy` activates the accepted revision: runs preflight, withdraws the service from traffic, stops and removes the old container, starts the new container, waits for readiness, publishes the new `ACTIVE` state, and routes traffic to it.
179
180If the app needs secrets, register their names in the manifest (`[services.<name>.secrets]` maps ENV name to secret name), then write values — values stay in pass, never in the file:
181
182```bash
183gordon apps secrets set blog --service web APP_ENV=production --remote prod
184```
185
186Your app is now live at `https://app.mydomain.com`!
187
188## 9. Update Your App
189
190Push a new image, update the manifest tag if needed, apply, deploy:
191
192```bash
193# Make changes, then build + push
194gordon images push myapp --build --remote prod
195
196# Deploy the new tag (re-resolves mutable tags)
197gordon apps deploy blog --remote prod
198```
199
200Gordon deploys services one at a time in sorted order. A stateless TCP or HTTP service (no volume, bind, device, or UDP interface) is replaced without downtime: the new container starts and becomes ready while the old one keeps serving, then traffic switches and the old one is stopped. A service that owns a volume, bind, device, or UDP interface is replaced sequentially, may be briefly interrupted, and never has two generations running at once. If preflight fails, the running service is left untouched.
201
202## Next Steps
203
204- [Installation Guide](./installation.md) - Production setup with firewall and rootless containers
205- [Configuration Reference](./config/index.md) - All installation configuration options
206- [Apps CLI](./cli/apps.md) - Apply, deploy, and lifecycle commands
207- [Authentication](./config/auth.md) - Secure your registry
208- [App secrets](./cli/apps.md#gordon-apps-secrets) - Service-scoped secret values in pass
209
210## Related
211
212- [First Deploy Tutorial](/wiki/tutorials/first-deploy.md)
213- [Podman Rootless Setup](/wiki/guides/podman-rootless.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