Gordon
[License: GPL-3.0] [Go Report Card]
Self-hosted web app deployments. Push to your registry, Gordon does the rest.
- Website: https://github.com/bnema/gordon
- Documentation: Configuration | Setup Guide
- Discuss: GitHub Discussions
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:
- Generate a CI token:
gordon auth token generate --subject github-actions --scopes push,pull --expiry 0 - Add secrets to your GitHub repository:
GORDON_REGISTRY,GORDON_USERNAME,GORDON_TOKEN - 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.