Core Concepts

Understanding how Gordon works and why it's designed this way.

Local-First Development

Your development 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 flips the typical deployment model:

  1. Build locally where you have computing power
  2. Push the finished image to your VPS
  3. Gordon deploys automatically

This means faster builds, less VPS resource usage, and a simpler deployment workflow.

Push-to-Deploy

Gordon combines a Docker registry with automatic deployment:

┌──────────────┐      push       ┌──────────────┐
│ docker build │  ──────────────>│   Gordon     │
│ docker push  │                 │   Registry   │
└──────────────┘                 └──────┬───────┘
                                        │
                                        │ event: image.pushed
                                        v
                                 ┌──────────────┐
                                 │   Deploy     │
                                 │   Container  │
                                 └──────────────┘

When you push an image, Gordon:

  1. Stores the image in its registry
  2. Fires an image.pushed event
  3. Looks up the route for that image
  4. Deploys a new container
  5. Updates the proxy routing
  6. Stops the old container

Zero-Downtime Updates

Gordon ensures your app stays available during updates:

  1. New container starts while old container is still running
  2. Health check waits for new container to be ready
  3. Traffic switches to the new container
  4. Old container stops after traffic has moved
Time ─────────────────────────────────────────────>

Old Container:  [═══════════════════]
                                    ↓ stop
New Container:           [═════════════════════════>
                         ↑ start    ↑ traffic routed

Routes

Routes map domains to container images:

[routes]
"app.mydomain.com" = "myapp:latest"
"api.mydomain.com" = "myapi:v2.1.0"

When a request comes in for app.mydomain.com, Gordon:

  1. Looks up the route configuration
  2. Finds the running container for myapp:latest
  3. Proxies the request to that container

HTTP vs HTTPS Routes

By default, routes expect HTTPS (terminated by Cloudflare). For HTTP-only routes:

[routes]
"http://internal.local" = "internal-app:latest"

Network Isolation

Each app runs in its own isolated Docker network:

┌────────────────────────────────────────────────┐
│ gordon-app-mydomain-com                        │
│                                                │
│  ┌──────────┐    ┌──────────┐    ┌──────────┐  │
│  │   App    │───>│ Postgres │    │  Redis   │  │
│  │ :3000    │    │ :5432    │    │ :6379    │  │
│  └──────────┘    └──────────┘    └──────────┘  │
│                                                │
└────────────────────────────────────────────────┘

Benefits:

  • Containers can't access each other's services
  • Services are only accessible by name within their network
  • No port conflicts between apps

Attachments

Attachments are service dependencies for your apps:

[attachments]
"app.mydomain.com" = ["postgres:latest", "redis:latest"]

Gordon deploys attachments to the same network as your app. Services are accessible by their image name:

// In your app
const db = await connect("postgresql://postgres:5432/mydb");
const cache = await connect("redis://redis:6379");

Network Groups

Network groups allow multiple apps to share services:

[network_groups]
"backend" = ["app.mydomain.com", "api.mydomain.com"]

[attachments]
"backend" = ["shared-postgres:latest", "shared-redis:latest"]

Both app.mydomain.com and api.mydomain.com can access the shared services.

Volumes

Gordon automatically creates persistent storage from Dockerfile VOLUME directives:

FROM postgres:18
VOLUME ["/var/lib/postgresql/data"]

Volume behavior:

  • auto_create: Volumes are created automatically (default: true)
  • prefix: Volume names are prefixed with gordon- (configurable)
  • preserve: Volumes persist across container updates (default: true)

Environment Variables

Gordon loads environment variables from files based on the domain:

~/.gordon/env/
├── app_mydomain_com.env
├── api_mydomain_com.env
└── admin_mydomain_com.env

Domain dots become underscores: app.mydomain.comapp_mydomain_com.env

Variables are merged in order:

  1. Dockerfile ENV directives (lowest priority)
  2. .env file values (highest priority)

Secret Providers

Environment files support secret provider syntax:

# From Unix password manager (pass)
DATABASE_PASSWORD=${pass:myapp/db-password}

# From SOPS encrypted files
API_SECRET=${sops:secrets.yaml:api.secret}

Configuration Hot-Reload

Gordon watches its config file and reloads automatically:

  1. Edit ~/.config/gordon/gordon.toml
  2. Save the file
  3. Gordon reloads routes, attachments, and network groups
  4. Containers sync to match new configuration

You can also trigger a manual reload:

gordon reload

This sends SIGUSR1 to the running Gordon process.

Event System

Gordon uses an internal event system for coordination:

Event Trigger Action
image.pushed Image pushed to registry Deploy container
config.reload Config file changed Sync containers
manual.reload gordon reload command Sync containers
manual.deploy gordon deploy <domain> command Deploy specific route
container.deployed Container started Update proxy cache

Backups and Recovery

Gordon can run logical PostgreSQL backups for attachment containers.

Current design:

  1. Detect PostgreSQL attachments attached to a route
  2. Execute pg_dump -Fc through Gordon runtime operations
  3. Store backup artifacts on local filesystem storage
  4. Expose backup actions through admin API and CLI

This is intentionally scoped for operational safety and predictable behavior. Future extensions can add physical backups, PITR, and remote object storage adapters. For configuration details and usage examples, see the Backups Configuration guide, Backup CLI reference, and Configuration Reference.

Container Labels

Gordon uses labels to track managed containers:

Label Purpose
gordon.managed=true Identifies Gordon-managed containers
gordon.domain Domain this container serves
gordon.image Image name and tag
gordon.route Route this container handles
gordon.attachment=true Container is an attachment service
gordon.attached-to Which route this attachment serves

Proxy Port Selection

When a container exposes multiple ports, Gordon needs to know which one serves HTTP:

FROM gitea/gitea:latest
LABEL gordon.proxy.port=3000  # Route HTTP to port 3000
EXPOSE 22   # SSH
EXPOSE 3000 # HTTP

Without the label, Gordon uses the first exposed port.