# Gordon [![License: GPL-3.0](https://img.shields.io/badge/License-GPL%203.0-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) [![Go Report Card](https://goreportcard.com/badge/github.com/bnema/gordon)](https://goreportcard.com/report/github.com/bnema/gordon) Self-hosted web app deployments. Push to your registry, Gordon does the rest. - Website: https://github.com/bnema/gordon - Documentation: [Configuration](examples/) | [Setup Guide](#detailed-setup-guide-podman-rootless-mode) - Discuss: [GitHub Discussions](https://github.com/bnema/gordon/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. ```bash 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 ```bash # 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](#detailed-setup-guide-podman-rootless-mode) 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 ```bash # 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 ```dockerfile # 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: ```dockerfile # 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: ```bash # 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/](examples/) for advanced usage. ### Network Isolation Each app runs in its own isolated network by default. Attach services for internal communication: ```toml [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/](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: ```toml # ~/.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/](examples/) for advanced configurations including network groups and more. ### Authentication Types **Password Authentication** (simple): ```bash # 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): ```bash # 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 ``` 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: ```yaml # .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](examples/github-workflow.yml) and [.github/actions/deploy/README.md](.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 ```bash # 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: ```bash # 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: > ```bash > 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 ```bash # 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 < ~/.config/systemd/user/gordon.service <