v2.1.0 0fb867d

README.md RawHistoryFolder

Gordon

[License: GPL-3.0] [Go Report Card]

Self-hosted web app deployments. Push to your registry, Gordon does the rest.


Gordon is a private container registry + HTTP reverse proxy for your VPS. Push a container image exposing a web port, it deploys automatically with zero downtime.

docker build -t myapp .
docker push registry.your-server.com/myapp:latest
# → Live at https://app.your-server.com

Build on your machine, push to deploy. Works from your laptop or CI.

What it does:

  • Runs a private Docker registry on your VPS
  • Routes domains to containers via HTTP reverse proxy
  • Deploys automatically when you push a new image
  • Zero downtime updates, persistent volumes, environment merging
  • Single binary, ~15MB RAM

Quick Start

Prerequisites

  • Ubuntu/Debian VPS with root access
  • Domain pointing to your VPS
  • Cloudflare account (free tier works) - Required for HTTPS

HTTPS Architecture: Cloudflare terminates HTTPS and proxies to Gordon over HTTP internally. Gordon doesn't handle TLS certificates directly (no Let's Encrypt support yet).

Installation

# Download and install
wget https://github.com/bnema/gordon/releases/latest/download/gordon_linux_amd64.tar.gz
tar -xzf gordon_linux_amd64.tar.gz
chmod +x gordon
sudo mv gordon /usr/local/bin/

# Start Gordon (generates config on first run)
gordon start

Config is created at ~/.config/gordon/gordon.toml on first start. Edit it to set your registry_domain and routes.

Important: For a complete working setup including networking, firewall, and systemd service, follow the detailed setup guide below.

Core Concepts

Local-First Development

Your dev machine likely has 8-16 cores and 16-32GB RAM. Your VPS has 1-2 cores and 1-4GB RAM. Why build containers on the weak machine? Gordon lets you build locally and deploy the finished product.

Push-to-Deploy Workflow

# Initial deployment
docker build -t myapp .
docker push registry.mydomain.com/myapp:latest
# Visit https://app.mydomain.com

# Update deployment
docker build -t myapp .
docker push registry.mydomain.com/myapp:latest
# Zero-downtime update applied automatically

Automatic Volume Management

# In your Dockerfile
FROM postgres:15
VOLUME ["/var/lib/postgresql/data"]  # Gordon creates persistent storage

FROM node:18
VOLUME ["/app/uploads", "/app/data"]  # Multiple volumes supported

Proxy Port Selection

When an image exposes multiple ports, Gordon needs to know which one to proxy HTTP traffic to. By default, Gordon uses the first exposed port, but you can specify the exact port with a label:

# Gitea exposes both SSH (22) and HTTP (3000)
FROM gitea/gitea:latest
LABEL gordon.proxy.port=3000
EXPOSE 22
EXPOSE 3000

Without the label, Gordon would proxy to port 22 (SSH), which would fail. The gordon.proxy.port label tells Gordon to route HTTP traffic to port 3000.

When to use this label:

  • Images that expose multiple ports (web + SSH, web + metrics, etc.)
  • When the first exposed port isn't the HTTP service
  • To be explicit about which port serves your web application

Environment Configuration

Gordon automatically creates and loads env files based on domain names:

# Auto-created on first run: ~/.gordon/env/app_mydomain_com.env
# Just add your variables:
DATABASE_URL=postgresql://localhost:5432/myapp
API_KEY=sk-1234567890
NODE_ENV=production

Supports secrets from pass and SOPS. See examples/ for advanced usage.

Network Isolation

Each app runs in its own isolated network by default. Attach services for internal communication:

[attachments]
"app.mydomain.com" = ["my-postgres:latest", "my-redis:latest"]
# Access services via hostname: my-postgres:5432, my-redis:6379

Gordon also supports network groups, see examples/ for advanced usage.

Persistent Storage

  • Volumes: Automatically created from Dockerfile VOLUME directives, persist across updates

Production ready logging

  • Logs: All container output saved to ~/.gordon/logs/containers/
  • Rotation: Logs rotate at 100MB, keeping 3 backups for 28 days

Configuration

Gordon uses a single TOML file for all configuration:

# ~/.config/gordon/gordon.toml
[server]
registry_domain = "registry.mydomain.com"  # Required

# Secrets backend for sensitive data (tokens, passwords)
[secrets]
backend = "pass"  # "pass", "sops", or "unsafe" (plain text in data_dir)

# Registry authentication (choose password or token type)
[registry_auth]
enabled = true
type = "password"  # "password" or "token"
# Password auth: bcrypt hash stored in secrets backend
username = "deploy"
password_hash = "gordon/registry/password_hash"  # path in secrets backend
# Token auth: JWT-based authentication
# token_secret = "gordon/registry/token_secret"  # path in secrets backend

[routes]
"app.mydomain.com" = "myapp:latest"        # Domain → Image mapping
"api.mydomain.com" = "myapi:v2.1.0"        # Pin specific versions

# Optional: Attach services to apps
[attachments]
"app.mydomain.com" = ["postgres:latest", "redis:latest"]
# Services accessible via internal DNS: postgres:5432, redis:6379

See examples/ for advanced configurations including network groups and more.

Authentication Types

Password Authentication (simple):

# Generate a bcrypt hash for your password
gordon auth password hash
# Store the hash in your secrets backend, then reference it in config

Token Authentication (recommended for CI/CD):

# Generate a never-expiring token for CI
gordon auth token generate --subject ci-bot --scopes push,pull --expiry 0

# List all tokens
gordon auth token list

# Revoke a compromised token
gordon auth token revoke <token-id>

Tokens are stored in your configured secrets backend and are unique to each Gordon instance (different token_secret = incompatible tokens).

GitHub Actions Integration

Gordon provides an official GitHub Action for automated deployments on tag push:

# .github/workflows/deploy.yml
name: Deploy to Gordon

on:
  push:
    tags:
      - 'v*'

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6

      - name: Deploy to Gordon
        uses: bnema/gordon/.github/actions/deploy@main
        with:
          registry: ${{ secrets.GORDON_REGISTRY }}
          username: ${{ secrets.GORDON_USERNAME }}
          password: ${{ secrets.GORDON_TOKEN }}

Setup:

  1. Generate a CI token: gordon auth token generate --subject github-actions --scopes push,pull --expiry 0
  2. Add secrets to your GitHub repository: GORDON_REGISTRY, GORDON_USERNAME, GORDON_TOKEN
  3. Push a tag to deploy: git tag v1.0.0 && git push origin v1.0.0

See examples/github-workflow.yml and .github/actions/deploy/README.md for advanced options.

Detailed Setup Guide (Podman Rootless Mode)

Note: This guide uses Podman in rootless mode for enhanced security. Docker users can follow similar steps. Gordon works with any OCI-compatible runtime.

1. System Preparation

# Update system
sudo apt update && sudo apt upgrade -y

# Install podman and firewalld
sudo apt install -y podman firewalld

# Enable and start firewalld
sudo systemctl enable --now firewalld

2. Firewall & Port Forwarding (rootless requires this)

Rootless containers can't bind to privileged ports (< 1024). Use firewalld to redirect 80/443 to unprivileged ports:

# Allow HTTP and HTTPS traffic
sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-service=https

# Port forward 80/443 to rootless container ports
sudo firewall-cmd --permanent --add-forward-port=port=80:proto=tcp:toport=8080
sudo firewall-cmd --permanent --add-forward-port=port=443:proto=tcp:toport=8443

# Set default policy to drop incoming traffic
sudo firewall-cmd --permanent --zone=public --set-target=DROP

# Reload to apply
sudo firewall-cmd --reload

# Verify rules
sudo firewall-cmd --list-all

Note: If using Tailscale for management, add its interface to the trusted zone:

sudo firewall-cmd --permanent --zone=trusted --add-interface=tailscale0
sudo firewall-cmd --permanent --add-port=41641/udp  # Tailscale WireGuard
sudo firewall-cmd --reload

3. Rootless Container Setup

# Enable user namespaces
echo 'user.max_user_namespaces=28633' | sudo tee -a /etc/sysctl.conf
sudo sysctl -p

# Setup subuid/subgid for your user
sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 $USER

# Enable podman socket
systemctl --user enable --now podman.socket

# Allow localhost registry (podman config)
mkdir -p ~/.config/containers
cat > ~/.config/containers/registries.conf <<EOF
[registries.insecure]
registries = ['localhost:5000']
EOF

4. Install Gordon

# Download from releases
wget https://github.com/bnema/gordon/releases/latest/download/gordon_linux_amd64.tar.gz
tar -xzf gordon_linux_amd64.tar.gz
chmod +x gordon
sudo mv gordon /usr/local/bin/

5. Configure Gordon

# Run once to generate config, then stop with Ctrl+C
gordon start

# Edit the generated config
nano ~/.config/gordon/gordon.toml

Set your registry_domain and add routes. Data is stored in ~/.gordon/.

6. Create Systemd Service

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

[Service]
Type=simple
Restart=always
RestartSec=5
ExecStart=/usr/local/bin/gordon start

[Install]
WantedBy=default.target
EOF

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

# Verify
systemctl --user status gordon
journalctl --user -u gordon -f  # Watch logs

Deployment Strategies

Simple Deployment

# Build locally
docker build -t myapp .

# Test locally
docker run -p 8080:8080 myapp

# Push to registry
docker tag myapp registry.mydomain.com/myapp:latest
docker push registry.mydomain.com/myapp:latest

# Profit!

Versioned Deployment

# Deploy specific version
docker tag myapp:v1.2.0 registry.mydomain.com/myapp:v1.2.0
docker push registry.mydomain.com/myapp:v1.2.0

# Update route in gordon.toml
# "app.mydomain.com" = "myapp:v1.2.0"

Advanced: Manifest Annotations

# Deploy with metadata
export VERSION=v1.2.0
docker manifest create myapp:latest
docker manifest add myapp:latest registry.mydomain.com/myapp:$VERSION
docker manifest annotate myapp:latest --annotation version=$VERSION registry.mydomain.com/myapp:$VERSION
docker manifest push myapp:latest registry.mydomain.com/myapp:latest

Community

Gordon is open source and welcomes contributions:

License

GPL-3.0 - Use freely, contribute back.

source
1# Gordon
2
3[![License: GPL-3.0](https://img.shields.io/badge/License-GPL%203.0-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
4[![Go Report Card](https://goreportcard.com/badge/github.com/bnema/gordon)](https://goreportcard.com/report/github.com/bnema/gordon)
5
6Self-hosted web app deployments. Push to your registry, Gordon does the rest.
7
8- Website: https://github.com/bnema/gordon
9- Documentation: [Configuration](examples/) | [Setup Guide](#detailed-setup-guide-podman-rootless-mode)
10- Discuss: [GitHub Discussions](https://github.com/bnema/gordon/discussions)
11
12---
13
14Gordon is a private container registry + HTTP reverse proxy for your VPS. Push a container image exposing a web port, it deploys automatically with zero downtime.
15
16```bash
17docker build -t myapp .
18docker push registry.your-server.com/myapp:latest
19# → Live at https://app.your-server.com
20```
21
22Build on your machine, push to deploy. Works from your laptop or CI.
23
24**What it does:**
25- Runs a private Docker registry on your VPS
26- Routes domains to containers via HTTP reverse proxy
27- Deploys automatically when you push a new image
28- Zero downtime updates, persistent volumes, environment merging
29- Single binary, ~15MB RAM
30
31## Quick Start
32
33### Prerequisites
34- Ubuntu/Debian VPS with root access
35- Domain pointing to your VPS
36- Cloudflare account (free tier works) - **Required for HTTPS**
37
38> **HTTPS Architecture:** Cloudflare terminates HTTPS and proxies to Gordon over HTTP internally. Gordon doesn't handle TLS certificates directly (no Let's Encrypt support yet).
39
40### Installation
41
42```bash
43# Download and install
44wget https://github.com/bnema/gordon/releases/latest/download/gordon_linux_amd64.tar.gz
45tar -xzf gordon_linux_amd64.tar.gz
46chmod +x gordon
47sudo mv gordon /usr/local/bin/
48
49# Start Gordon (generates config on first run)
50gordon start
51```
52
53Config is created at `~/.config/gordon/gordon.toml` on first start. Edit it to set your `registry_domain` and routes.
54
55**Important:** For a complete working setup including networking, firewall, and systemd service, follow the [detailed setup guide](#detailed-setup-guide-podman-rootless-mode) below.
56
57## Core Concepts
58
59### Local-First Development
60Your dev machine likely has 8-16 cores and 16-32GB RAM. Your VPS has 1-2 cores and 1-4GB RAM. Why build containers on the weak machine? Gordon lets you build locally and deploy the finished product.
61
62### Push-to-Deploy Workflow
63```bash
64# Initial deployment
65docker build -t myapp .
66docker push registry.mydomain.com/myapp:latest
67# Visit https://app.mydomain.com
68
69# Update deployment
70docker build -t myapp .
71docker push registry.mydomain.com/myapp:latest
72# Zero-downtime update applied automatically
73```
74
75### Automatic Volume Management
76```dockerfile
77# In your Dockerfile
78FROM postgres:15
79VOLUME ["/var/lib/postgresql/data"]  # Gordon creates persistent storage
80
81FROM node:18
82VOLUME ["/app/uploads", "/app/data"]  # Multiple volumes supported
83```
84
85### Proxy Port Selection
86When an image exposes multiple ports, Gordon needs to know which one to proxy HTTP traffic to. By default, Gordon uses the first exposed port, but you can specify the exact port with a label:
87
88```dockerfile
89# Gitea exposes both SSH (22) and HTTP (3000)
90FROM gitea/gitea:latest
91LABEL gordon.proxy.port=3000
92EXPOSE 22
93EXPOSE 3000
94```
95
96Without the label, Gordon would proxy to port 22 (SSH), which would fail. The `gordon.proxy.port` label tells Gordon to route HTTP traffic to port 3000.
97
98**When to use this label:**
99- Images that expose multiple ports (web + SSH, web + metrics, etc.)
100- When the first exposed port isn't the HTTP service
101- To be explicit about which port serves your web application
102
103### Environment Configuration
104Gordon automatically creates and loads env files based on domain names:
105```bash
106# Auto-created on first run: ~/.gordon/env/app_mydomain_com.env
107# Just add your variables:
108DATABASE_URL=postgresql://localhost:5432/myapp
109API_KEY=sk-1234567890
110NODE_ENV=production
111```
112Supports secrets from `pass` and `SOPS`. See [examples/](examples/) for advanced usage.
113
114### Network Isolation
115Each app runs in its own isolated network by default. Attach services for internal communication:
116```toml
117[attachments]
118"app.mydomain.com" = ["my-postgres:latest", "my-redis:latest"]
119# Access services via hostname: my-postgres:5432, my-redis:6379
120```
121Gordon also supports network groups, see [examples/](examples/) for advanced usage.
122
123### Persistent Storage
124- **Volumes**: Automatically created from Dockerfile VOLUME directives, persist across updates
125
126### Production ready logging
127- **Logs**: All container output saved to `~/.gordon/logs/containers/`
128- **Rotation**: Logs rotate at 100MB, keeping 3 backups for 28 days
129
130## Configuration
131
132Gordon uses a single TOML file for all configuration:
133
134```toml
135# ~/.config/gordon/gordon.toml
136[server]
137registry_domain = "registry.mydomain.com"  # Required
138
139# Secrets backend for sensitive data (tokens, passwords)
140[secrets]
141backend = "pass"  # "pass", "sops", or "unsafe" (plain text in data_dir)
142
143# Registry authentication (choose password or token type)
144[registry_auth]
145enabled = true
146type = "password"  # "password" or "token"
147# Password auth: bcrypt hash stored in secrets backend
148username = "deploy"
149password_hash = "gordon/registry/password_hash"  # path in secrets backend
150# Token auth: JWT-based authentication
151# token_secret = "gordon/registry/token_secret"  # path in secrets backend
152
153[routes]
154"app.mydomain.com" = "myapp:latest"        # Domain → Image mapping
155"api.mydomain.com" = "myapi:v2.1.0"        # Pin specific versions
156
157# Optional: Attach services to apps
158[attachments]
159"app.mydomain.com" = ["postgres:latest", "redis:latest"]
160# Services accessible via internal DNS: postgres:5432, redis:6379
161```
162
163See [examples/](examples/) for advanced configurations including network groups and more.
164
165### Authentication Types
166
167**Password Authentication** (simple):
168```bash
169# Generate a bcrypt hash for your password
170gordon auth password hash
171# Store the hash in your secrets backend, then reference it in config
172```
173
174**Token Authentication** (recommended for CI/CD):
175```bash
176# Generate a never-expiring token for CI
177gordon auth token generate --subject ci-bot --scopes push,pull --expiry 0
178
179# List all tokens
180gordon auth token list
181
182# Revoke a compromised token
183gordon auth token revoke <token-id>
184```
185
186Tokens are stored in your configured secrets backend and are unique to each Gordon instance (different `token_secret` = incompatible tokens).
187
188### GitHub Actions Integration
189
190Gordon provides an official GitHub Action for automated deployments on tag push:
191
192```yaml
193# .github/workflows/deploy.yml
194name: Deploy to Gordon
195
196on:
197  push:
198    tags:
199      - 'v*'
200
201jobs:
202  deploy:
203    runs-on: ubuntu-latest
204    steps:
205      - uses: actions/checkout@v6
206
207      - name: Deploy to Gordon
208        uses: bnema/gordon/.github/actions/deploy@main
209        with:
210          registry: ${{ secrets.GORDON_REGISTRY }}
211          username: ${{ secrets.GORDON_USERNAME }}
212          password: ${{ secrets.GORDON_TOKEN }}
213```
214
215**Setup:**
2161. Generate a CI token: `gordon auth token generate --subject github-actions --scopes push,pull --expiry 0`
2172. Add secrets to your GitHub repository: `GORDON_REGISTRY`, `GORDON_USERNAME`, `GORDON_TOKEN`
2183. Push a tag to deploy: `git tag v1.0.0 && git push origin v1.0.0`
219
220See [examples/github-workflow.yml](examples/github-workflow.yml) and [.github/actions/deploy/README.md](.github/actions/deploy/README.md) for advanced options.
221
222## Detailed Setup Guide (Podman Rootless Mode)
223
224> **Note:** This guide uses Podman in rootless mode for enhanced security. Docker users can follow similar steps. Gordon works with any OCI-compatible runtime.
225
226### 1. System Preparation
227```bash
228# Update system
229sudo apt update && sudo apt upgrade -y
230
231# Install podman and firewalld
232sudo apt install -y podman firewalld
233
234# Enable and start firewalld
235sudo systemctl enable --now firewalld
236```
237
238### 2. Firewall & Port Forwarding (rootless requires this)
239
240Rootless containers can't bind to privileged ports (< 1024). Use firewalld to redirect 80/443 to unprivileged ports:
241
242```bash
243# Allow HTTP and HTTPS traffic
244sudo firewall-cmd --permanent --add-service=http
245sudo firewall-cmd --permanent --add-service=https
246
247# Port forward 80/443 to rootless container ports
248sudo firewall-cmd --permanent --add-forward-port=port=80:proto=tcp:toport=8080
249sudo firewall-cmd --permanent --add-forward-port=port=443:proto=tcp:toport=8443
250
251# Set default policy to drop incoming traffic
252sudo firewall-cmd --permanent --zone=public --set-target=DROP
253
254# Reload to apply
255sudo firewall-cmd --reload
256
257# Verify rules
258sudo firewall-cmd --list-all
259```
260
261> **Note:** If using Tailscale for management, add its interface to the trusted zone:
262> ```bash
263> sudo firewall-cmd --permanent --zone=trusted --add-interface=tailscale0
264> sudo firewall-cmd --permanent --add-port=41641/udp  # Tailscale WireGuard
265> sudo firewall-cmd --reload
266> ```
267
268### 3. Rootless Container Setup
269```bash
270# Enable user namespaces
271echo 'user.max_user_namespaces=28633' | sudo tee -a /etc/sysctl.conf
272sudo sysctl -p
273
274# Setup subuid/subgid for your user
275sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 $USER
276
277# Enable podman socket
278systemctl --user enable --now podman.socket
279
280# Allow localhost registry (podman config)
281mkdir -p ~/.config/containers
282cat > ~/.config/containers/registries.conf <<EOF
283[registries.insecure]
284registries = ['localhost:5000']
285EOF
286```
287
288### 4. Install Gordon
289```bash
290# Download from releases
291wget https://github.com/bnema/gordon/releases/latest/download/gordon_linux_amd64.tar.gz
292tar -xzf gordon_linux_amd64.tar.gz
293chmod +x gordon
294sudo mv gordon /usr/local/bin/
295```
296
297### 5. Configure Gordon
298```bash
299# Run once to generate config, then stop with Ctrl+C
300gordon start
301
302# Edit the generated config
303nano ~/.config/gordon/gordon.toml
304```
305
306Set your `registry_domain` and add routes. Data is stored in `~/.gordon/`.
307
308### 6. Create Systemd Service
309```bash
310mkdir -p ~/.config/systemd/user
311cat > ~/.config/systemd/user/gordon.service <<EOF
312[Unit]
313Description=Gordon Container Platform
314After=podman.socket
315
316[Service]
317Type=simple
318Restart=always
319RestartSec=5
320ExecStart=/usr/local/bin/gordon start
321
322[Install]
323WantedBy=default.target
324EOF
325
326# Enable and start
327systemctl --user daemon-reload
328systemctl --user enable --now gordon
329sudo loginctl enable-linger $USER
330
331# Verify
332systemctl --user status gordon
333journalctl --user -u gordon -f  # Watch logs
334```
335
336## Deployment Strategies
337
338### Simple Deployment
339```bash
340# Build locally
341docker build -t myapp .
342
343# Test locally
344docker run -p 8080:8080 myapp
345
346# Push to registry
347docker tag myapp registry.mydomain.com/myapp:latest
348docker push registry.mydomain.com/myapp:latest
349
350# Profit!
351```
352
353### Versioned Deployment
354```bash
355# Deploy specific version
356docker tag myapp:v1.2.0 registry.mydomain.com/myapp:v1.2.0
357docker push registry.mydomain.com/myapp:v1.2.0
358
359# Update route in gordon.toml
360# "app.mydomain.com" = "myapp:v1.2.0"
361```
362
363### Advanced: Manifest Annotations
364```bash
365# Deploy with metadata
366export VERSION=v1.2.0
367docker manifest create myapp:latest
368docker manifest add myapp:latest registry.mydomain.com/myapp:$VERSION
369docker manifest annotate myapp:latest --annotation version=$VERSION registry.mydomain.com/myapp:$VERSION
370docker manifest push myapp:latest registry.mydomain.com/myapp:latest
371```
372
373## Community
374
375Gordon is open source and welcomes contributions:
376- [Report bugs](https://github.com/bnema/gordon/issues)
377- [Suggest features](https://github.com/bnema/gordon/discussions)
378- [Submit PRs](https://github.com/bnema/gordon/pulls)
379
380## License
381
382GPL-3.0 - Use freely, contribute back.

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