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)
- HTTPS setup: Cloudflare proxy, Tailscale, or Gordon's internal CA (
server.tls_port = 8443)
- pass (password manager) with GPG key initialized
1. Install Gordon
curl -fsSL https://gordon.bnema.dev/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]
port = 8088 # HTTP proxy port (default)
registry_port = 5000 # Registry port
tls_port = 8443 # HTTPS listener (default)
gordon_domain = "gordon.mydomain.com" # Registry + Admin API domain
[routes]
"app.mydomain.com" = "myapp:latest" # Domain → Image mapping
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 routes like
app.mydomain.com, api.mydomain.com, demo.mydomain.com, etc.
- You can add new domains in
[routes] 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's internal CA is enabled (default tls_port = 8443), add your proxy edge IPs to server.proxy_allowed_ips or proxied HTTP traffic will get 403 Forbidden. See Installation. If you do not want Gordon to manage TLS at all, set tls_port = 0.
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 deploys (skip if auth is disabled):
gordon auth token generate --subject deploy --scopes push,pull --expiry 0
--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
# Recommended first-time setup
gordon bootstrap app.example.com myapp:latest --attachment postgres:18 --env APP_ENV=production
# Then build, push, and deploy
gordon push myapp:latest --domain app.example.com --build --no-confirm
What this command does:
gordon bootstrap creates or updates the route, applies requested attachments,
and stores environment variables.
- This is the recommended path for first deploys because it does not require the
route to exist ahead of time.
- Run
gordon push separately after bootstrap to build, upload, and deploy the image.
If the route already exists and you only need to push a new image version, use:
gordon push myapp --build --no-confirm
gordon push still requires the route to already exist so it can resolve the
deploy target.
Your app is now live at https://app.mydomain.com!
9. Update Your App
Push a new image to deploy with zero downtime:
# Make changes, then build + push + deploy
gordon push myapp --build --no-confirm
Gordon automatically:
- Starts the new container
- Waits for it to be ready
- Routes traffic to the new container
- Stops the old container
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- HTTPS setup: Cloudflare proxy, Tailscale, or Gordon's internal CA (`server.tls_port = 8443`)
10- [pass](https://www.passwordstore.org/) (password manager) with GPG key initialized
11
12## 1. Install Gordon
13
14```bash
15curl -fsSL https://gordon.bnema.dev/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]
76port = 8088 # HTTP proxy port (default)
77registry_port = 5000 # Registry port
78tls_port = 8443 # HTTPS listener (default)
79gordon_domain = "gordon.mydomain.com" # Registry + Admin API domain
80
81[routes]
82"app.mydomain.com" = "myapp:latest" # Domain → Image mapping
83```
84
85## 5. Set Up DNS (Including Wildcard)
86
87`gordon_domain` is the single domain used by both the registry and admin API.
88
89In Cloudflare (or your DNS provider), create:
90
91| Type | Name | Content | Proxy |
92|------|------|---------|-------|
93| A | `gordon` | `YOUR_SERVER_IP` | Yes |
94| A/CNAME | `*` | `YOUR_SERVER_IP` or `gordon.mydomain.com` | Yes |
95
96Why wildcard (`*`)?
97- It automatically covers app routes like `app.mydomain.com`, `api.mydomain.com`, `demo.mydomain.com`, etc.
98- You can add new domains in `[routes]` without creating DNS records one by one.
99
100If your DNS provider supports wildcard CNAME flattening (Cloudflare does), `* -> gordon.mydomain.com` is usually the cleanest option.
101
102> **Important for Cloudflare or other proxied setups:** when Gordon's internal CA is enabled (default `tls_port = 8443`), add your proxy edge IPs to `server.proxy_allowed_ips` or proxied HTTP traffic will get `403 Forbidden`. See [Installation](./installation.md#proxy-origin-allowlist). If you do not want Gordon to manage TLS at all, set `tls_port = 0`.
103
104## 6. Start Gordon as a Service
105
106```bash
107# Create systemd user service
108mkdir -p ~/.config/systemd/user
109cat > ~/.config/systemd/user/gordon.service <<EOF
110[Unit]
111Description=Gordon Container Platform
112
113[Service]
114Type=simple
115Restart=always
116ExecStart=/usr/local/bin/gordon serve
117
118[Install]
119WantedBy=default.target
120EOF
121
122# Enable and start
123systemctl --user daemon-reload
124systemctl --user enable --now gordon
125sudo loginctl enable-linger $USER
126```
127
128## 7. Generate a Deploy Token
129
130Create a token for remote CLI deploys (skip if auth is disabled):
131
132```bash
133gordon auth token generate --subject deploy --scopes push,pull --expiry 0
134```
135
136`--expiry 0` creates a non-expiring token. Prefer a finite expiry and a rotation policy unless you explicitly need a long-lived deploy token.
137
138Save this token securely -- you will use it to authenticate with your Gordon server from CI/CD pipelines and remote CLI sessions.
139
140## 8. Deploy Your First App
141
142On your local machine:
143
144```bash
145# Save and select your Gordon remote (one-time)
146gordon remotes add prod https://gordon.mydomain.com --token <your-token>
147gordon remotes use prod
148
149# Recommended first-time setup
150gordon bootstrap app.example.com myapp:latest --attachment postgres:18 --env APP_ENV=production
151
152# Then build, push, and deploy
153gordon push myapp:latest --domain app.example.com --build --no-confirm
154```
155
156What this command does:
157
158- `gordon bootstrap` creates or updates the route, applies requested attachments,
159 and stores environment variables.
160- This is the recommended path for first deploys because it does not require the
161 route to exist ahead of time.
162- Run `gordon push` separately after bootstrap to build, upload, and deploy the image.
163
164If the route already exists and you only need to push a new image version, use:
165
166```bash
167gordon push myapp --build --no-confirm
168```
169
170`gordon push` still requires the route to already exist so it can resolve the
171deploy target.
172
173Your app is now live at `https://app.mydomain.com`!
174
175## 9. Update Your App
176
177Push a new image to deploy with zero downtime:
178
179```bash
180# Make changes, then build + push + deploy
181gordon push myapp --build --no-confirm
182```
183
184Gordon automatically:
1851. Starts the new container
1862. Waits for it to be ready
1873. Routes traffic to the new container
1884. Stops the old container
189
190## Next Steps
191
192- [Installation Guide](./installation.md) - Production setup with firewall and rootless containers
193- [Configuration Reference](./config/index.md) - All configuration options
194- [Authentication](./config/auth.md) - Secure your registry
195- [Environment Variables](./config/env.md) - Configure per-app settings
196
197## Related
198
199- [First Deploy Tutorial](/wiki/tutorials/first-deploy.md)
200- [Podman Rootless Setup](/wiki/guides/podman-rootless.md)