Installation
Detailed installation guide for production environments.
System Requirements
- OS: Linux (Ubuntu 24.04 LTS, Debian 13 recommended) or macOS
- Architecture: x86_64 (amd64) or ARM64
- Memory: 512MB minimum, 1GB recommended
- Disk: 10GB minimum for registry storage
- Runtime: Docker or Podman with a Docker-compatible API version 1.40 or later
Download Gordon
Quick Install (Recommended)
(
set -euo pipefail
installer="$(mktemp)"
trap 'rm -f "$installer"' EXIT
curl -fsSL --output "$installer" https://bnema.dev/gordon/install
if command -v less >/dev/null 2>&1; then
less "$installer"
else
cat "$installer"
fi
bash "$installer"
)
Download and inspect the installer before executing it. The installer verifies the release archive checksum, detects Linux/macOS and amd64/arm64, and installs Gordon to ~/.local/bin without sudo. If that directory is absent from PATH, an interactive install can offer to add an idempotent marker block to Fish (~/.config/fish/config.fish), Bash (~/.bashrc), or Zsh (~/.zshrc). Restart the shell after accepting, or follow the printed command to update the current shell.
For unattended installs, control PATH configuration explicitly:
# Update supported shell configuration without prompting
curl -fsSL https://bnema.dev/gordon/install | GORDON_UPDATE_PATH=1 sh
# Never modify shell configuration
curl -fsSL https://bnema.dev/gordon/install | GORDON_UPDATE_PATH=0 sh
# Use another user-local directory
curl -fsSL https://bnema.dev/gordon/install | GORDON_INSTALL_DIR="$HOME/bin" GORDON_UPDATE_PATH=1 sh
# Explicit global installation (may request sudo)
curl -fsSL https://bnema.dev/gordon/install | GORDON_INSTALL_DIR=/usr/local/bin GORDON_UPDATE_PATH=0 sh
GORDON_INSTALL_DIR accepts arbitrary safe absolute destinations, including "$HOME/bin", "$HOME/.local/bin", and /usr/local/bin. PATH detection and shell configuration use that effective directory. Relative paths, PATH separators, and control characters are rejected. GORDON_UPDATE_PATH accepts only 0 or 1. Do not run the default installer through sudo: it refuses to infer a user home and install silently under /root.
Manual Installation
Manual archive installation requires downloading the matching checksums.txt release asset and verifying the archive's SHA-256 checksum before extraction. The checksum-verifying installer above is recommended.
Linux (x86_64)
wget https://github.com/bnema/gordon/releases/latest/download/{gordon_linux_amd64.tar.gz,checksums.txt}
grep -E '^[[:xdigit:]]{64} ([* ]?)gordon_linux_amd64\.tar\.gz$' checksums.txt | sha256sum --check --strict
tar -xzf gordon_linux_amd64.tar.gz
chmod +x gordon
sudo mv gordon /usr/local/bin/
Linux (ARM64) - for Raspberry Pi 4, AWS Graviton, Oracle Ampere, etc.
wget https://github.com/bnema/gordon/releases/latest/download/{gordon_linux_arm64.tar.gz,checksums.txt}
grep -E '^[[:xdigit:]]{64} ([* ]?)gordon_linux_arm64\.tar\.gz$' checksums.txt | sha256sum --check --strict
tar -xzf gordon_linux_arm64.tar.gz
chmod +x gordon
sudo mv gordon /usr/local/bin/
macOS (Apple Silicon)
curl -LO https://github.com/bnema/gordon/releases/latest/download/{gordon_darwin_arm64.tar.gz,checksums.txt}
grep -E '^[[:xdigit:]]{64} ([* ]?)gordon_darwin_arm64\.tar\.gz$' checksums.txt | shasum -a 256 --check
tar -xzf gordon_darwin_arm64.tar.gz
chmod +x gordon
sudo mv gordon /usr/local/bin/
macOS (Intel)
curl -LO https://github.com/bnema/gordon/releases/latest/download/{gordon_darwin_amd64.tar.gz,checksums.txt}
grep -E '^[[:xdigit:]]{64} ([* ]?)gordon_darwin_amd64\.tar\.gz$' checksums.txt | shasum -a 256 --check
tar -xzf gordon_darwin_amd64.tar.gz
chmod +x gordon
sudo mv gordon /usr/local/bin/
Verify installation:
gordon version
From Source
# Requires Go 1.21+
git clone https://github.com/bnema/gordon.git
cd gordon
make build
sudo mv gordon /usr/local/bin/
Container Runtime Setup
Docker
# Install Docker from your distribution's signed package repository.
sudo apt update
sudo apt install docker.io
# Add user to docker group
sudo usermod -aG docker $USER
newgrp docker
# Verify
docker run hello-world
Podman (Rootless)
Podman rootless mode provides enhanced security by running containers without root privileges.
# Install Podman
sudo apt update
sudo apt install -y podman
# Enable user namespaces
echo 'user.max_user_namespaces=28633' | sudo tee -a /etc/sysctl.conf
sudo sysctl -p
# Setup subuid/subgid
sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 $USER
# Enable Podman socket
systemctl --user enable --now podman.socket
# Configure insecure localhost registry (required — Podman blocks HTTP registries by default)
mkdir -p ~/.config/containers/registries.conf.d
cat > ~/.config/containers/registries.conf.d/gordon.conf <<EOF
[[registry]]
location = "localhost:5000"
insecure = true
EOF
Podman assigns loopback backend ports from the host's ephemeral port range when Gordon publishes managed service ports. If an nftables output policy restricts loopback access, allow the Gordon daemon's primary UID—not its subordinate container UIDs—to connect to that range for the protocols used by managed services:
# Confirm the range with: sysctl net.ipv4.ip_local_port_range
meta skuid <gordon-uid> ip daddr 127.0.0.1 tcp dport 32768-60999 accept
meta skuid <gordon-uid> ip daddr 127.0.0.1 udp dport 32768-60999 accept
Place these rules before any broader loopback rejection. Keep public entrypoint and administrative-port policy separate; these rules only let Gordon reach rootless Podman backends on the local host.
Firewall Configuration
Gordon needs ports accessible for the registry and the public edge entrypoint.
Using firewalld
# Install and enable firewalld
sudo apt install -y firewalld
sudo systemctl enable --now firewalld
# Allow HTTP/HTTPS
sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-service=https
# For rootless services, bind entrypoints.edge.address to a high port (for example :9000)
# and redirect the external TCP edge ports your deployment uses.
sudo firewall-cmd --permanent --add-forward-port=port=80:proto=tcp:toport=9000
sudo firewall-cmd --permanent --add-forward-port=port=443:proto=tcp:toport=9000
# Apply changes
sudo firewall-cmd --reload
# Verify
sudo firewall-cmd --list-all
Using ufw
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 5000/tcp # Registry port (if accessing directly)
sudo ufw enable
Configuration
Gordon creates a default configuration on first run:
# Run once to generate config, then stop
gordon serve
# Press Ctrl+C
# Edit configuration
nano ~/.config/gordon/gordon.toml
Minimum required configuration:
[server]
registry_port = 5000
gordon_domain = "gordon.yourdomain.com"
[entrypoints.edge]
address = ":443"
protocol = "smart_tcp"
Application workloads live in standalone app files, not in gordon.toml (see Getting Started).
See Configuration Reference for all options.
Systemd Service
User Service (Recommended for Rootless)
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
# Required for Podman rootless — tells Gordon where to find the Podman socket
Environment=XDG_RUNTIME_DIR=/run/user/%U
Environment=DOCKER_HOST=unix:///run/user/%U/podman/podman.sock
ExecStart=/usr/local/bin/gordon serve
[Install]
WantedBy=default.target
EOF
> **Docker users:** Omit the two `Environment=` lines above — Docker's socket path is set automatically.
# Enable and start
systemctl --user daemon-reload
systemctl --user enable --now gordon
# Enable linger (keep service running after logout)
sudo loginctl enable-linger $USER
# Check status
systemctl --user status gordon
System Service (Root Mode)
sudo cat > /etc/systemd/system/gordon.service <<EOF
[Unit]
Description=Gordon Container Platform
After=docker.service
Requires=docker.service
[Service]
Type=simple
Restart=always
RestartSec=5
ExecStart=/usr/local/bin/gordon serve
User=root
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now gordon
DNS Configuration
Point your domains to your server:
| Type | Name | Content | Proxy |
|---|---|---|---|
| A | app |
SERVER_IP |
Yes (Cloudflare) |
| A | registry |
SERVER_IP |
Yes (Cloudflare) |
Note: Enable Cloudflare proxy (orange cloud) for automatic HTTPS. Gordon receives HTTP traffic from Cloudflare.
Cloudflare SSL Mode
Gordon serves public application traffic through HTTP-capable entrypoints such as entrypoints.edge with protocol = "smart_tcp". Choose the Cloudflare mode that matches how you want Cloudflare to reach your origin.
| Mode | How it works | Use when |
|---|---|---|
| Flexible | Cloudflare terminates TLS; connects to Gordon with cleartext HTTP | You want edge HTTPS only |
| Full (Strict) | Cloudflare connects to Gordon over HTTPS with a valid cert | You want end-to-end HTTPS using Gordon's public ACME certs or a static cert (tls_cert_file / tls_key_file) |
Wrong mode causes: 521 (Cloudflare can't connect) or 525 (TLS handshake failed).
Important: For Cloudflare-proxied HTTP paths, set
proxy_allowed_ipswith Cloudflare edge IPs — see Proxy Origin Allowlist below.Rootless note: Unprivileged users can't bind privileged ports. Bind
entrypoints.edge.addressto a high port (for example:9000) and forward/map external ports to it via firewall or container settings.
Proxy Origin Allowlist
When Gordon serves HTTP paths through a smart TCP edge, direct non-localhost HTTP requests can be restricted to certificate onboarding paths. Cloudflare and other reverse proxies must be listed in proxy_allowed_ips to reach your applications.
Add Cloudflare edge IPs to your gordon.toml:
[server]
proxy_allowed_ips = [
"173.245.48.0/20", "103.21.244.0/22", "103.22.200.0/22",
"103.31.4.0/22", "141.101.64.0/18", "108.162.192.0/18",
"190.93.240.0/20", "188.114.96.0/20", "197.234.240.0/22",
"198.41.128.0/17", "162.158.0.0/15", "104.16.0.0/13",
"104.24.0.0/14", "172.64.0.0/13", "131.0.72.0/22",
]
Without this setting, Cloudflare traffic receives 403 Forbidden: Only certificate onboarding is available over HTTP.
Note: This is separate from
[api.rate_limit] trusted_proxies, which controls IP extraction fromX-Forwarded-For. Both should list your proxy IPs. See Proxy Origin IP Allowlist for details.
Choosing an Install Channel
# Install an exact release
curl -fsSL https://bnema.dev/gordon/install | GORDON_VERSION=v2.30.1 sh
# Install the latest pre-release
curl -fsSL https://bnema.dev/gordon/install | GORDON_PRERELEASE=1 sh
# Build the current next branch commit from source
curl -fsSL https://bnema.dev/gordon/install | GORDON_CHANNEL=next sh
Stable, exact-version, and pre-release installs download release binaries and verify their published checksums. The next channel is an unverified development source build: it resolves the branch through the GitHub API, pins the resulting commit SHA, downloads that exact source snapshot, and builds it locally for the detected platform. It requires the Go version declared by that commit's go.mod, is not covered by release checksums, and may be unstable. Do not combine GORDON_CHANNEL=next with GORDON_VERSION or GORDON_PRERELEASE.
Verify Installation
# Check Gordon is running
systemctl --user status gordon
# View logs
journalctl --user -u gordon -f
# Test registry (from local machine)
docker login registry.yourdomain.com
docker pull alpine
docker tag alpine registry.yourdomain.com/test:latest
docker push registry.yourdomain.com/test:latest
Data Directories
Gordon stores data in the following locations:
| Path | Purpose |
|---|---|
~/.config/gordon/gordon.toml |
Configuration file |
~/.gordon/ |
Default data directory |
~/.gordon/registry/ |
Container images |
~/.gordon/logs/ |
Application logs |
~/.gordon/secrets/ |
Secrets (unsafe backend only) |