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:
- Build locally where you have computing power
- Push the finished image to your VPS
- 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:
- Stores the image in its registry
- Fires an
image.pushedevent - Looks up the route for that image
- Deploys a new container
- Updates the proxy routing
- Stops the old container
Zero-Downtime Updates
Gordon ensures your app stays available during updates:
- New container starts while old container is still running
- Health check waits for new container to be ready
- Traffic switches to the new container
- 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:
- Looks up the route configuration
- Finds the running container for
myapp:latest - 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:15
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.com → app_mydomain_com.env
Variables are merged in order:
- Dockerfile
ENVdirectives (lowest priority) .envfile 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:
- Edit
~/.config/gordon/gordon.toml - Save the file
- Gordon reloads routes, attachments, and network groups
- 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 |
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.