docs(README): Revise deployment instructions and enhance feature descriptions
- Update sections for clarity, including renaming "Build Locally, Deploy Instantly" to "Build & Test Locally" and "Instant Rollbacks" to "Version Control & Rollbacks". - Consolidate feature descriptions for better readability and remove outdated content. - Streamline quick start guide and configuration instructions for improved user experience. - Ensure consistency in terminology and formatting throughout the document.
- commit
- 432bbf5b37c5125d79da1740d77cfe427c20deb4
- author
- bamen · · 2025-06-08 09:57 UTC
- parent
- 33eb2a5
- signature
- unsigned
1 file · +98 -401
M README.md +98 -401
| @@ -13,7 +13,6 @@ | |||
| 13 | 13 | Gordon is the missing piece that makes container deployment on budget VPS servers as simple as expensive PaaS solutions. One binary, one config file, unlimited apps. | |
| 14 | 14 | ||
| 15 | 15 | ### Perfect For | |
| 16 | - | ||
| 17 | 16 | - **Solo developers** with multiple side projects on one VPS | |
| 18 | 17 | - **Small teams** who want Heroku-like simplicity without the cost | |
| 19 | 18 | - **Agencies** deploying client apps across VPS servers | |
| @@ -21,10 +20,8 @@ Gordon is the missing piece that makes container deployment on budget VPS server | |||
| 21 | 20 | ||
| 22 | 21 | ## How It Works | |
| 23 | 22 | ||
| 24 | - | ### Build Locally, Deploy Instantly | |
| 25 | - | ||
| 26 | 23 | ```bash | |
| 27 | - | # 1. Build & test on YOUR machine | |
| 24 | + | # 1. Build & test locally | |
| 28 | 25 | podman build -t myapp . | |
| 29 | 26 | podman run -p 8080:8080 myapp # Works? Great! | |
| 30 | 27 | ||
| @@ -37,90 +34,49 @@ podman push registry.mydomain.com/myapp:latest | |||
| 37 | 34 | ||
| 38 | 35 | **No build servers. No CI/CD complexity. If it runs on your machine, it runs in production.** | |
| 39 | 36 | ||
| 40 | - | ### Instant Rollbacks | |
| 41 | - | ||
| 42 | - | Something broke? Just change your config: | |
| 43 | - | ||
| 44 | - | ```toml | |
| 45 | - | # Before (in gordon.toml) | |
| 46 | - | "app.mydomain.com" = "myapp:latest" | |
| 47 | - | ||
| 48 | - | # After - instant rollback! | |
| 49 | - | "app.mydomain.com" = "myapp:v1.2.3" | |
| 50 | - | ``` | |
| 51 | - | ||
| 52 | - | Save the file. Gordon redeploys the previous version. Problem solved in seconds. | |
| 53 | - | ||
| 54 | 37 | ## Key Features | |
| 55 | 38 | ||
| 56 | - | ### Local-First Development | |
| 57 | - | **Your machine is the build server.** Test locally with your favorite container engine, push when ready. No waiting for remote builds. | |
| 58 | - | ||
| 59 | - | ### Built-in Container Registry | |
| 60 | - | Your VPS becomes a private container registry (Docker/Podman compatible). No Docker Hub subscription needed. | |
| 61 | - | ||
| 62 | - | ### Push-to-Deploy Magic | |
| 63 | - | Gordon watches for new images and deploys them instantly to configured domains. | |
| 64 | - | ||
| 65 | - | ### Version Control Built-In | |
| 66 | - | Keep multiple versions in your registry. Switch between them instantly by editing the config. | |
| 67 | - | ||
| 68 | - | ### Multi-Domain Routing | |
| 69 | - | Run unlimited apps on one server. Each gets its own domain with automatic HTTPS via Cloudflare. | |
| 70 | - | ||
| 71 | - | ### Zero-Downtime Updates | |
| 72 | - | Push new versions anytime. Gordon handles graceful container swaps. | |
| 73 | - | ||
| 74 | - | ### Automatic Deployment | |
| 75 | - | Containers deploy instantly when you push new images. No manual deployment steps needed. | |
| 76 | - | ||
| 77 | - | ### Auto-Route Creation | |
| 78 | - | Automatically create routes from image names containing domains. Push an image named like `myapp.mydomain.com:latest` and Gordon creates the route automatically when enabled. | |
| 39 | + | - **Local-First Development**: Your machine is the build server | |
| 40 | + | - **Built-in Container Registry**: Your VPS becomes a private registry | |
| 41 | + | - **Push-to-Deploy**: New images deploy instantly | |
| 42 | + | - **Multi-Domain Routing**: Unlimited apps with automatic HTTPS via Cloudflare | |
| 43 | + | - **Zero-Downtime Updates**: Graceful container swaps | |
| 44 | + | - **Auto-Route Creation**: Push `myapp.mydomain.com:latest` → route created automatically | |
| 79 | 45 | ||
| 80 | 46 | ## Quick Start (5 minutes) | |
| 81 | 47 | ||
| 82 | - | ### 1. Get a VPS, Install Podman and UFW | |
| 48 | + | ### 1. Setup VPS | |
| 83 | 49 | ```bash | |
| 84 | - | # Any VPS provider: DigitalOcean, Linode, Vultr, Hetzner | |
| 85 | - | # Ubuntu/Debian recommended | |
| 86 | - | ||
| 87 | - | # Install Essentials | |
| 88 | - | sudo apt update | |
| 89 | - | sudo apt install -y podman ufw | |
| 50 | + | # Install essentials on Ubuntu/Debian | |
| 51 | + | sudo apt update && sudo apt install -y podman ufw | |
| 90 | 52 | ||
| 91 | - | # Configure UFW firewall first | |
| 53 | + | # Configure firewall | |
| 92 | 54 | sudo ufw --force enable | |
| 93 | 55 | sudo ufw default deny incoming | |
| 94 | 56 | sudo ufw default allow outgoing | |
| 95 | - | sudo ufw allow 22/tcp | |
| 96 | - | sudo ufw allow 80/tcp | |
| 97 | - | sudo ufw allow 443/tcp | |
| 57 | + | sudo ufw allow 22/tcp 80/tcp 443/tcp | |
| 98 | 58 | sudo ufw enable | |
| 99 | 59 | ``` | |
| 100 | - | ### 2. Redirect Ports 80/443 to 8080 | |
| 60 | + | ||
| 61 | + | ### 2. Redirect Ports | |
| 101 | 62 | ```bash | |
| 102 | - | # Simple iptables redirect | |
| 63 | + | # Redirect 80/443 to 8080 | |
| 103 | 64 | sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 8080 | |
| 104 | 65 | sudo iptables -t nat -A PREROUTING -p tcp --dport 443 -j REDIRECT --to-port 8080 | |
| 105 | - | ||
| 106 | - | # Make rules persistent across reboots | |
| 107 | 66 | sudo apt install -y iptables-persistent | |
| 108 | 67 | sudo netfilter-persistent save | |
| 109 | 68 | ``` | |
| 110 | - | ### 3. Enable rootless mode for enhanced security | |
| 111 | - | ``` | |
| 69 | + | ||
| 70 | + | ### 3. Enable Rootless Containers | |
| 71 | + | ```bash | |
| 112 | 72 | echo 'user.max_user_namespaces=28633' | sudo tee -a /etc/sysctl.conf | |
| 113 | 73 | sudo sysctl -p | |
| 114 | - | ||
| 115 | - | # Configure user for rootless containers | |
| 116 | 74 | sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 $USER | |
| 117 | - | ||
| 118 | - | # Start user services (no root required!) | |
| 119 | 75 | systemctl --user enable --now podman.socket | |
| 120 | 76 | ``` | |
| 121 | 77 | ||
| 122 | - | ### 4. Configure registries for your Gordon registry | |
| 123 | - | ``` | |
| 78 | + | ### 4. Configure Registry | |
| 79 | + | ```bash | |
| 124 | 80 | mkdir -p ~/.config/containers | |
| 125 | 81 | tee ~/.config/containers/registries.conf > /dev/null <<EOF | |
| 126 | 82 | [registries.search] | |
| @@ -128,9 +84,6 @@ registries = ['docker.io', 'registry.mydomain.com'] | |||
| 128 | 84 | ||
| 129 | 85 | [registries.insecure] | |
| 130 | 86 | registries = ['registry.mydomain.com'] | |
| 131 | - | ||
| 132 | - | [registries.block] | |
| 133 | - | registries = [] | |
| 134 | 87 | EOF | |
| 135 | 88 | ``` | |
| 136 | 89 | ||
| @@ -141,48 +94,31 @@ chmod +x gordon-linux-amd64 | |||
| 141 | 94 | sudo mv gordon-linux-amd64 /usr/local/bin/gordon | |
| 142 | 95 | ``` | |
| 143 | 96 | ||
| 144 | - | ### 6. Create Config file | |
| 145 | - | ```toml | |
| 146 | - | # Gordon searches for `gordon.toml` config in: ./ → ~/.config/gordon/ → ~/.gordon/ → ~/ → /etc/gordon/ | |
| 147 | - | # (or use --config flag) | |
| 148 | - | [server] | |
| 149 | - | port = 8080 | |
| 150 | - | registry_domain = "registry.mydomain.com" | |
| 151 | - | runtime = "podman-rootless" # Can be auto, docker, podman, podman-rootless | |
| 152 | - | ||
| 153 | - | [registry_auth] | |
| 154 | - | enabled = true | |
| 155 | - | username = "admin" | |
| 156 | - | password = "your-secure-password" | |
| 157 | - | ||
| 158 | - | [routes] | |
| 159 | - | "app.mydomain.com" = "myapp:latest" | |
| 160 | - | "api.mydomain.com" = "api:v1" | |
| 161 | - | "blog.mydomain.com" = "wordpress:latest" | |
| 162 | - | ||
| 163 | - | [auto_route] | |
| 164 | - | enabled = false # Enable automatic route creation | |
| 97 | + | ### 6. Configure Gordon | |
| 98 | + | ```bash | |
| 99 | + | # Gordon creates a default config file on first run | |
| 100 | + | # Edit it to set your registry domain and credentials: | |
| 101 | + | # gordon.toml location: ./ → ~/.config/gordon/ → ~/.gordon/ → ~/ → /etc/gordon/ | |
| 165 | 102 | ||
| 166 | - | # Custom config file location (optional): | |
| 167 | - | # gordon --config /path/to/custom.toml start | |
| 103 | + | # Key settings to update: | |
| 104 | + | # - registry_domain = "registry.mydomain.com" | |
| 105 | + | # - registry_auth username/password | |
| 106 | + | # - routes for your domains | |
| 168 | 107 | ``` | |
| 169 | 108 | ||
| 170 | - | ### 7. Point Cloudflare DNS | |
| 109 | + | ### 7. Point DNS | |
| 171 | 110 | ``` | |
| 172 | 111 | A *.mydomain.com → YOUR_VPS_IP | |
| 173 | 112 | A mydomain.com → YOUR_VPS_IP | |
| 174 | 113 | ``` | |
| 175 | 114 | ||
| 176 | - | ### 8. Create Systemd Service (Rootless) | |
| 115 | + | ### 8. Create Service | |
| 177 | 116 | ```bash | |
| 178 | - | # Create user systemd service (no root privileges needed!) | |
| 179 | 117 | mkdir -p ~/.config/systemd/user | |
| 180 | - | ||
| 181 | 118 | tee ~/.config/systemd/user/gordon.service > /dev/null <<EOF | |
| 182 | 119 | [Unit] | |
| 183 | - | Description=Gordon Container Platform (Rootless) | |
| 120 | + | Description=Gordon Container Platform | |
| 184 | 121 | After=podman.socket | |
| 185 | - | Requires=podman.socket | |
| 186 | 122 | ||
| 187 | 123 | [Service] | |
| 188 | 124 | Type=simple | |
| @@ -196,25 +132,17 @@ WorkingDirectory=%h | |||
| 196 | 132 | WantedBy=default.target | |
| 197 | 133 | EOF | |
| 198 | 134 | ||
| 199 | - | # Enable and start the user service | |
| 200 | 135 | systemctl --user daemon-reload | |
| 201 | 136 | systemctl --user enable --now gordon | |
| 202 | - | ||
| 203 | - | # Enable lingering to start service on boot | |
| 204 | 137 | sudo loginctl enable-linger $USER | |
| 205 | - | ||
| 206 | - | # Check service status | |
| 207 | - | systemctl --user status gordon | |
| 208 | 138 | ``` | |
| 209 | 139 | ||
| 210 | - | ### 8. Deploy Your First App | |
| 140 | + | ### 9. Deploy Your First App | |
| 211 | 141 | ```bash | |
| 212 | - | # Now from your local development machine: | |
| 213 | - | # Authenticate with your Gordon registry | |
| 142 | + | # From your dev machine: | |
| 214 | 143 | podman login registry.mydomain.com | |
| 215 | - | # Use the username/password from your gordon.toml config | |
| 144 | + | # (Use the credentials from your gordon.toml) | |
| 216 | 145 | ||
| 217 | - | # Deploy your first app (using podman) | |
| 218 | 146 | podman tag myapp:latest registry.mydomain.com/myapp:latest | |
| 219 | 147 | podman push registry.mydomain.com/myapp:latest | |
| 220 | 148 | # Visit https://app.mydomain.com | |
| @@ -222,342 +150,111 @@ podman push registry.mydomain.com/myapp:latest | |||
| 222 | 150 | ||
| 223 | 151 | ## Real-World Examples | |
| 224 | 152 | ||
| 225 | - | ### Deploy a Node.js App (The "Push-to-Deploy" Way) | |
| 226 | - | This is the simplest way to deploy. Your production environment is always in sync with your `latest` tag. | |
| 227 | - | ||
| 153 | + | ### Push-to-Deploy Workflow | |
| 228 | 154 | ```bash | |
| 229 | - | # 1. Build and test locally first | |
| 155 | + | # Build locally | |
| 230 | 156 | podman build -t myapp . | |
| 231 | - | podman run -p 3000:3000 myapp # Test it! | |
| 232 | 157 | ||
| 233 | - | # 2. In gordon.toml, point your domain to the 'latest' tag | |
| 234 | - | # "app.mydomain.com" = "myapp:latest" | |
| 235 | - | ||
| 236 | - | # 3. Tag your image as 'latest' for your registry | |
| 237 | - | podman tag myapp registry.mydomain.com/myapp:latest | |
| 238 | - | ||
| 239 | - | # 4. Push to deploy | |
| 158 | + | # Push latest = instant deploy | |
| 240 | 159 | podman push registry.mydomain.com/myapp:latest | |
| 241 | - | ||
| 242 | - | # Gordon automatically detects the push and deploys the new 'latest' version. | |
| 243 | 160 | ``` | |
| 244 | 161 | ||
| 245 | - | ### Smart Versioning Strategy | |
| 246 | - | For more control, especially in production, you can use a combination of version tags and the `latest` tag. This makes rollbacks trivial and allows for staging environments. | |
| 247 | - | ||
| 162 | + | ### Version Control & Rollbacks | |
| 248 | 163 | ```bash | |
| 249 | - | # 1. Always push a specific version tag first. This creates a history. | |
| 250 | - | podman tag myapp registry.mydomain.com/myapp:v1.0.1 | |
| 164 | + | # Push versioned releases | |
| 251 | 165 | podman push registry.mydomain.com/myapp:v1.0.1 | |
| 252 | 166 | ||
| 253 | - | # 2. Test the new version on a staging domain. | |
| 254 | - | # In gordon.toml: | |
| 255 | - | "staging.mydomain.com" = "myapp:v1.0.1" | |
| 256 | - | # Get your production route ready : | |
| 257 | - | "app.mydomain.com" = "myapp:latest" | |
| 258 | - | ||
| 259 | - | # 3. Once tested and confirmed, promote it to production by updating 'latest'. | |
| 260 | - | podman tag myapp registry.mydomain.com/myapp:latest | |
| 261 | - | podman push registry.mydomain.com/myapp:latest | |
| 262 | - | ``` | |
| 263 | - | ||
| 264 | - | ### Instant Rollback When Things Break | |
| 265 | - | ```toml | |
| 266 | - | # Production broke after the latest push? | |
| 267 | - | # Just edit gordon.toml to point to a previously pushed, stable version tag: | |
| 268 | - | ||
| 269 | - | # From: | |
| 270 | - | "app.mydomain.com" = "myapp:latest" | |
| 271 | - | ||
| 272 | - | # To: | |
| 273 | - | "app.mydomain.com" = "myapp:v1.0.0" | |
| 274 | - | ||
| 275 | - | # Save the file. Gordon redeploys the stable version in seconds. No scripts, no drama. | |
| 167 | + | # Instant rollback in gordon.toml: | |
| 168 | + | # From: "app.mydomain.com" = "myapp:latest" | |
| 169 | + | # To: "app.mydomain.com" = "myapp:v1.0.0" | |
| 276 | 170 | ``` | |
| 277 | 171 | ||
| 278 | - | ### Auto-Route Creation for Testing | |
| 279 | - | ||
| 280 | - | Gordon can automatically create routes from image names that contain valid domain names. This is perfect for testing deployments without manually editing config files. | |
| 281 | - | ||
| 282 | - | #### Enable Auto-Routes | |
| 172 | + | ### Auto-Route Creation | |
| 283 | 173 | ```toml | |
| 284 | 174 | [auto_route] | |
| 285 | - | enabled = true # Default: false | |
| 175 | + | enabled = true | |
| 286 | 176 | ``` | |
| 287 | 177 | ||
| 288 | - | #### How It Works | |
| 289 | - | When you push an image with a domain name as the image name, Gordon automatically creates a route: | |
| 290 | - | ||
| 291 | 178 | ```bash | |
| 292 | - | # Build your app normally | |
| 293 | - | podman build -t myapp . | |
| 294 | - | ||
| 295 | - | # Tag with domain name as the image name | |
| 296 | - | podman tag myapp:latest registry.mydomain.com/myapp.mydomain.dev:latest | |
| 297 | - | podman push registry.mydomain.com/myapp.mydomain.dev:latest | |
| 298 | - | ||
| 299 | - | # Gordon automatically creates: | |
| 300 | - | # "myapp.mydomain.dev" = "myapp.mydomain.dev:latest" | |
| 301 | - | # The route is added to your config file and deployed instantly! | |
| 179 | + | # Image name becomes route automatically | |
| 180 | + | podman push registry.mydomain.com/staging.example.com:latest | |
| 181 | + | # Creates: "staging.example.com" = "staging.example.com:latest" | |
| 302 | 182 | ``` | |
| 303 | 183 | ||
| 304 | - | #### Perfect for Testing Workflows | |
| 305 | - | ```bash | |
| 306 | - | # Test different domains for the same app | |
| 307 | - | podman build -t myapp . | |
| 308 | - | ||
| 309 | - | # Push to different test domains using image names | |
| 310 | - | podman tag myapp:latest registry.mydomain.com/staging.example.com:latest | |
| 311 | - | podman push registry.mydomain.com/staging.example.com:latest | |
| 184 | + | ## Advanced Configuration | |
| 312 | 185 | ||
| 313 | - | podman tag myapp:latest registry.mydomain.com/demo.example.com:v1.0.0 | |
| 314 | - | podman push registry.mydomain.com/demo.example.com:v1.0.0 | |
| 186 | + | ### Full Config Structure | |
| 187 | + | ```toml | |
| 188 | + | # gordon.toml - Auto-generated on first run | |
| 189 | + | [server] | |
| 190 | + | port = 8080 | |
| 191 | + | registry_domain = "registry.mydomain.com" | |
| 192 | + | runtime = "podman-rootless" # auto, docker, podman, podman-rootless | |
| 193 | + | # runtime auto-detects: docker → podman-rootless → podman | |
| 194 | + | # Override with env: CONTAINER_HOST=unix:///custom/socket | |
| 315 | 195 | ||
| 316 | - | # Both automatically get their own routes: | |
| 317 | - | # staging.example.com -> staging.example.com:latest | |
| 318 | - | # demo.example.com -> demo.example.com:v1.0.0 | |
| 319 | - | ``` | |
| 196 | + | [registry_auth] | |
| 197 | + | enabled = true | |
| 198 | + | username = "admin" | |
| 199 | + | password = "your-secure-password" | |
| 320 | 200 | ||
| 321 | - | #### Domain Validation | |
| 322 | - | Gordon only creates auto-routes for valid domain names in image names: | |
| 323 | - | - ✅ `api.example.com:latest` → Valid subdomain name, route created | |
| 324 | - | - ✅ `myapp.dev:v1.0.0` → Valid domain name, route created | |
| 325 | - | - ❌ `myapp:latest` → Not a domain, ignored | |
| 326 | - | - ❌ `myapp:v1.0.0` → Not a domain, ignored | |
| 327 | - | - ❌ `localhost:latest` → Not a valid domain, ignored | |
| 201 | + | [routes] | |
| 202 | + | "app.mydomain.com" = "myapp:latest" | |
| 328 | 203 | ||
| 329 | - | #### Route Precedence | |
| 330 | - | - Existing routes in `[routes]` are never overwritten | |
| 331 | - | - Auto-routes are saved to your config file permanently | |
| 332 | - | - You can manually edit or remove auto-created routes anytime | |
| 204 | + | [auto_route] | |
| 205 | + | enabled = false # Auto-create routes from image names | |
| 333 | 206 | ||
| 207 | + | [env] | |
| 208 | + | dir = "./data/env" # Gordon auto-creates env files here | |
| 209 | + | providers = ["pass", "sops"] # Optional secret management | |
| 210 | + | # Example env file: ./data/env/app_mydomain_com.env | |
| 211 | + | # NODE_ENV=production | |
| 212 | + | # DATABASE_URL=postgresql://localhost:5432/prod | |
| 213 | + | # API_KEY=${pass:company/api-key} # Unix pass integration | |
| 214 | + | # JWT_SECRET=${sops:secrets.yaml:jwt} # SOPS integration | |
| 215 | + | ||
| 216 | + | [logging] | |
| 217 | + | enabled = false | |
| 218 | + | level = "info" # trace, debug, info, warn, error | |
| 219 | + | dir = "./logs" # Log directory | |
| 220 | + | main_log_file = "gordon.log" # Main app log | |
| 221 | + | proxy_log_file = "proxy.log" # HTTP traffic log | |
| 222 | + | container_log_dir = "containers" # Container logs subdirectory | |
| 223 | + | max_size = 100 # MB before rotation | |
| 224 | + | max_backups = 3 # Number of old files to keep | |
| 225 | + | max_age = 28 # Days to keep old files | |
| 226 | + | compress = true # Gzip rotated logs | |
| 227 | + | # Creates: gordon.log, proxy.log, containers/*.log | |
| 228 | + | ``` | |
| 334 | 229 | ||
| 335 | 230 | ## FAQ | |
| 336 | 231 | ||
| 337 | 232 | **Q: How is this different from Traefik/Nginx Proxy Manager?** | |
| 338 | - | A: Those are just reverse proxies - you still need to manually start/stop containers. Gordon is a complete deployment platform: built-in registry, automatic deployment on push, and container lifecycle management. It's the difference between a router and a full PaaS. | |
| 233 | + | A: Those are just reverse proxies. Gordon is a complete deployment platform with built-in registry, automatic deployment, and container lifecycle management. | |
| 339 | 234 | ||
| 340 | 235 | **Q: Where do builds happen?** | |
| 341 | - | A: On YOUR machine. If it runs locally, it'll run in production. No mysterious build server issues. | |
| 342 | - | ||
| 343 | - | **Q: How do I handle broken deployments?** | |
| 344 | - | A: Just edit gordon.toml to point to a previous version. Rollback takes seconds, not minutes. | |
| 236 | + | A: On YOUR machine. If it runs locally, it runs in production. | |
| 345 | 237 | ||
| 346 | - | **Q: Do I need CI/CD?** | |
| 347 | - | A: Nope! Your computer is the CI/CD. Build, test locally, push when ready. Keep it stupid simple. | |
| 238 | + | **Q: How do I rollback?** | |
| 239 | + | A: Edit gordon.toml to point to a previous version. Takes seconds. | |
| 348 | 240 | ||
| 349 | 241 | **Q: Do I need Cloudflare?** | |
| 350 | - | A: Yes for SSL certificates. Gordon doesn't handle Let's Encrypt yet, so Cloudflare provides the SSL termination and DDoS protection. | |
| 351 | - | ||
| 352 | - | **Q: Can I run databases?** | |
| 353 | - | A: You can in theory, but exposing databases on the internet is risky. Database/app isolation is coming soon for safer database deployments. | |
| 242 | + | A: Yes, for SSL certificates and DDoS protection. | |
| 354 | 243 | ||
| 355 | 244 | **Q: Resource requirements?** | |
| 356 | - | A: Runs comfortably on 1GB RAM VPS. Gordon itself uses <15MB of RAM. | |
| 357 | - | ||
| 358 | - | **Q: What about secrets?** | |
| 359 | - | A: Gordon provides built-in environment variable support with secret management via `pass` and `sops`. See the Environment Variables section below. | |
| 360 | - | ||
| 361 | - | **Q: How does auto-route creation work?** | |
| 362 | - | A: When enabled, Gordon watches for pushed images with domain names as image names (like `staging.example.com:latest`) and automatically creates routes. Perfect for testing deployments without manual config edits. | |
| 245 | + | A: Runs on 1GB RAM VPS. Gordon uses <15MB RAM. | |
| 363 | 246 | ||
| 364 | - | ## Philosophy: Build Local, Deploy Simple | |
| 365 | - | ||
| 366 | - | ### Why No Build Servers? | |
| 367 | - | ||
| 368 | - | 1. **Your machine already works** - Why replicate your dev environment in CI/CD? | |
| 369 | - | 2. **Instant feedback** - Build errors show up immediately, not after pushing | |
| 370 | - | 3. **Perfect reproducibility** - "Works on my machine" becomes a feature, not a bug | |
| 371 | - | 4. **Zero build queues** - Deploy as fast as your internet can push | |
| 372 | - | ||
| 373 | - | ### The Gordon Way | |
| 247 | + | ## Philosophy | |
| 374 | 248 | ||
| 375 | 249 | ```bash | |
| 376 | 250 | # Traditional CI/CD | |
| 377 | 251 | push code → wait for build → hope it works → debug remotely → repeat | |
| 378 | 252 | ||
| 379 | - | # The Gordon Way (with Podman) | |
| 380 | - | podman build locally → test locally → push image → instant deploy | |
| 381 | - | ``` | |
| 382 | - | ||
| 383 | - | ## Architecture | |
| 384 | - | ||
| 385 | - | - **Event-Driven**: Push events trigger deployments automatically | |
| 386 | - | - **Config Hot-Reload**: Edit gordon.toml, changes apply instantly | |
| 387 | - | - **Single Binary**: No dependencies except Docker | |
| 388 | - | - **Stateless**: Configuration is the only state | |
| 389 | - | - **Fast**: Written in Go for minimal overhead | |
| 390 | - | ||
| 391 | - | ## Advanced Usage | |
| 392 | - | ||
| 393 | - | ### Container Runtime Configuration | |
| 394 | - | ```toml | |
| 395 | - | [server] | |
| 396 | - | runtime = "auto" # auto, docker, podman, podman-rootless | |
| 397 | - | socket_path = "" # optional custom socket path | |
| 398 | - | ||
| 399 | - | # Examples: | |
| 400 | - | # runtime = "docker" | |
| 401 | - | # runtime = "podman" | |
| 402 | - | # socket_path = "unix:///run/user/1000/podman/podman.sock" | |
| 403 | - | ``` | |
| 404 | - | ||
| 405 | - | ### Environment Override | |
| 406 | - | ```bash | |
| 407 | - | # Override container socket with environment variable | |
| 408 | - | export CONTAINER_HOST=unix:///custom/path/container.sock | |
| 409 | - | export CONTAINER_HOST=tcp://remote-docker:2376 | |
| 410 | - | ||
| 411 | - | # Works for both Docker and Podman | |
| 412 | - | gordon start | |
| 413 | - | ``` | |
| 414 | - | ||
| 415 | - | ### Runtime Auto-Detection | |
| 416 | - | Gordon automatically detects available container runtimes in this order: | |
| 417 | - | 1. **Docker** (`/var/run/docker.sock`) | |
| 418 | - | 2. **Podman rootless** (`$XDG_RUNTIME_DIR/podman/podman.sock`) | |
| 419 | - | 3. **Podman root** (`/run/podman/podman.sock`) | |
| 420 | - | ||
| 421 | - | ### Environment Variables & Secret Management | |
| 422 | - | ||
| 423 | - | Gordon supports per-route environment variables with secure secret management. | |
| 424 | - | ||
| 425 | - | #### Configuration | |
| 426 | - | ```toml | |
| 427 | - | [env] | |
| 428 | - | # Directory where .env files are stored for each route | |
| 429 | - | dir = "./data/env" # Default location | |
| 430 | - | # Secret providers for secure credential management | |
| 431 | - | providers = ["pass", "sops"] # Unix password manager and SOPS | |
| 432 | - | ``` | |
| 433 | - | ||
| 434 | - | #### Creating Environment Files | |
| 435 | - | Gordon automatically creates empty `.env` files for all configured routes when it starts up. The files are named after the domain with special characters replaced by underscores: | |
| 436 | - | ||
| 437 | - | ```bash | |
| 438 | - | # Gordon automatically creates these files for you: | |
| 439 | - | # For route "app.mydomain.com" | |
| 440 | - | ./data/env/app_mydomain_com.env | |
| 441 | - | ||
| 442 | - | # For route "api-v2.mydomain.com" | |
| 443 | - | ./data/env/api-v2_mydomain_com.env | |
| 444 | - | ``` | |
| 445 | - | ||
| 446 | - | #### Environment File Format | |
| 447 | - | When Gordon creates env files automatically, they include helpful comments and examples. | |
| 448 | - | ||
| 449 | - | **Security:** Gordon creates the env directory with `0700` permissions and env files with `0600` permissions (owner read/write only) to protect sensitive information. | |
| 450 | - | ||
| 451 | - | ```bash | |
| 452 | - | # ./data/env/app_mydomain_com.env (automatically created by Gordon) | |
| 453 | - | # Environment variables for route: app.mydomain.com | |
| 454 | - | # Image: myapp:latest | |
| 455 | - | ||
| 456 | - | NODE_ENV=production | |
| 457 | - | PORT=3000 | |
| 458 | - | DATABASE_URL=postgresql://localhost:5432/prod | |
| 459 | - | ||
| 460 | - | # Secret references (resolved at container start) | |
| 461 | - | API_KEY=${pass:company/api-key} | |
| 462 | - | JWT_SECRET=${sops:secrets.yaml:app.jwt_secret} | |
| 463 | - | ``` | |
| 464 | - | ||
| 465 | - | #### Secret Providers | |
| 466 | - | ||
| 467 | - | **Unix Password Manager (pass)** | |
| 468 | - | ```bash | |
| 469 | - | # Store secrets with pass | |
| 470 | - | pass insert company/api-key | |
| 471 | - | pass insert company/database-password | |
| 472 | - | ||
| 473 | - | # Reference in .env files | |
| 474 | - | API_KEY=${pass:company/api-key} | |
| 475 | - | DB_PASSWORD=${pass:company/database-password} | |
| 476 | - | ``` | |
| 477 | - | ||
| 478 | - | **SOPS (Secrets OPerationS)** | |
| 479 | - | ```bash | |
| 480 | - | # Create encrypted YAML file | |
| 481 | - | sops secrets.yaml | |
| 482 | - | ||
| 483 | - | # secrets.yaml content: | |
| 484 | - | # app: | |
| 485 | - | # jwt_secret: "super-secret-key" | |
| 486 | - | # database_url: "postgresql://user:pass@db:5432/prod" | |
| 487 | - | ||
| 488 | - | # Reference in .env files | |
| 489 | - | JWT_SECRET=${sops:secrets.yaml:app.jwt_secret} | |
| 490 | - | DATABASE_URL=${sops:secrets.yaml:app.database_url} | |
| 491 | - | ``` | |
| 492 | - | ||
| 493 | - | #### How It Works | |
| 494 | - | 1. Gordon loads the `.env` file for each route during container deployment | |
| 495 | - | 2. Secret references (`${provider:path}`) are resolved using the configured providers | |
| 496 | - | 3. Environment variables are passed to the container at startup | |
| 497 | - | 4. Missing env files are silently ignored (optional env vars) | |
| 498 | - | 5. Secret resolution failures cause deployment to fail for security | |
| 499 | - | ||
| 500 | - | #### Examples | |
| 501 | - | ||
| 502 | - | **Development Setup** | |
| 503 | - | ```toml | |
| 504 | - | # gordon.toml | |
| 505 | - | [env] | |
| 506 | - | dir = "./dev-data/env" | |
| 507 | - | providers = ["pass"] # Optional for dev secrets | |
| 508 | - | ||
| 509 | - | [routes] | |
| 510 | - | "app.local" = "myapp:latest" | |
| 253 | + | # The Gordon Way | |
| 254 | + | build locally → test locally → push image → instant deploy | |
| 511 | 255 | ``` | |
| 512 | 256 | ||
| 513 | - | ```bash | |
| 514 | - | # ./dev-data/env/app_local.env | |
| 515 | - | NODE_ENV=development | |
| 516 | - | DEBUG=true | |
| 517 | - | DATABASE_URL=postgresql://localhost:5432/dev | |
| 518 | - | API_KEY=${pass:dev/api-key} # Optional: use pass for dev secrets | |
| 519 | - | ``` | |
| 520 | - | ||
| 521 | - | **Production Setup** | |
| 522 | - | ```toml | |
| 523 | - | # gordon.toml | |
| 524 | - | [env] | |
| 525 | - | dir = "/var/lib/gordon/env" | |
| 526 | - | providers = ["pass", "sops"] | |
| 527 | - | ||
| 528 | - | [routes] | |
| 529 | - | "app.company.com" = "company-app:v2.1.0" | |
| 530 | - | ``` | |
| 531 | - | ||
| 532 | - | ```bash | |
| 533 | - | # /var/lib/gordon/env/app_company_com.env | |
| 534 | - | NODE_ENV=production | |
| 535 | - | DATABASE_URL=postgresql://user:${pass:company/db-password}@db:5432/prod | |
| 536 | - | API_KEY=${sops:company-secrets.yaml:api.key} | |
| 537 | - | JWT_SECRET=${sops:company-secrets.yaml:jwt.secret} | |
| 538 | - | ``` | |
| 539 | - | ||
| 540 | - | ### Port Binding | |
| 541 | - | Gordon requires your Dockerfile to explicitly expose ports using the `EXPOSE` instruction. When multiple ports are exposed, Gordon will use the first exposed port for HTTP traffic. | |
| 542 | - | ||
| 543 | - | Example Dockerfile: | |
| 544 | - | ``` | |
| 545 | - | ENTRYPOINT ["/myapp"] | |
| 546 | - | EXPOSE 8080 | |
| 547 | - | ``` | |
| 548 | - | ||
| 549 | - | ### Registry Operations | |
| 550 | - | ```bash | |
| 551 | - | # List images | |
| 552 | - | curl -u admin:password https://registry.mydomain.com/v2/_catalog | |
| 553 | - | ||
| 554 | - | # List tags | |
| 555 | - | curl -u admin:password https://registry.mydomain.com/v2/myapp/tags/list | |
| 556 | - | ``` | |
| 557 | - | ||
| 558 | - | ## Contributing | |
| 559 | - | ||
| 560 | - | Gordon is open source and we love contributions! Check out our [issues](https://github.com/yourusername/gordon/issues) or submit a PR. | |
| 257 | + | Your machine already works. Why replicate it in CI/CD? | |
| 561 | 258 | ||
| 562 | 259 | ## License | |
| 563 | 260 | ||
| @@ -13,7 +13,6 @@ | |||||
| 13 | Gordon is the missing piece that makes container deployment on budget VPS servers as simple as expensive PaaS solutions. One binary, one config file, unlimited apps. | 13 | Gordon is the missing piece that makes container deployment on budget VPS servers as simple as expensive PaaS solutions. One binary, one config file, unlimited apps. | ||
| 14 | 14 | ||||
| 15 | ### Perfect For | 15 | ### Perfect For | ||
| 16 | - | ||||
| 17 | - **Solo developers** with multiple side projects on one VPS | 16 | - **Solo developers** with multiple side projects on one VPS | ||
| 18 | - **Small teams** who want Heroku-like simplicity without the cost | 17 | - **Small teams** who want Heroku-like simplicity without the cost | ||
| 19 | - **Agencies** deploying client apps across VPS servers | 18 | - **Agencies** deploying client apps across VPS servers | ||
| @@ -21,10 +20,8 @@ Gordon is the missing piece that makes container deployment on budget VPS server | |||||
| 21 | 20 | ||||
| 22 | ## How It Works | 21 | ## How It Works | ||
| 23 | 22 | ||||
| 24 | - | ### Build Locally, Deploy Instantly | |||
| 25 | - | ||||
| 26 | ```bash | 23 | ```bash | ||
| 27 | - | # 1. Build & test on YOUR machine | 24 | + | # 1. Build & test locally |
| 28 | podman build -t myapp . | 25 | podman build -t myapp . | ||
| 29 | podman run -p 8080:8080 myapp # Works? Great! | 26 | podman run -p 8080:8080 myapp # Works? Great! | ||
| 30 | 27 | ||||
| @@ -37,90 +34,49 @@ podman push registry.mydomain.com/myapp:latest | |||||
| 37 | 34 | ||||
| 38 | **No build servers. No CI/CD complexity. If it runs on your machine, it runs in production.** | 35 | **No build servers. No CI/CD complexity. If it runs on your machine, it runs in production.** | ||
| 39 | 36 | ||||
| 40 | - | ### Instant Rollbacks | |||
| 41 | - | ||||
| 42 | - | Something broke? Just change your config: | |||
| 43 | - | ||||
| 44 | - | ```toml | |||
| 45 | - | # Before (in gordon.toml) | |||
| 46 | - | "app.mydomain.com" = "myapp:latest" | |||
| 47 | - | ||||
| 48 | - | # After - instant rollback! | |||
| 49 | - | "app.mydomain.com" = "myapp:v1.2.3" | |||
| 50 | - | ``` | |||
| 51 | - | ||||
| 52 | - | Save the file. Gordon redeploys the previous version. Problem solved in seconds. | |||
| 53 | - | ||||
| 54 | ## Key Features | 37 | ## Key Features | ||
| 55 | 38 | ||||
| 56 | - | ### Local-First Development | 39 | + | - **Local-First Development**: Your machine is the build server |
| 57 | - | **Your machine is the build server.** Test locally with your favorite container engine, push when ready. No waiting for remote builds. | 40 | + | - **Built-in Container Registry**: Your VPS becomes a private registry |
| 58 | - | 41 | + | - **Push-to-Deploy**: New images deploy instantly | |
| 59 | - | ### Built-in Container Registry | 42 | + | - **Multi-Domain Routing**: Unlimited apps with automatic HTTPS via Cloudflare |
| 60 | - | Your VPS becomes a private container registry (Docker/Podman compatible). No Docker Hub subscription needed. | 43 | + | - **Zero-Downtime Updates**: Graceful container swaps |
| 61 | - | 44 | + | - **Auto-Route Creation**: Push `myapp.mydomain.com:latest` → route created automatically | |
| 62 | - | ### Push-to-Deploy Magic | |||
| 63 | - | Gordon watches for new images and deploys them instantly to configured domains. | |||
| 64 | - | ||||
| 65 | - | ### Version Control Built-In | |||
| 66 | - | Keep multiple versions in your registry. Switch between them instantly by editing the config. | |||
| 67 | - | ||||
| 68 | - | ### Multi-Domain Routing | |||
| 69 | - | Run unlimited apps on one server. Each gets its own domain with automatic HTTPS via Cloudflare. | |||
| 70 | - | ||||
| 71 | - | ### Zero-Downtime Updates | |||
| 72 | - | Push new versions anytime. Gordon handles graceful container swaps. | |||
| 73 | - | ||||
| 74 | - | ### Automatic Deployment | |||
| 75 | - | Containers deploy instantly when you push new images. No manual deployment steps needed. | |||
| 76 | - | ||||
| 77 | - | ### Auto-Route Creation | |||
| 78 | - | Automatically create routes from image names containing domains. Push an image named like `myapp.mydomain.com:latest` and Gordon creates the route automatically when enabled. | |||
| 79 | 45 | ||||
| 80 | ## Quick Start (5 minutes) | 46 | ## Quick Start (5 minutes) | ||
| 81 | 47 | ||||
| 82 | - | ### 1. Get a VPS, Install Podman and UFW | 48 | + | ### 1. Setup VPS |
| 83 | ```bash | 49 | ```bash | ||
| 84 | - | # Any VPS provider: DigitalOcean, Linode, Vultr, Hetzner | 50 | + | # Install essentials on Ubuntu/Debian |
| 85 | - | # Ubuntu/Debian recommended | 51 | + | sudo apt update && sudo apt install -y podman ufw |
| 86 | - | ||||
| 87 | - | # Install Essentials | |||
| 88 | - | sudo apt update | |||
| 89 | - | sudo apt install -y podman ufw | |||
| 90 | 52 | ||||
| 91 | - | # Configure UFW firewall first | 53 | + | # Configure firewall |
| 92 | sudo ufw --force enable | 54 | sudo ufw --force enable | ||
| 93 | sudo ufw default deny incoming | 55 | sudo ufw default deny incoming | ||
| 94 | sudo ufw default allow outgoing | 56 | sudo ufw default allow outgoing | ||
| 95 | - | sudo ufw allow 22/tcp | 57 | + | sudo ufw allow 22/tcp 80/tcp 443/tcp |
| 96 | - | sudo ufw allow 80/tcp | |||
| 97 | - | sudo ufw allow 443/tcp | |||
| 98 | sudo ufw enable | 58 | sudo ufw enable | ||
| 99 | ``` | 59 | ``` | ||
| 100 | - | ### 2. Redirect Ports 80/443 to 8080 | 60 | + | |
| 61 | + | ### 2. Redirect Ports | |||
| 101 | ```bash | 62 | ```bash | ||
| 102 | - | # Simple iptables redirect | 63 | + | # Redirect 80/443 to 8080 |
| 103 | sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 8080 | 64 | sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 8080 | ||
| 104 | sudo iptables -t nat -A PREROUTING -p tcp --dport 443 -j REDIRECT --to-port 8080 | 65 | sudo iptables -t nat -A PREROUTING -p tcp --dport 443 -j REDIRECT --to-port 8080 | ||
| 105 | - | ||||
| 106 | - | # Make rules persistent across reboots | |||
| 107 | sudo apt install -y iptables-persistent | 66 | sudo apt install -y iptables-persistent | ||
| 108 | sudo netfilter-persistent save | 67 | sudo netfilter-persistent save | ||
| 109 | ``` | 68 | ``` | ||
| 110 | - | ### 3. Enable rootless mode for enhanced security | 69 | + | |
| 111 | - | ``` | 70 | + | ### 3. Enable Rootless Containers |
| 71 | + | ```bash | |||
| 112 | echo 'user.max_user_namespaces=28633' | sudo tee -a /etc/sysctl.conf | 72 | echo 'user.max_user_namespaces=28633' | sudo tee -a /etc/sysctl.conf | ||
| 113 | sudo sysctl -p | 73 | sudo sysctl -p | ||
| 114 | - | ||||
| 115 | - | # Configure user for rootless containers | |||
| 116 | sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 $USER | 74 | sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 $USER | ||
| 117 | - | ||||
| 118 | - | # Start user services (no root required!) | |||
| 119 | systemctl --user enable --now podman.socket | 75 | systemctl --user enable --now podman.socket | ||
| 120 | ``` | 76 | ``` | ||
| 121 | 77 | ||||
| 122 | - | ### 4. Configure registries for your Gordon registry | 78 | + | ### 4. Configure Registry |
| 123 | - | ``` | 79 | + | ```bash |
| 124 | mkdir -p ~/.config/containers | 80 | mkdir -p ~/.config/containers | ||
| 125 | tee ~/.config/containers/registries.conf > /dev/null <<EOF | 81 | tee ~/.config/containers/registries.conf > /dev/null <<EOF | ||
| 126 | [registries.search] | 82 | [registries.search] | ||
| @@ -128,9 +84,6 @@ registries = ['docker.io', 'registry.mydomain.com'] | |||||
| 128 | 84 | ||||
| 129 | [registries.insecure] | 85 | [registries.insecure] | ||
| 130 | registries = ['registry.mydomain.com'] | 86 | registries = ['registry.mydomain.com'] | ||
| 131 | - | ||||
| 132 | - | [registries.block] | |||
| 133 | - | registries = [] | |||
| 134 | EOF | 87 | EOF | ||
| 135 | ``` | 88 | ``` | ||
| 136 | 89 | ||||
| @@ -141,48 +94,31 @@ chmod +x gordon-linux-amd64 | |||||
| 141 | sudo mv gordon-linux-amd64 /usr/local/bin/gordon | 94 | sudo mv gordon-linux-amd64 /usr/local/bin/gordon | ||
| 142 | ``` | 95 | ``` | ||
| 143 | 96 | ||||
| 144 | - | ### 6. Create Config file | 97 | + | ### 6. Configure Gordon |
| 145 | - | ```toml | 98 | + | ```bash |
| 146 | - | # Gordon searches for `gordon.toml` config in: ./ → ~/.config/gordon/ → ~/.gordon/ → ~/ → /etc/gordon/ | 99 | + | # Gordon creates a default config file on first run |
| 147 | - | # (or use --config flag) | 100 | + | # Edit it to set your registry domain and credentials: |
| 148 | - | [server] | 101 | + | # gordon.toml location: ./ → ~/.config/gordon/ → ~/.gordon/ → ~/ → /etc/gordon/ |
| 149 | - | port = 8080 | |||
| 150 | - | registry_domain = "registry.mydomain.com" | |||
| 151 | - | runtime = "podman-rootless" # Can be auto, docker, podman, podman-rootless | |||
| 152 | - | ||||
| 153 | - | [registry_auth] | |||
| 154 | - | enabled = true | |||
| 155 | - | username = "admin" | |||
| 156 | - | password = "your-secure-password" | |||
| 157 | - | ||||
| 158 | - | [routes] | |||
| 159 | - | "app.mydomain.com" = "myapp:latest" | |||
| 160 | - | "api.mydomain.com" = "api:v1" | |||
| 161 | - | "blog.mydomain.com" = "wordpress:latest" | |||
| 162 | - | ||||
| 163 | - | [auto_route] | |||
| 164 | - | enabled = false # Enable automatic route creation | |||
| 165 | 102 | ||||
| 166 | - | # Custom config file location (optional): | 103 | + | # Key settings to update: |
| 167 | - | # gordon --config /path/to/custom.toml start | 104 | + | # - registry_domain = "registry.mydomain.com" |
| 105 | + | # - registry_auth username/password | |||
| 106 | + | # - routes for your domains | |||
| 168 | ``` | 107 | ``` | ||
| 169 | 108 | ||||
| 170 | - | ### 7. Point Cloudflare DNS | 109 | + | ### 7. Point DNS |
| 171 | ``` | 110 | ``` | ||
| 172 | A *.mydomain.com → YOUR_VPS_IP | 111 | A *.mydomain.com → YOUR_VPS_IP | ||
| 173 | A mydomain.com → YOUR_VPS_IP | 112 | A mydomain.com → YOUR_VPS_IP | ||
| 174 | ``` | 113 | ``` | ||
| 175 | 114 | ||||
| 176 | - | ### 8. Create Systemd Service (Rootless) | 115 | + | ### 8. Create Service |
| 177 | ```bash | 116 | ```bash | ||
| 178 | - | # Create user systemd service (no root privileges needed!) | |||
| 179 | mkdir -p ~/.config/systemd/user | 117 | mkdir -p ~/.config/systemd/user | ||
| 180 | - | ||||
| 181 | tee ~/.config/systemd/user/gordon.service > /dev/null <<EOF | 118 | tee ~/.config/systemd/user/gordon.service > /dev/null <<EOF | ||
| 182 | [Unit] | 119 | [Unit] | ||
| 183 | - | Description=Gordon Container Platform (Rootless) | 120 | + | Description=Gordon Container Platform |
| 184 | After=podman.socket | 121 | After=podman.socket | ||
| 185 | - | Requires=podman.socket | |||
| 186 | 122 | ||||
| 187 | [Service] | 123 | [Service] | ||
| 188 | Type=simple | 124 | Type=simple | ||
| @@ -196,25 +132,17 @@ WorkingDirectory=%h | |||||
| 196 | WantedBy=default.target | 132 | WantedBy=default.target | ||
| 197 | EOF | 133 | EOF | ||
| 198 | 134 | ||||
| 199 | - | # Enable and start the user service | |||
| 200 | systemctl --user daemon-reload | 135 | systemctl --user daemon-reload | ||
| 201 | systemctl --user enable --now gordon | 136 | systemctl --user enable --now gordon | ||
| 202 | - | ||||
| 203 | - | # Enable lingering to start service on boot | |||
| 204 | sudo loginctl enable-linger $USER | 137 | sudo loginctl enable-linger $USER | ||
| 205 | - | ||||
| 206 | - | # Check service status | |||
| 207 | - | systemctl --user status gordon | |||
| 208 | ``` | 138 | ``` | ||
| 209 | 139 | ||||
| 210 | - | ### 8. Deploy Your First App | 140 | + | ### 9. Deploy Your First App |
| 211 | ```bash | 141 | ```bash | ||
| 212 | - | # Now from your local development machine: | 142 | + | # From your dev machine: |
| 213 | - | # Authenticate with your Gordon registry | |||
| 214 | podman login registry.mydomain.com | 143 | podman login registry.mydomain.com | ||
| 215 | - | # Use the username/password from your gordon.toml config | 144 | + | # (Use the credentials from your gordon.toml) |
| 216 | 145 | ||||
| 217 | - | # Deploy your first app (using podman) | |||
| 218 | podman tag myapp:latest registry.mydomain.com/myapp:latest | 146 | podman tag myapp:latest registry.mydomain.com/myapp:latest | ||
| 219 | podman push registry.mydomain.com/myapp:latest | 147 | podman push registry.mydomain.com/myapp:latest | ||
| 220 | # Visit https://app.mydomain.com | 148 | # Visit https://app.mydomain.com | ||
| @@ -222,342 +150,111 @@ podman push registry.mydomain.com/myapp:latest | |||||
| 222 | 150 | ||||
| 223 | ## Real-World Examples | 151 | ## Real-World Examples | ||
| 224 | 152 | ||||
| 225 | - | ### Deploy a Node.js App (The "Push-to-Deploy" Way) | 153 | + | ### Push-to-Deploy Workflow |
| 226 | - | This is the simplest way to deploy. Your production environment is always in sync with your `latest` tag. | |||
| 227 | - | ||||
| 228 | ```bash | 154 | ```bash | ||
| 229 | - | # 1. Build and test locally first | 155 | + | # Build locally |
| 230 | podman build -t myapp . | 156 | podman build -t myapp . | ||
| 231 | - | podman run -p 3000:3000 myapp # Test it! | |||
| 232 | 157 | ||||
| 233 | - | # 2. In gordon.toml, point your domain to the 'latest' tag | 158 | + | # Push latest = instant deploy |
| 234 | - | # "app.mydomain.com" = "myapp:latest" | |||
| 235 | - | ||||
| 236 | - | # 3. Tag your image as 'latest' for your registry | |||
| 237 | - | podman tag myapp registry.mydomain.com/myapp:latest | |||
| 238 | - | ||||
| 239 | - | # 4. Push to deploy | |||
| 240 | podman push registry.mydomain.com/myapp:latest | 159 | podman push registry.mydomain.com/myapp:latest | ||
| 241 | - | ||||
| 242 | - | # Gordon automatically detects the push and deploys the new 'latest' version. | |||
| 243 | ``` | 160 | ``` | ||
| 244 | 161 | ||||
| 245 | - | ### Smart Versioning Strategy | 162 | + | ### Version Control & Rollbacks |
| 246 | - | For more control, especially in production, you can use a combination of version tags and the `latest` tag. This makes rollbacks trivial and allows for staging environments. | |||
| 247 | - | ||||
| 248 | ```bash | 163 | ```bash | ||
| 249 | - | # 1. Always push a specific version tag first. This creates a history. | 164 | + | # Push versioned releases |
| 250 | - | podman tag myapp registry.mydomain.com/myapp:v1.0.1 | |||
| 251 | podman push registry.mydomain.com/myapp:v1.0.1 | 165 | podman push registry.mydomain.com/myapp:v1.0.1 | ||
| 252 | 166 | ||||
| 253 | - | # 2. Test the new version on a staging domain. | 167 | + | # Instant rollback in gordon.toml: |
| 254 | - | # In gordon.toml: | 168 | + | # From: "app.mydomain.com" = "myapp:latest" |
| 255 | - | "staging.mydomain.com" = "myapp:v1.0.1" | 169 | + | # To: "app.mydomain.com" = "myapp:v1.0.0" |
| 256 | - | # Get your production route ready : | |||
| 257 | - | "app.mydomain.com" = "myapp:latest" | |||
| 258 | - | ||||
| 259 | - | # 3. Once tested and confirmed, promote it to production by updating 'latest'. | |||
| 260 | - | podman tag myapp registry.mydomain.com/myapp:latest | |||
| 261 | - | podman push registry.mydomain.com/myapp:latest | |||
| 262 | - | ``` | |||
| 263 | - | ||||
| 264 | - | ### Instant Rollback When Things Break | |||
| 265 | - | ```toml | |||
| 266 | - | # Production broke after the latest push? | |||
| 267 | - | # Just edit gordon.toml to point to a previously pushed, stable version tag: | |||
| 268 | - | ||||
| 269 | - | # From: | |||
| 270 | - | "app.mydomain.com" = "myapp:latest" | |||
| 271 | - | ||||
| 272 | - | # To: | |||
| 273 | - | "app.mydomain.com" = "myapp:v1.0.0" | |||
| 274 | - | ||||
| 275 | - | # Save the file. Gordon redeploys the stable version in seconds. No scripts, no drama. | |||
| 276 | ``` | 170 | ``` | ||
| 277 | 171 | ||||
| 278 | - | ### Auto-Route Creation for Testing | 172 | + | ### Auto-Route Creation |
| 279 | - | ||||
| 280 | - | Gordon can automatically create routes from image names that contain valid domain names. This is perfect for testing deployments without manually editing config files. | |||
| 281 | - | ||||
| 282 | - | #### Enable Auto-Routes | |||
| 283 | ```toml | 173 | ```toml | ||
| 284 | [auto_route] | 174 | [auto_route] | ||
| 285 | - | enabled = true # Default: false | 175 | + | enabled = true |
| 286 | ``` | 176 | ``` | ||
| 287 | 177 | ||||
| 288 | - | #### How It Works | |||
| 289 | - | When you push an image with a domain name as the image name, Gordon automatically creates a route: | |||
| 290 | - | ||||
| 291 | ```bash | 178 | ```bash | ||
| 292 | - | # Build your app normally | 179 | + | # Image name becomes route automatically |
| 293 | - | podman build -t myapp . | 180 | + | podman push registry.mydomain.com/staging.example.com:latest |
| 294 | - | 181 | + | # Creates: "staging.example.com" = "staging.example.com:latest" | |
| 295 | - | # Tag with domain name as the image name | |||
| 296 | - | podman tag myapp:latest registry.mydomain.com/myapp.mydomain.dev:latest | |||
| 297 | - | podman push registry.mydomain.com/myapp.mydomain.dev:latest | |||
| 298 | - | ||||
| 299 | - | # Gordon automatically creates: | |||
| 300 | - | # "myapp.mydomain.dev" = "myapp.mydomain.dev:latest" | |||
| 301 | - | # The route is added to your config file and deployed instantly! | |||
| 302 | ``` | 182 | ``` | ||
| 303 | 183 | ||||
| 304 | - | #### Perfect for Testing Workflows | 184 | + | ## Advanced Configuration |
| 305 | - | ```bash | |||
| 306 | - | # Test different domains for the same app | |||
| 307 | - | podman build -t myapp . | |||
| 308 | - | ||||
| 309 | - | # Push to different test domains using image names | |||
| 310 | - | podman tag myapp:latest registry.mydomain.com/staging.example.com:latest | |||
| 311 | - | podman push registry.mydomain.com/staging.example.com:latest | |||
| 312 | 185 | ||||
| 313 | - | podman tag myapp:latest registry.mydomain.com/demo.example.com:v1.0.0 | 186 | + | ### Full Config Structure |
| 314 | - | podman push registry.mydomain.com/demo.example.com:v1.0.0 | 187 | + | ```toml |
| 188 | + | # gordon.toml - Auto-generated on first run | |||
| 189 | + | [server] | |||
| 190 | + | port = 8080 | |||
| 191 | + | registry_domain = "registry.mydomain.com" | |||
| 192 | + | runtime = "podman-rootless" # auto, docker, podman, podman-rootless | |||
| 193 | + | # runtime auto-detects: docker → podman-rootless → podman | |||
| 194 | + | # Override with env: CONTAINER_HOST=unix:///custom/socket | |||
| 315 | 195 | ||||
| 316 | - | # Both automatically get their own routes: | 196 | + | [registry_auth] |
| 317 | - | # staging.example.com -> staging.example.com:latest | 197 | + | enabled = true |
| 318 | - | # demo.example.com -> demo.example.com:v1.0.0 | 198 | + | username = "admin" |
| 319 | - | ``` | 199 | + | password = "your-secure-password" |
| 320 | 200 | ||||
| 321 | - | #### Domain Validation | 201 | + | [routes] |
| 322 | - | Gordon only creates auto-routes for valid domain names in image names: | 202 | + | "app.mydomain.com" = "myapp:latest" |
| 323 | - | - ✅ `api.example.com:latest` → Valid subdomain name, route created | |||
| 324 | - | - ✅ `myapp.dev:v1.0.0` → Valid domain name, route created | |||
| 325 | - | - ❌ `myapp:latest` → Not a domain, ignored | |||
| 326 | - | - ❌ `myapp:v1.0.0` → Not a domain, ignored | |||
| 327 | - | - ❌ `localhost:latest` → Not a valid domain, ignored | |||
| 328 | 203 | ||||
| 329 | - | #### Route Precedence | 204 | + | [auto_route] |
| 330 | - | - Existing routes in `[routes]` are never overwritten | 205 | + | enabled = false # Auto-create routes from image names |
| 331 | - | - Auto-routes are saved to your config file permanently | |||
| 332 | - | - You can manually edit or remove auto-created routes anytime | |||
| 333 | 206 | ||||
| 207 | + | [env] | |||
| 208 | + | dir = "./data/env" # Gordon auto-creates env files here | |||
| 209 | + | providers = ["pass", "sops"] # Optional secret management | |||
| 210 | + | # Example env file: ./data/env/app_mydomain_com.env | |||
| 211 | + | # NODE_ENV=production | |||
| 212 | + | # DATABASE_URL=postgresql://localhost:5432/prod | |||
| 213 | + | # API_KEY=${pass:company/api-key} # Unix pass integration | |||
| 214 | + | # JWT_SECRET=${sops:secrets.yaml:jwt} # SOPS integration | |||
| 215 | + | ||||
| 216 | + | [logging] | |||
| 217 | + | enabled = false | |||
| 218 | + | level = "info" # trace, debug, info, warn, error | |||
| 219 | + | dir = "./logs" # Log directory | |||
| 220 | + | main_log_file = "gordon.log" # Main app log | |||
| 221 | + | proxy_log_file = "proxy.log" # HTTP traffic log | |||
| 222 | + | container_log_dir = "containers" # Container logs subdirectory | |||
| 223 | + | max_size = 100 # MB before rotation | |||
| 224 | + | max_backups = 3 # Number of old files to keep | |||
| 225 | + | max_age = 28 # Days to keep old files | |||
| 226 | + | compress = true # Gzip rotated logs | |||
| 227 | + | # Creates: gordon.log, proxy.log, containers/*.log | |||
| 228 | + | ``` | |||
| 334 | 229 | ||||
| 335 | ## FAQ | 230 | ## FAQ | ||
| 336 | 231 | ||||
| 337 | **Q: How is this different from Traefik/Nginx Proxy Manager?** | 232 | **Q: How is this different from Traefik/Nginx Proxy Manager?** | ||
| 338 | - | A: Those are just reverse proxies - you still need to manually start/stop containers. Gordon is a complete deployment platform: built-in registry, automatic deployment on push, and container lifecycle management. It's the difference between a router and a full PaaS. | 233 | + | A: Those are just reverse proxies. Gordon is a complete deployment platform with built-in registry, automatic deployment, and container lifecycle management. |
| 339 | 234 | ||||
| 340 | **Q: Where do builds happen?** | 235 | **Q: Where do builds happen?** | ||
| 341 | - | A: On YOUR machine. If it runs locally, it'll run in production. No mysterious build server issues. | 236 | + | A: On YOUR machine. If it runs locally, it runs in production. |
| 342 | - | ||||
| 343 | - | **Q: How do I handle broken deployments?** | |||
| 344 | - | A: Just edit gordon.toml to point to a previous version. Rollback takes seconds, not minutes. | |||
| 345 | 237 | ||||
| 346 | - | **Q: Do I need CI/CD?** | 238 | + | **Q: How do I rollback?** |
| 347 | - | A: Nope! Your computer is the CI/CD. Build, test locally, push when ready. Keep it stupid simple. | 239 | + | A: Edit gordon.toml to point to a previous version. Takes seconds. |
| 348 | 240 | ||||
| 349 | **Q: Do I need Cloudflare?** | 241 | **Q: Do I need Cloudflare?** | ||
| 350 | - | A: Yes for SSL certificates. Gordon doesn't handle Let's Encrypt yet, so Cloudflare provides the SSL termination and DDoS protection. | 242 | + | A: Yes, for SSL certificates and DDoS protection. |
| 351 | - | ||||
| 352 | - | **Q: Can I run databases?** | |||
| 353 | - | A: You can in theory, but exposing databases on the internet is risky. Database/app isolation is coming soon for safer database deployments. | |||
| 354 | 243 | ||||
| 355 | **Q: Resource requirements?** | 244 | **Q: Resource requirements?** | ||
| 356 | - | A: Runs comfortably on 1GB RAM VPS. Gordon itself uses <15MB of RAM. | 245 | + | A: Runs on 1GB RAM VPS. Gordon uses <15MB RAM. |
| 357 | - | ||||
| 358 | - | **Q: What about secrets?** | |||
| 359 | - | A: Gordon provides built-in environment variable support with secret management via `pass` and `sops`. See the Environment Variables section below. | |||
| 360 | - | ||||
| 361 | - | **Q: How does auto-route creation work?** | |||
| 362 | - | A: When enabled, Gordon watches for pushed images with domain names as image names (like `staging.example.com:latest`) and automatically creates routes. Perfect for testing deployments without manual config edits. | |||
| 363 | 246 | ||||
| 364 | - | ## Philosophy: Build Local, Deploy Simple | 247 | + | ## Philosophy |
| 365 | - | ||||
| 366 | - | ### Why No Build Servers? | |||
| 367 | - | ||||
| 368 | - | 1. **Your machine already works** - Why replicate your dev environment in CI/CD? | |||
| 369 | - | 2. **Instant feedback** - Build errors show up immediately, not after pushing | |||
| 370 | - | 3. **Perfect reproducibility** - "Works on my machine" becomes a feature, not a bug | |||
| 371 | - | 4. **Zero build queues** - Deploy as fast as your internet can push | |||
| 372 | - | ||||
| 373 | - | ### The Gordon Way | |||
| 374 | 248 | ||||
| 375 | ```bash | 249 | ```bash | ||
| 376 | # Traditional CI/CD | 250 | # Traditional CI/CD | ||
| 377 | push code → wait for build → hope it works → debug remotely → repeat | 251 | push code → wait for build → hope it works → debug remotely → repeat | ||
| 378 | 252 | ||||
| 379 | - | # The Gordon Way (with Podman) | 253 | + | # The Gordon Way |
| 380 | - | podman build locally → test locally → push image → instant deploy | 254 | + | build locally → test locally → push image → instant deploy |
| 381 | - | ``` | |||
| 382 | - | ||||
| 383 | - | ## Architecture | |||
| 384 | - | ||||
| 385 | - | - **Event-Driven**: Push events trigger deployments automatically | |||
| 386 | - | - **Config Hot-Reload**: Edit gordon.toml, changes apply instantly | |||
| 387 | - | - **Single Binary**: No dependencies except Docker | |||
| 388 | - | - **Stateless**: Configuration is the only state | |||
| 389 | - | - **Fast**: Written in Go for minimal overhead | |||
| 390 | - | ||||
| 391 | - | ## Advanced Usage | |||
| 392 | - | ||||
| 393 | - | ### Container Runtime Configuration | |||
| 394 | - | ```toml | |||
| 395 | - | [server] | |||
| 396 | - | runtime = "auto" # auto, docker, podman, podman-rootless | |||
| 397 | - | socket_path = "" # optional custom socket path | |||
| 398 | - | ||||
| 399 | - | # Examples: | |||
| 400 | - | # runtime = "docker" | |||
| 401 | - | # runtime = "podman" | |||
| 402 | - | # socket_path = "unix:///run/user/1000/podman/podman.sock" | |||
| 403 | - | ``` | |||
| 404 | - | ||||
| 405 | - | ### Environment Override | |||
| 406 | - | ```bash | |||
| 407 | - | # Override container socket with environment variable | |||
| 408 | - | export CONTAINER_HOST=unix:///custom/path/container.sock | |||
| 409 | - | export CONTAINER_HOST=tcp://remote-docker:2376 | |||
| 410 | - | ||||
| 411 | - | # Works for both Docker and Podman | |||
| 412 | - | gordon start | |||
| 413 | - | ``` | |||
| 414 | - | ||||
| 415 | - | ### Runtime Auto-Detection | |||
| 416 | - | Gordon automatically detects available container runtimes in this order: | |||
| 417 | - | 1. **Docker** (`/var/run/docker.sock`) | |||
| 418 | - | 2. **Podman rootless** (`$XDG_RUNTIME_DIR/podman/podman.sock`) | |||
| 419 | - | 3. **Podman root** (`/run/podman/podman.sock`) | |||
| 420 | - | ||||
| 421 | - | ### Environment Variables & Secret Management | |||
| 422 | - | ||||
| 423 | - | Gordon supports per-route environment variables with secure secret management. | |||
| 424 | - | ||||
| 425 | - | #### Configuration | |||
| 426 | - | ```toml | |||
| 427 | - | [env] | |||
| 428 | - | # Directory where .env files are stored for each route | |||
| 429 | - | dir = "./data/env" # Default location | |||
| 430 | - | # Secret providers for secure credential management | |||
| 431 | - | providers = ["pass", "sops"] # Unix password manager and SOPS | |||
| 432 | - | ``` | |||
| 433 | - | ||||
| 434 | - | #### Creating Environment Files | |||
| 435 | - | Gordon automatically creates empty `.env` files for all configured routes when it starts up. The files are named after the domain with special characters replaced by underscores: | |||
| 436 | - | ||||
| 437 | - | ```bash | |||
| 438 | - | # Gordon automatically creates these files for you: | |||
| 439 | - | # For route "app.mydomain.com" | |||
| 440 | - | ./data/env/app_mydomain_com.env | |||
| 441 | - | ||||
| 442 | - | # For route "api-v2.mydomain.com" | |||
| 443 | - | ./data/env/api-v2_mydomain_com.env | |||
| 444 | - | ``` | |||
| 445 | - | ||||
| 446 | - | #### Environment File Format | |||
| 447 | - | When Gordon creates env files automatically, they include helpful comments and examples. | |||
| 448 | - | ||||
| 449 | - | **Security:** Gordon creates the env directory with `0700` permissions and env files with `0600` permissions (owner read/write only) to protect sensitive information. | |||
| 450 | - | ||||
| 451 | - | ```bash | |||
| 452 | - | # ./data/env/app_mydomain_com.env (automatically created by Gordon) | |||
| 453 | - | # Environment variables for route: app.mydomain.com | |||
| 454 | - | # Image: myapp:latest | |||
| 455 | - | ||||
| 456 | - | NODE_ENV=production | |||
| 457 | - | PORT=3000 | |||
| 458 | - | DATABASE_URL=postgresql://localhost:5432/prod | |||
| 459 | - | ||||
| 460 | - | # Secret references (resolved at container start) | |||
| 461 | - | API_KEY=${pass:company/api-key} | |||
| 462 | - | JWT_SECRET=${sops:secrets.yaml:app.jwt_secret} | |||
| 463 | - | ``` | |||
| 464 | - | ||||
| 465 | - | #### Secret Providers | |||
| 466 | - | ||||
| 467 | - | **Unix Password Manager (pass)** | |||
| 468 | - | ```bash | |||
| 469 | - | # Store secrets with pass | |||
| 470 | - | pass insert company/api-key | |||
| 471 | - | pass insert company/database-password | |||
| 472 | - | ||||
| 473 | - | # Reference in .env files | |||
| 474 | - | API_KEY=${pass:company/api-key} | |||
| 475 | - | DB_PASSWORD=${pass:company/database-password} | |||
| 476 | - | ``` | |||
| 477 | - | ||||
| 478 | - | **SOPS (Secrets OPerationS)** | |||
| 479 | - | ```bash | |||
| 480 | - | # Create encrypted YAML file | |||
| 481 | - | sops secrets.yaml | |||
| 482 | - | ||||
| 483 | - | # secrets.yaml content: | |||
| 484 | - | # app: | |||
| 485 | - | # jwt_secret: "super-secret-key" | |||
| 486 | - | # database_url: "postgresql://user:pass@db:5432/prod" | |||
| 487 | - | ||||
| 488 | - | # Reference in .env files | |||
| 489 | - | JWT_SECRET=${sops:secrets.yaml:app.jwt_secret} | |||
| 490 | - | DATABASE_URL=${sops:secrets.yaml:app.database_url} | |||
| 491 | - | ``` | |||
| 492 | - | ||||
| 493 | - | #### How It Works | |||
| 494 | - | 1. Gordon loads the `.env` file for each route during container deployment | |||
| 495 | - | 2. Secret references (`${provider:path}`) are resolved using the configured providers | |||
| 496 | - | 3. Environment variables are passed to the container at startup | |||
| 497 | - | 4. Missing env files are silently ignored (optional env vars) | |||
| 498 | - | 5. Secret resolution failures cause deployment to fail for security | |||
| 499 | - | ||||
| 500 | - | #### Examples | |||
| 501 | - | ||||
| 502 | - | **Development Setup** | |||
| 503 | - | ```toml | |||
| 504 | - | # gordon.toml | |||
| 505 | - | [env] | |||
| 506 | - | dir = "./dev-data/env" | |||
| 507 | - | providers = ["pass"] # Optional for dev secrets | |||
| 508 | - | ||||
| 509 | - | [routes] | |||
| 510 | - | "app.local" = "myapp:latest" | |||
| 511 | ``` | 255 | ``` | ||
| 512 | 256 | ||||
| 513 | - | ```bash | 257 | + | Your machine already works. Why replicate it in CI/CD? |
| 514 | - | # ./dev-data/env/app_local.env | |||
| 515 | - | NODE_ENV=development | |||
| 516 | - | DEBUG=true | |||
| 517 | - | DATABASE_URL=postgresql://localhost:5432/dev | |||
| 518 | - | API_KEY=${pass:dev/api-key} # Optional: use pass for dev secrets | |||
| 519 | - | ``` | |||
| 520 | - | ||||
| 521 | - | **Production Setup** | |||
| 522 | - | ```toml | |||
| 523 | - | # gordon.toml | |||
| 524 | - | [env] | |||
| 525 | - | dir = "/var/lib/gordon/env" | |||
| 526 | - | providers = ["pass", "sops"] | |||
| 527 | - | ||||
| 528 | - | [routes] | |||
| 529 | - | "app.company.com" = "company-app:v2.1.0" | |||
| 530 | - | ``` | |||
| 531 | - | ||||
| 532 | - | ```bash | |||
| 533 | - | # /var/lib/gordon/env/app_company_com.env | |||
| 534 | - | NODE_ENV=production | |||
| 535 | - | DATABASE_URL=postgresql://user:${pass:company/db-password}@db:5432/prod | |||
| 536 | - | API_KEY=${sops:company-secrets.yaml:api.key} | |||
| 537 | - | JWT_SECRET=${sops:company-secrets.yaml:jwt.secret} | |||
| 538 | - | ``` | |||
| 539 | - | ||||
| 540 | - | ### Port Binding | |||
| 541 | - | Gordon requires your Dockerfile to explicitly expose ports using the `EXPOSE` instruction. When multiple ports are exposed, Gordon will use the first exposed port for HTTP traffic. | |||
| 542 | - | ||||
| 543 | - | Example Dockerfile: | |||
| 544 | - | ``` | |||
| 545 | - | ENTRYPOINT ["/myapp"] | |||
| 546 | - | EXPOSE 8080 | |||
| 547 | - | ``` | |||
| 548 | - | ||||
| 549 | - | ### Registry Operations | |||
| 550 | - | ```bash | |||
| 551 | - | # List images | |||
| 552 | - | curl -u admin:password https://registry.mydomain.com/v2/_catalog | |||
| 553 | - | ||||
| 554 | - | # List tags | |||
| 555 | - | curl -u admin:password https://registry.mydomain.com/v2/myapp/tags/list | |||
| 556 | - | ``` | |||
| 557 | - | ||||
| 558 | - | ## Contributing | |||
| 559 | - | ||||
| 560 | - | Gordon is open source and we love contributions! Check out our [issues](https://github.com/yourusername/gordon/issues) or submit a PR. | |||
| 561 | 258 | ||||
| 562 | ## License | 259 | ## License | ||
| 563 | 260 | ||||