Getting Started

Deploy your first app with Gordon in under 5 minutes.

Prerequisites

  • A Linux VPS with Docker or Podman installed
  • A domain pointing to your VPS (DNS A record)
  • HTTPS setup: Cloudflare proxy or native TLS in Gordon (server.tls_enabled = true)
  • pass (password manager) with GPG key initialized

1. Install Gordon

curl -fsSL https://gordon.bnema.dev/install | bash

This script automatically detects your OS and architecture, downloads the appropriate binary, and installs it to /usr/local/bin.

2. Initialize Configuration

# First run creates the default config
gordon serve
# Press Ctrl+C to stop

Config is created at ~/.config/gordon/gordon.toml.

3. Set Up Authentication

Gordon requires a token_secret for JWT authentication. You can store it in pass or provide it via the GORDON_AUTH_TOKEN_SECRET environment variable. We recommend using pass to store secrets securely.

Local development? If you just want to try Gordon locally, you can disable auth temporarily:

[auth]
enabled = false

Skip to Step 4. For production, continue below.

Initialize pass (if not already done):

# Generate a GPG key if you don't have one
gpg --gen-key

# Initialize pass with your GPG key ID
pass init <your-gpg-key-id>

Create the JWT token secret:

# Generate and store a random 32-character secret
openssl rand -base64 32 | pass insert -m gordon/auth/token_secret

Or set it via environment variable:

export GORDON_AUTH_TOKEN_SECRET="your-32-character-secret-here"

Update your config (~/.config/gordon/gordon.toml):

[auth]
enabled = true
secrets_backend = "pass"
token_secret = "gordon/auth/token_secret"

4. Configure Your Gordon Domain

Edit ~/.config/gordon/gordon.toml:

[server]
port = 8080                              # Proxy port (use with Cloudflare/rootless)
registry_port = 5000                     # Registry port
gordon_domain = "gordon.mydomain.com"    # Registry + Admin API domain

[routes]
"app.mydomain.com" = "myapp:latest"      # Domain → Image mapping

5. Set Up DNS (Including Wildcard)

gordon_domain is the single domain used by both the registry and admin API.

In Cloudflare (or your DNS provider), create:

Type Name Content Proxy
A gordon YOUR_SERVER_IP Yes
A/CNAME * YOUR_SERVER_IP or gordon.mydomain.com Yes

Why wildcard (*)?

  • It automatically covers app routes like app.mydomain.com, api.mydomain.com, demo.mydomain.com, etc.
  • You can add new domains in [routes] without creating DNS records one by one.

If your DNS provider supports wildcard CNAME flattening (Cloudflare does), * -> gordon.mydomain.com is usually the cleanest option.

6. Start Gordon as a Service

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

[Service]
Type=simple
Restart=always
ExecStart=/usr/local/bin/gordon serve

[Install]
WantedBy=default.target
EOF

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

7. Generate a Deploy Token

Create a token for remote CLI deploys (skip if auth is disabled):

gordon auth token generate --subject deploy --scopes push,pull --expiry 0

--expiry 0 creates a non-expiring token. Prefer a finite expiry and a rotation policy unless you explicitly need a long-lived deploy token.

Save this token securely -- you will use it to authenticate with your Gordon server from CI/CD pipelines and remote CLI sessions.

8. Deploy Your First App

On your local machine:

# Save and select your Gordon remote (one-time)
gordon remotes add prod https://gordon.mydomain.com --token <your-token>
gordon remotes use prod

# Recommended first-time setup
gordon bootstrap app.example.com myapp:latest --attachment postgres:18 --env APP_ENV=production

# Then build, push, and deploy
gordon push app.example.com --build --no-confirm

What this command does:

  • gordon bootstrap creates or updates the route, applies requested attachments, and stores environment variables.
  • This is the recommended path for first deploys because it does not require the route to exist ahead of time.
  • Run gordon push separately after bootstrap to build, upload, and deploy the image.

If the route already exists and you only need to push a new image version, use:

gordon push app.example.com --build --no-confirm

gordon push still requires the route to already exist so it can resolve the target image.

Your app is now live at https://app.mydomain.com!

9. Update Your App

Push a new image to deploy with zero downtime:

# Make changes, then build + push + deploy
gordon push myapp --build --no-confirm

Gordon automatically:

  1. Starts the new container
  2. Waits for it to be ready
  3. Routes traffic to the new container
  4. Stops the old container

Next Steps