feat: Add comprehensive Gordon v2 container platform with documentation and examples
- Remove deprecated generate command and JWT auth dependencies - Add complete configuration examples in TOML, YAML, and HCL formats - Implement robust config file discovery and live reload functionality - Add comprehensive README with quick start guide and real-world examples - Enhance container management with automatic port detection and routing - Add support for registry domain routing via Cloudflare - Implement complete event-driven deployment system - Add production-ready Systemd service configuration - Include firewall setup documentation and example configurations
- commit
- d99e74e09a905532f961bd47334936722552b4a2
- author
- bamen · · 2025-06-06 16:48 UTC
- parent
- 0642b94
- signature
- unsigned
24 files · +1293 -360
A Dockerfile +72 -0
| @@ -0,0 +1,72 @@ | |||
| 1 | + | # Gordon v2 - Production Dockerfile | |
| 2 | + | # Multi-stage build for optimized container image | |
| 3 | + | ||
| 4 | + | # Build stage | |
| 5 | + | FROM golang:1.24-alpine AS builder | |
| 6 | + | ||
| 7 | + | # Install build dependencies | |
| 8 | + | RUN apk add --no-cache git ca-certificates tzdata | |
| 9 | + | ||
| 10 | + | # Set working directory | |
| 11 | + | WORKDIR /app | |
| 12 | + | ||
| 13 | + | # Copy go mod files | |
| 14 | + | COPY go.mod go.sum ./ | |
| 15 | + | ||
| 16 | + | # Download dependencies | |
| 17 | + | RUN go mod download | |
| 18 | + | ||
| 19 | + | # Copy source code | |
| 20 | + | COPY . . | |
| 21 | + | ||
| 22 | + | # Build the binary | |
| 23 | + | RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \ | |
| 24 | + | -ldflags='-w -s -extldflags "-static"' \ | |
| 25 | + | -a -installsuffix cgo \ | |
| 26 | + | -o gordon . | |
| 27 | + | ||
| 28 | + | # Runtime stage | |
| 29 | + | FROM alpine:latest | |
| 30 | + | ||
| 31 | + | # Install runtime dependencies | |
| 32 | + | RUN apk add --no-cache \ | |
| 33 | + | ca-certificates \ | |
| 34 | + | docker-cli \ | |
| 35 | + | curl \ | |
| 36 | + | wget \ | |
| 37 | + | tzdata \ | |
| 38 | + | && rm -rf /var/cache/apk/* | |
| 39 | + | ||
| 40 | + | # Create non-root user | |
| 41 | + | RUN adduser -D -s /bin/sh gordon | |
| 42 | + | ||
| 43 | + | # Set working directory | |
| 44 | + | WORKDIR /app | |
| 45 | + | ||
| 46 | + | # Copy binary from builder | |
| 47 | + | COPY --from=builder /app/gordon . | |
| 48 | + | ||
| 49 | + | # Create data directory | |
| 50 | + | RUN mkdir -p /data && chown gordon:gordon /data | |
| 51 | + | ||
| 52 | + | # Copy default configuration (optional) | |
| 53 | + | COPY --chown=gordon:gordon gordon.toml.example /app/gordon.toml.example | |
| 54 | + | ||
| 55 | + | # Switch to non-root user | |
| 56 | + | USER gordon | |
| 57 | + | ||
| 58 | + | # Expose ports | |
| 59 | + | EXPOSE 8080 5000 | |
| 60 | + | ||
| 61 | + | # Health check | |
| 62 | + | HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \ | |
| 63 | + | CMD wget --quiet --tries=1 --spider http://localhost:8080/health || exit 1 | |
| 64 | + | ||
| 65 | + | # Default command | |
| 66 | + | CMD ["./gordon", "start"] | |
| 67 | + | ||
| 68 | + | # Metadata | |
| 69 | + | LABEL maintainer="bnemam" | |
| 70 | + | LABEL version="2.0" | |
| 71 | + | LABEL description="Event-driven container deployment platform" | |
| 72 | + | LABEL org.opencontainers.image.source="https://github.com/bnema/gordon"⏎̸ | |
| @@ -0,0 +1,72 @@ | |||||
| 1 | + | # Gordon v2 - Production Dockerfile | |||
| 2 | + | # Multi-stage build for optimized container image | |||
| 3 | + | ||||
| 4 | + | # Build stage | |||
| 5 | + | FROM golang:1.24-alpine AS builder | |||
| 6 | + | ||||
| 7 | + | # Install build dependencies | |||
| 8 | + | RUN apk add --no-cache git ca-certificates tzdata | |||
| 9 | + | ||||
| 10 | + | # Set working directory | |||
| 11 | + | WORKDIR /app | |||
| 12 | + | ||||
| 13 | + | # Copy go mod files | |||
| 14 | + | COPY go.mod go.sum ./ | |||
| 15 | + | ||||
| 16 | + | # Download dependencies | |||
| 17 | + | RUN go mod download | |||
| 18 | + | ||||
| 19 | + | # Copy source code | |||
| 20 | + | COPY . . | |||
| 21 | + | ||||
| 22 | + | # Build the binary | |||
| 23 | + | RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \ | |||
| 24 | + | -ldflags='-w -s -extldflags "-static"' \ | |||
| 25 | + | -a -installsuffix cgo \ | |||
| 26 | + | -o gordon . | |||
| 27 | + | ||||
| 28 | + | # Runtime stage | |||
| 29 | + | FROM alpine:latest | |||
| 30 | + | ||||
| 31 | + | # Install runtime dependencies | |||
| 32 | + | RUN apk add --no-cache \ | |||
| 33 | + | ca-certificates \ | |||
| 34 | + | docker-cli \ | |||
| 35 | + | curl \ | |||
| 36 | + | wget \ | |||
| 37 | + | tzdata \ | |||
| 38 | + | && rm -rf /var/cache/apk/* | |||
| 39 | + | ||||
| 40 | + | # Create non-root user | |||
| 41 | + | RUN adduser -D -s /bin/sh gordon | |||
| 42 | + | ||||
| 43 | + | # Set working directory | |||
| 44 | + | WORKDIR /app | |||
| 45 | + | ||||
| 46 | + | # Copy binary from builder | |||
| 47 | + | COPY --from=builder /app/gordon . | |||
| 48 | + | ||||
| 49 | + | # Create data directory | |||
| 50 | + | RUN mkdir -p /data && chown gordon:gordon /data | |||
| 51 | + | ||||
| 52 | + | # Copy default configuration (optional) | |||
| 53 | + | COPY --chown=gordon:gordon gordon.toml.example /app/gordon.toml.example | |||
| 54 | + | ||||
| 55 | + | # Switch to non-root user | |||
| 56 | + | USER gordon | |||
| 57 | + | ||||
| 58 | + | # Expose ports | |||
| 59 | + | EXPOSE 8080 5000 | |||
| 60 | + | ||||
| 61 | + | # Health check | |||
| 62 | + | HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \ | |||
| 63 | + | CMD wget --quiet --tries=1 --spider http://localhost:8080/health || exit 1 | |||
| 64 | + | ||||
| 65 | + | # Default command | |||
| 66 | + | CMD ["./gordon", "start"] | |||
| 67 | + | ||||
| 68 | + | # Metadata | |||
| 69 | + | LABEL maintainer="bnemam" | |||
| 70 | + | LABEL version="2.0" | |||
| 71 | + | LABEL description="Event-driven container deployment platform" | |||
| 72 | + | LABEL org.opencontainers.image.source="https://github.com/bnema/gordon"⏎̸ | |||
A README.md +335 -0
| @@ -0,0 +1,335 @@ | |||
| 1 | + | # Gordon | |
| 2 | + | ||
| 3 | + | <div align="center"> | |
| 4 | + | <img src="assets/gordon-mascot.png" alt="Gordon Mascot" width="200"> | |
| 5 | + | <h3>The Smart Way to Deploy Containers on Your VPS</h3> | |
| 6 | + | <p><em>Push code → Auto-deploy → Zero complexity</em></p> | |
| 7 | + | </div> | |
| 8 | + | ||
| 9 | + | ## Why Gordon? | |
| 10 | + | ||
| 11 | + | **You have a $5 VPS. You want to run multiple apps. You don't want Kubernetes.** | |
| 12 | + | ||
| 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 | + | ||
| 15 | + | ### 🎯 Perfect For | |
| 16 | + | ||
| 17 | + | - **Solo developers** with multiple side projects on one VPS | |
| 18 | + | - **Small teams** who want Heroku-like simplicity without the cost | |
| 19 | + | - **Agencies** deploying client apps across VPS servers | |
| 20 | + | - **Anyone** tired of complex deployment pipelines | |
| 21 | + | ||
| 22 | + | ## 🚀 How It Works | |
| 23 | + | ||
| 24 | + | ### Build Locally, Deploy Instantly | |
| 25 | + | ||
| 26 | + | ```bash | |
| 27 | + | # 1. Build & test on YOUR machine | |
| 28 | + | docker build -t myapp . | |
| 29 | + | docker run -p 8080:8080 myapp # Works? Great! | |
| 30 | + | ||
| 31 | + | # 2. Push to deploy | |
| 32 | + | docker tag myapp registry.yourdomain.com/myapp:latest | |
| 33 | + | docker push registry.yourdomain.com/myapp:latest | |
| 34 | + | ||
| 35 | + | # 3. That's it. Gordon deploys it automatically. | |
| 36 | + | ``` | |
| 37 | + | ||
| 38 | + | **No build servers. No CI/CD complexity. If it runs on your machine, it runs in production.** | |
| 39 | + | ||
| 40 | + | ### 🔄 Instant Rollbacks | |
| 41 | + | ||
| 42 | + | Something broke? Just change your config: | |
| 43 | + | ||
| 44 | + | ```toml | |
| 45 | + | # Before (in gordon.toml) | |
| 46 | + | "app.yourdomain.com" = "myapp:latest" | |
| 47 | + | ||
| 48 | + | # After - instant rollback! | |
| 49 | + | "app.yourdomain.com" = "myapp:v1.2.3" | |
| 50 | + | ``` | |
| 51 | + | ||
| 52 | + | Save the file. Gordon redeploys the previous version. Problem solved in seconds. | |
| 53 | + | ||
| 54 | + | ## ✨ Key Features | |
| 55 | + | ||
| 56 | + | ### 🏠 Local-First Development | |
| 57 | + | **Your machine is the build server.** Test locally, push when ready. No waiting for remote builds. | |
| 58 | + | ||
| 59 | + | ### 🏦 Built-in Docker Registry | |
| 60 | + | Your VPS becomes a private Docker registry. 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 | + | ## 💰 Cost Comparison | |
| 78 | + | ||
| 79 | + | | Solution | Monthly Cost | Setup Complexity | # Apps | | |
| 80 | + | |----------|-------------|------------------|--------| | |
| 81 | + | | Heroku | $7-25/app | ⭐ | Limited | | |
| 82 | + | | AWS ECS | $50-200+ | ⭐⭐⭐⭐⭐ | Unlimited | | |
| 83 | + | | Kubernetes | $100+ | ⭐⭐⭐⭐⭐ | Unlimited | | |
| 84 | + | | **Gordon + VPS** | **$5-20 total** | **⭐⭐** | **Unlimited** | | |
| 85 | + | ||
| 86 | + | ## 🚀 Quick Start (5 minutes) | |
| 87 | + | ||
| 88 | + | ### 1. Get a VPS & Install Docker | |
| 89 | + | ```bash | |
| 90 | + | # Any VPS provider: DigitalOcean, Linode, Vultr, Hetzner | |
| 91 | + | # Ubuntu/Debian recommended | |
| 92 | + | curl -fsSL https://get.docker.com | sh | |
| 93 | + | ``` | |
| 94 | + | ||
| 95 | + | ### 2. Install Gordon | |
| 96 | + | ```bash | |
| 97 | + | wget https://github.com/yourusername/gordon/releases/latest/download/gordon-linux-amd64 | |
| 98 | + | chmod +x gordon-linux-amd64 | |
| 99 | + | sudo mv gordon-linux-amd64 /usr/local/bin/gordon | |
| 100 | + | ``` | |
| 101 | + | ||
| 102 | + | ### 3. Create Config | |
| 103 | + | ```toml | |
| 104 | + | # gordon.toml | |
| 105 | + | [server] | |
| 106 | + | port = 8080 | |
| 107 | + | registry_domain = "registry.yourdomain.com" | |
| 108 | + | ||
| 109 | + | [registry_auth] | |
| 110 | + | enabled = true | |
| 111 | + | username = "admin" | |
| 112 | + | password = "your-secure-password" | |
| 113 | + | ||
| 114 | + | [routes] | |
| 115 | + | "app.yourdomain.com" = "myapp:latest" | |
| 116 | + | "api.yourdomain.com" = "api:v1" | |
| 117 | + | "blog.yourdomain.com" = "wordpress:latest" | |
| 118 | + | ``` | |
| 119 | + | ||
| 120 | + | ### 4. Point Cloudflare DNS | |
| 121 | + | ``` | |
| 122 | + | A *.yourdomain.com → YOUR_VPS_IP | |
| 123 | + | A yourdomain.com → YOUR_VPS_IP | |
| 124 | + | ``` | |
| 125 | + | ||
| 126 | + | ### 5. Start Gordon & Configure Firewall | |
| 127 | + | ```bash | |
| 128 | + | # Start Gordon | |
| 129 | + | sudo gordon start | |
| 130 | + | ||
| 131 | + | # Set up port forwarding (choose one option) | |
| 132 | + | ||
| 133 | + | # Option A: iptables (most common) | |
| 134 | + | sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 8080 | |
| 135 | + | sudo iptables -t nat -A PREROUTING -p tcp --dport 443 -j REDIRECT --to-port 8080 | |
| 136 | + | ||
| 137 | + | # Option B: UFW with NAT (Ubuntu) | |
| 138 | + | sudo ufw allow 22,80,443,8080/tcp | |
| 139 | + | echo 'net.ipv4.ip_forward=1' | sudo tee -a /etc/sysctl.conf | |
| 140 | + | sudo sysctl -p | |
| 141 | + | ||
| 142 | + | # Deploy your first app | |
| 143 | + | docker tag myapp:latest registry.yourdomain.com/myapp:latest | |
| 144 | + | docker push registry.yourdomain.com/myapp:latest | |
| 145 | + | # Visit https://app.yourdomain.com 🎉 | |
| 146 | + | ``` | |
| 147 | + | ||
| 148 | + | ## 🎯 Real-World Examples | |
| 149 | + | ||
| 150 | + | ### Deploy a Node.js App | |
| 151 | + | ```bash | |
| 152 | + | # Build and test locally first | |
| 153 | + | docker build -t myapp . | |
| 154 | + | docker run -p 3000:3000 myapp # Test it! | |
| 155 | + | ||
| 156 | + | # Tag with version AND latest | |
| 157 | + | docker tag myapp registry.yourdomain.com/myapp:v1.0.0 | |
| 158 | + | docker tag myapp registry.yourdomain.com/myapp:latest | |
| 159 | + | ||
| 160 | + | # Push both tags | |
| 161 | + | docker push registry.yourdomain.com/myapp:v1.0.0 | |
| 162 | + | docker push registry.yourdomain.com/myapp:latest | |
| 163 | + | ||
| 164 | + | # Auto-deployed to app.yourdomain.com | |
| 165 | + | ``` | |
| 166 | + | ||
| 167 | + | ### Smart Versioning Strategy | |
| 168 | + | ```bash | |
| 169 | + | # Always push versioned tags | |
| 170 | + | docker tag myapp registry.yourdomain.com/myapp:v1.0.1 | |
| 171 | + | docker push registry.yourdomain.com/myapp:v1.0.1 | |
| 172 | + | ||
| 173 | + | # Update config to test | |
| 174 | + | "app.yourdomain.com" = "myapp:v1.0.1" | |
| 175 | + | ||
| 176 | + | # Happy? Update latest | |
| 177 | + | docker tag myapp registry.yourdomain.com/myapp:latest | |
| 178 | + | docker push registry.yourdomain.com/myapp:latest | |
| 179 | + | ``` | |
| 180 | + | ||
| 181 | + | ### Instant Rollback When Things Break | |
| 182 | + | ```toml | |
| 183 | + | # Production broke after deploying v1.0.2? | |
| 184 | + | # Just edit gordon.toml: | |
| 185 | + | ||
| 186 | + | # From: | |
| 187 | + | "app.yourdomain.com" = "myapp:v1.0.2" | |
| 188 | + | ||
| 189 | + | # To: | |
| 190 | + | "app.yourdomain.com" = "myapp:v1.0.1" | |
| 191 | + | ||
| 192 | + | # Save. Fixed in seconds. No scripts, no drama. | |
| 193 | + | ``` | |
| 194 | + | ||
| 195 | + | ### Multiple Environments | |
| 196 | + | ```toml | |
| 197 | + | [routes] | |
| 198 | + | "app.yourdomain.com" = "myapp:v1.0.0" # Stable production | |
| 199 | + | "staging.yourdomain.com" = "myapp:latest" # Latest builds | |
| 200 | + | "feature-xyz.yourdomain.com" = "myapp:feature-xyz" # Feature branch | |
| 201 | + | ``` | |
| 202 | + | ||
| 203 | + | ## 🛡️ Production-Ready | |
| 204 | + | ||
| 205 | + | ### Systemd Service | |
| 206 | + | ```bash | |
| 207 | + | sudo tee /etc/systemd/system/gordon.service > /dev/null <<EOF | |
| 208 | + | [Unit] | |
| 209 | + | Description=Gordon Container Platform | |
| 210 | + | After=docker.service | |
| 211 | + | ||
| 212 | + | [Service] | |
| 213 | + | Type=simple | |
| 214 | + | Restart=always | |
| 215 | + | RestartSec=5 | |
| 216 | + | ExecStart=/usr/local/bin/gordon start | |
| 217 | + | ||
| 218 | + | [Install] | |
| 219 | + | WantedBy=multi-user.target | |
| 220 | + | EOF | |
| 221 | + | ||
| 222 | + | sudo systemctl enable --now gordon | |
| 223 | + | ``` | |
| 224 | + | ||
| 225 | + | ### With Docker Compose | |
| 226 | + | ```yaml | |
| 227 | + | version: '3.8' | |
| 228 | + | services: | |
| 229 | + | gordon: | |
| 230 | + | image: gordon:latest | |
| 231 | + | ports: | |
| 232 | + | - "80:8080" | |
| 233 | + | - "443:8080" | |
| 234 | + | volumes: | |
| 235 | + | - /var/run/docker.sock:/var/run/docker.sock | |
| 236 | + | - ./gordon.toml:/app/gordon.toml:ro | |
| 237 | + | - ./data:/data | |
| 238 | + | restart: unless-stopped | |
| 239 | + | ``` | |
| 240 | + | ||
| 241 | + | ## 🤔 FAQ | |
| 242 | + | ||
| 243 | + | **Q: How is this different from Traefik/Nginx Proxy Manager?** | |
| 244 | + | A: Gordon includes a Docker registry and automatic deployment. Push an image, get a deployment. No manual container management. | |
| 245 | + | ||
| 246 | + | **Q: Where do builds happen?** | |
| 247 | + | A: On YOUR machine. If it runs locally, it'll run in production. No mysterious build server issues. | |
| 248 | + | ||
| 249 | + | **Q: How do I handle broken deployments?** | |
| 250 | + | A: Just edit gordon.toml to point to a previous version. Rollback takes seconds, not minutes. | |
| 251 | + | ||
| 252 | + | **Q: Do I need CI/CD?** | |
| 253 | + | A: Nope! Your laptop is the CI/CD. Build, test locally, push when ready. Keep it simple. | |
| 254 | + | ||
| 255 | + | **Q: Do I need Cloudflare?** | |
| 256 | + | A: Recommended for free SSL and DDoS protection, but Gordon works with any DNS provider. | |
| 257 | + | ||
| 258 | + | **Q: Can I run databases?** | |
| 259 | + | A: Yes! Gordon manages any container. Use volumes for persistent data. | |
| 260 | + | ||
| 261 | + | **Q: Resource requirements?** | |
| 262 | + | A: Runs comfortably on 1GB RAM VPS. Gordon itself uses <50MB. | |
| 263 | + | ||
| 264 | + | **Q: What about secrets?** | |
| 265 | + | A: Use environment variables in your container or Docker secrets. Gordon doesn't interfere. | |
| 266 | + | ||
| 267 | + | ## 💡 Philosophy: Build Local, Deploy Simple | |
| 268 | + | ||
| 269 | + | ### Why No Build Servers? | |
| 270 | + | ||
| 271 | + | 1. **Your machine already works** - Why replicate your dev environment in CI/CD? | |
| 272 | + | 2. **Instant feedback** - Build errors show up immediately, not after pushing | |
| 273 | + | 3. **Perfect reproducibility** - "Works on my machine" becomes a feature, not a bug | |
| 274 | + | 4. **Zero build queues** - Deploy as fast as your internet can push | |
| 275 | + | ||
| 276 | + | ### The Gordon Way | |
| 277 | + | ||
| 278 | + | ```bash | |
| 279 | + | # Traditional CI/CD | |
| 280 | + | push code → wait for build → hope it works → debug remotely → repeat | |
| 281 | + | ||
| 282 | + | # The Gordon Way | |
| 283 | + | build locally → test locally → push image → instant deploy | |
| 284 | + | ``` | |
| 285 | + | ||
| 286 | + | ## 🏗️ Architecture | |
| 287 | + | ||
| 288 | + | ``` | |
| 289 | + | Your Machine → Docker Image → Gordon Registry → Auto-Deploy → Live App | |
| 290 | + | ↓ | |
| 291 | + | Event System | |
| 292 | + | ↓ | |
| 293 | + | Domain Router → Container | |
| 294 | + | ``` | |
| 295 | + | ||
| 296 | + | - **Event-Driven**: Push events trigger deployments automatically | |
| 297 | + | - **Config Hot-Reload**: Edit gordon.toml, changes apply instantly | |
| 298 | + | - **Single Binary**: No dependencies except Docker | |
| 299 | + | - **Stateless**: Configuration is the only state | |
| 300 | + | - **Fast**: Written in Go for minimal overhead | |
| 301 | + | ||
| 302 | + | ## 🚀 Advanced Usage | |
| 303 | + | ||
| 304 | + | ### Custom Ports | |
| 305 | + | ```toml | |
| 306 | + | # Gordon auto-detects ports: 80, 8080, 3000 | |
| 307 | + | # Or use EXPOSE in Dockerfile | |
| 308 | + | ``` | |
| 309 | + | ||
| 310 | + | ### Registry Operations | |
| 311 | + | ```bash | |
| 312 | + | # List images | |
| 313 | + | curl -u admin:password https://registry.yourdomain.com/v2/_catalog | |
| 314 | + | ||
| 315 | + | # List tags | |
| 316 | + | curl -u admin:password https://registry.yourdomain.com/v2/myapp/tags/list | |
| 317 | + | ``` | |
| 318 | + | ||
| 319 | + | ### Multi-Server Setup | |
| 320 | + | Deploy Gordon on multiple VPS servers with different configs for geographic distribution or separation of concerns. | |
| 321 | + | ||
| 322 | + | ## 🤝 Contributing | |
| 323 | + | ||
| 324 | + | Gordon is open source and we love contributions! Check out our [issues](https://github.com/yourusername/gordon/issues) or submit a PR. | |
| 325 | + | ||
| 326 | + | ## 📄 License | |
| 327 | + | ||
| 328 | + | GPL-3.0 - Use freely, contribute back. | |
| 329 | + | ||
| 330 | + | --- | |
| 331 | + | ||
| 332 | + | <div align="center"> | |
| 333 | + | <p><strong>Stop overcomplicating container deployment.</strong></p> | |
| 334 | + | <p>Star ⭐ this repo if Gordon saves you money!</p> | |
| 335 | + | </div>⏎̸ | |
| @@ -0,0 +1,335 @@ | |||||
| 1 | + | # Gordon | |||
| 2 | + | ||||
| 3 | + | <div align="center"> | |||
| 4 | + | <img src="assets/gordon-mascot.png" alt="Gordon Mascot" width="200"> | |||
| 5 | + | <h3>The Smart Way to Deploy Containers on Your VPS</h3> | |||
| 6 | + | <p><em>Push code → Auto-deploy → Zero complexity</em></p> | |||
| 7 | + | </div> | |||
| 8 | + | ||||
| 9 | + | ## Why Gordon? | |||
| 10 | + | ||||
| 11 | + | **You have a $5 VPS. You want to run multiple apps. You don't want Kubernetes.** | |||
| 12 | + | ||||
| 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 | + | ||||
| 15 | + | ### 🎯 Perfect For | |||
| 16 | + | ||||
| 17 | + | - **Solo developers** with multiple side projects on one VPS | |||
| 18 | + | - **Small teams** who want Heroku-like simplicity without the cost | |||
| 19 | + | - **Agencies** deploying client apps across VPS servers | |||
| 20 | + | - **Anyone** tired of complex deployment pipelines | |||
| 21 | + | ||||
| 22 | + | ## 🚀 How It Works | |||
| 23 | + | ||||
| 24 | + | ### Build Locally, Deploy Instantly | |||
| 25 | + | ||||
| 26 | + | ```bash | |||
| 27 | + | # 1. Build & test on YOUR machine | |||
| 28 | + | docker build -t myapp . | |||
| 29 | + | docker run -p 8080:8080 myapp # Works? Great! | |||
| 30 | + | ||||
| 31 | + | # 2. Push to deploy | |||
| 32 | + | docker tag myapp registry.yourdomain.com/myapp:latest | |||
| 33 | + | docker push registry.yourdomain.com/myapp:latest | |||
| 34 | + | ||||
| 35 | + | # 3. That's it. Gordon deploys it automatically. | |||
| 36 | + | ``` | |||
| 37 | + | ||||
| 38 | + | **No build servers. No CI/CD complexity. If it runs on your machine, it runs in production.** | |||
| 39 | + | ||||
| 40 | + | ### 🔄 Instant Rollbacks | |||
| 41 | + | ||||
| 42 | + | Something broke? Just change your config: | |||
| 43 | + | ||||
| 44 | + | ```toml | |||
| 45 | + | # Before (in gordon.toml) | |||
| 46 | + | "app.yourdomain.com" = "myapp:latest" | |||
| 47 | + | ||||
| 48 | + | # After - instant rollback! | |||
| 49 | + | "app.yourdomain.com" = "myapp:v1.2.3" | |||
| 50 | + | ``` | |||
| 51 | + | ||||
| 52 | + | Save the file. Gordon redeploys the previous version. Problem solved in seconds. | |||
| 53 | + | ||||
| 54 | + | ## ✨ Key Features | |||
| 55 | + | ||||
| 56 | + | ### 🏠 Local-First Development | |||
| 57 | + | **Your machine is the build server.** Test locally, push when ready. No waiting for remote builds. | |||
| 58 | + | ||||
| 59 | + | ### 🏦 Built-in Docker Registry | |||
| 60 | + | Your VPS becomes a private Docker registry. 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 | + | ## 💰 Cost Comparison | |||
| 78 | + | ||||
| 79 | + | | Solution | Monthly Cost | Setup Complexity | # Apps | | |||
| 80 | + | |----------|-------------|------------------|--------| | |||
| 81 | + | | Heroku | $7-25/app | ⭐ | Limited | | |||
| 82 | + | | AWS ECS | $50-200+ | ⭐⭐⭐⭐⭐ | Unlimited | | |||
| 83 | + | | Kubernetes | $100+ | ⭐⭐⭐⭐⭐ | Unlimited | | |||
| 84 | + | | **Gordon + VPS** | **$5-20 total** | **⭐⭐** | **Unlimited** | | |||
| 85 | + | ||||
| 86 | + | ## 🚀 Quick Start (5 minutes) | |||
| 87 | + | ||||
| 88 | + | ### 1. Get a VPS & Install Docker | |||
| 89 | + | ```bash | |||
| 90 | + | # Any VPS provider: DigitalOcean, Linode, Vultr, Hetzner | |||
| 91 | + | # Ubuntu/Debian recommended | |||
| 92 | + | curl -fsSL https://get.docker.com | sh | |||
| 93 | + | ``` | |||
| 94 | + | ||||
| 95 | + | ### 2. Install Gordon | |||
| 96 | + | ```bash | |||
| 97 | + | wget https://github.com/yourusername/gordon/releases/latest/download/gordon-linux-amd64 | |||
| 98 | + | chmod +x gordon-linux-amd64 | |||
| 99 | + | sudo mv gordon-linux-amd64 /usr/local/bin/gordon | |||
| 100 | + | ``` | |||
| 101 | + | ||||
| 102 | + | ### 3. Create Config | |||
| 103 | + | ```toml | |||
| 104 | + | # gordon.toml | |||
| 105 | + | [server] | |||
| 106 | + | port = 8080 | |||
| 107 | + | registry_domain = "registry.yourdomain.com" | |||
| 108 | + | ||||
| 109 | + | [registry_auth] | |||
| 110 | + | enabled = true | |||
| 111 | + | username = "admin" | |||
| 112 | + | password = "your-secure-password" | |||
| 113 | + | ||||
| 114 | + | [routes] | |||
| 115 | + | "app.yourdomain.com" = "myapp:latest" | |||
| 116 | + | "api.yourdomain.com" = "api:v1" | |||
| 117 | + | "blog.yourdomain.com" = "wordpress:latest" | |||
| 118 | + | ``` | |||
| 119 | + | ||||
| 120 | + | ### 4. Point Cloudflare DNS | |||
| 121 | + | ``` | |||
| 122 | + | A *.yourdomain.com → YOUR_VPS_IP | |||
| 123 | + | A yourdomain.com → YOUR_VPS_IP | |||
| 124 | + | ``` | |||
| 125 | + | ||||
| 126 | + | ### 5. Start Gordon & Configure Firewall | |||
| 127 | + | ```bash | |||
| 128 | + | # Start Gordon | |||
| 129 | + | sudo gordon start | |||
| 130 | + | ||||
| 131 | + | # Set up port forwarding (choose one option) | |||
| 132 | + | ||||
| 133 | + | # Option A: iptables (most common) | |||
| 134 | + | sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 8080 | |||
| 135 | + | sudo iptables -t nat -A PREROUTING -p tcp --dport 443 -j REDIRECT --to-port 8080 | |||
| 136 | + | ||||
| 137 | + | # Option B: UFW with NAT (Ubuntu) | |||
| 138 | + | sudo ufw allow 22,80,443,8080/tcp | |||
| 139 | + | echo 'net.ipv4.ip_forward=1' | sudo tee -a /etc/sysctl.conf | |||
| 140 | + | sudo sysctl -p | |||
| 141 | + | ||||
| 142 | + | # Deploy your first app | |||
| 143 | + | docker tag myapp:latest registry.yourdomain.com/myapp:latest | |||
| 144 | + | docker push registry.yourdomain.com/myapp:latest | |||
| 145 | + | # Visit https://app.yourdomain.com 🎉 | |||
| 146 | + | ``` | |||
| 147 | + | ||||
| 148 | + | ## 🎯 Real-World Examples | |||
| 149 | + | ||||
| 150 | + | ### Deploy a Node.js App | |||
| 151 | + | ```bash | |||
| 152 | + | # Build and test locally first | |||
| 153 | + | docker build -t myapp . | |||
| 154 | + | docker run -p 3000:3000 myapp # Test it! | |||
| 155 | + | ||||
| 156 | + | # Tag with version AND latest | |||
| 157 | + | docker tag myapp registry.yourdomain.com/myapp:v1.0.0 | |||
| 158 | + | docker tag myapp registry.yourdomain.com/myapp:latest | |||
| 159 | + | ||||
| 160 | + | # Push both tags | |||
| 161 | + | docker push registry.yourdomain.com/myapp:v1.0.0 | |||
| 162 | + | docker push registry.yourdomain.com/myapp:latest | |||
| 163 | + | ||||
| 164 | + | # Auto-deployed to app.yourdomain.com | |||
| 165 | + | ``` | |||
| 166 | + | ||||
| 167 | + | ### Smart Versioning Strategy | |||
| 168 | + | ```bash | |||
| 169 | + | # Always push versioned tags | |||
| 170 | + | docker tag myapp registry.yourdomain.com/myapp:v1.0.1 | |||
| 171 | + | docker push registry.yourdomain.com/myapp:v1.0.1 | |||
| 172 | + | ||||
| 173 | + | # Update config to test | |||
| 174 | + | "app.yourdomain.com" = "myapp:v1.0.1" | |||
| 175 | + | ||||
| 176 | + | # Happy? Update latest | |||
| 177 | + | docker tag myapp registry.yourdomain.com/myapp:latest | |||
| 178 | + | docker push registry.yourdomain.com/myapp:latest | |||
| 179 | + | ``` | |||
| 180 | + | ||||
| 181 | + | ### Instant Rollback When Things Break | |||
| 182 | + | ```toml | |||
| 183 | + | # Production broke after deploying v1.0.2? | |||
| 184 | + | # Just edit gordon.toml: | |||
| 185 | + | ||||
| 186 | + | # From: | |||
| 187 | + | "app.yourdomain.com" = "myapp:v1.0.2" | |||
| 188 | + | ||||
| 189 | + | # To: | |||
| 190 | + | "app.yourdomain.com" = "myapp:v1.0.1" | |||
| 191 | + | ||||
| 192 | + | # Save. Fixed in seconds. No scripts, no drama. | |||
| 193 | + | ``` | |||
| 194 | + | ||||
| 195 | + | ### Multiple Environments | |||
| 196 | + | ```toml | |||
| 197 | + | [routes] | |||
| 198 | + | "app.yourdomain.com" = "myapp:v1.0.0" # Stable production | |||
| 199 | + | "staging.yourdomain.com" = "myapp:latest" # Latest builds | |||
| 200 | + | "feature-xyz.yourdomain.com" = "myapp:feature-xyz" # Feature branch | |||
| 201 | + | ``` | |||
| 202 | + | ||||
| 203 | + | ## 🛡️ Production-Ready | |||
| 204 | + | ||||
| 205 | + | ### Systemd Service | |||
| 206 | + | ```bash | |||
| 207 | + | sudo tee /etc/systemd/system/gordon.service > /dev/null <<EOF | |||
| 208 | + | [Unit] | |||
| 209 | + | Description=Gordon Container Platform | |||
| 210 | + | After=docker.service | |||
| 211 | + | ||||
| 212 | + | [Service] | |||
| 213 | + | Type=simple | |||
| 214 | + | Restart=always | |||
| 215 | + | RestartSec=5 | |||
| 216 | + | ExecStart=/usr/local/bin/gordon start | |||
| 217 | + | ||||
| 218 | + | [Install] | |||
| 219 | + | WantedBy=multi-user.target | |||
| 220 | + | EOF | |||
| 221 | + | ||||
| 222 | + | sudo systemctl enable --now gordon | |||
| 223 | + | ``` | |||
| 224 | + | ||||
| 225 | + | ### With Docker Compose | |||
| 226 | + | ```yaml | |||
| 227 | + | version: '3.8' | |||
| 228 | + | services: | |||
| 229 | + | gordon: | |||
| 230 | + | image: gordon:latest | |||
| 231 | + | ports: | |||
| 232 | + | - "80:8080" | |||
| 233 | + | - "443:8080" | |||
| 234 | + | volumes: | |||
| 235 | + | - /var/run/docker.sock:/var/run/docker.sock | |||
| 236 | + | - ./gordon.toml:/app/gordon.toml:ro | |||
| 237 | + | - ./data:/data | |||
| 238 | + | restart: unless-stopped | |||
| 239 | + | ``` | |||
| 240 | + | ||||
| 241 | + | ## 🤔 FAQ | |||
| 242 | + | ||||
| 243 | + | **Q: How is this different from Traefik/Nginx Proxy Manager?** | |||
| 244 | + | A: Gordon includes a Docker registry and automatic deployment. Push an image, get a deployment. No manual container management. | |||
| 245 | + | ||||
| 246 | + | **Q: Where do builds happen?** | |||
| 247 | + | A: On YOUR machine. If it runs locally, it'll run in production. No mysterious build server issues. | |||
| 248 | + | ||||
| 249 | + | **Q: How do I handle broken deployments?** | |||
| 250 | + | A: Just edit gordon.toml to point to a previous version. Rollback takes seconds, not minutes. | |||
| 251 | + | ||||
| 252 | + | **Q: Do I need CI/CD?** | |||
| 253 | + | A: Nope! Your laptop is the CI/CD. Build, test locally, push when ready. Keep it simple. | |||
| 254 | + | ||||
| 255 | + | **Q: Do I need Cloudflare?** | |||
| 256 | + | A: Recommended for free SSL and DDoS protection, but Gordon works with any DNS provider. | |||
| 257 | + | ||||
| 258 | + | **Q: Can I run databases?** | |||
| 259 | + | A: Yes! Gordon manages any container. Use volumes for persistent data. | |||
| 260 | + | ||||
| 261 | + | **Q: Resource requirements?** | |||
| 262 | + | A: Runs comfortably on 1GB RAM VPS. Gordon itself uses <50MB. | |||
| 263 | + | ||||
| 264 | + | **Q: What about secrets?** | |||
| 265 | + | A: Use environment variables in your container or Docker secrets. Gordon doesn't interfere. | |||
| 266 | + | ||||
| 267 | + | ## 💡 Philosophy: Build Local, Deploy Simple | |||
| 268 | + | ||||
| 269 | + | ### Why No Build Servers? | |||
| 270 | + | ||||
| 271 | + | 1. **Your machine already works** - Why replicate your dev environment in CI/CD? | |||
| 272 | + | 2. **Instant feedback** - Build errors show up immediately, not after pushing | |||
| 273 | + | 3. **Perfect reproducibility** - "Works on my machine" becomes a feature, not a bug | |||
| 274 | + | 4. **Zero build queues** - Deploy as fast as your internet can push | |||
| 275 | + | ||||
| 276 | + | ### The Gordon Way | |||
| 277 | + | ||||
| 278 | + | ```bash | |||
| 279 | + | # Traditional CI/CD | |||
| 280 | + | push code → wait for build → hope it works → debug remotely → repeat | |||
| 281 | + | ||||
| 282 | + | # The Gordon Way | |||
| 283 | + | build locally → test locally → push image → instant deploy | |||
| 284 | + | ``` | |||
| 285 | + | ||||
| 286 | + | ## 🏗️ Architecture | |||
| 287 | + | ||||
| 288 | + | ``` | |||
| 289 | + | Your Machine → Docker Image → Gordon Registry → Auto-Deploy → Live App | |||
| 290 | + | ↓ | |||
| 291 | + | Event System | |||
| 292 | + | ↓ | |||
| 293 | + | Domain Router → Container | |||
| 294 | + | ``` | |||
| 295 | + | ||||
| 296 | + | - **Event-Driven**: Push events trigger deployments automatically | |||
| 297 | + | - **Config Hot-Reload**: Edit gordon.toml, changes apply instantly | |||
| 298 | + | - **Single Binary**: No dependencies except Docker | |||
| 299 | + | - **Stateless**: Configuration is the only state | |||
| 300 | + | - **Fast**: Written in Go for minimal overhead | |||
| 301 | + | ||||
| 302 | + | ## 🚀 Advanced Usage | |||
| 303 | + | ||||
| 304 | + | ### Custom Ports | |||
| 305 | + | ```toml | |||
| 306 | + | # Gordon auto-detects ports: 80, 8080, 3000 | |||
| 307 | + | # Or use EXPOSE in Dockerfile | |||
| 308 | + | ``` | |||
| 309 | + | ||||
| 310 | + | ### Registry Operations | |||
| 311 | + | ```bash | |||
| 312 | + | # List images | |||
| 313 | + | curl -u admin:password https://registry.yourdomain.com/v2/_catalog | |||
| 314 | + | ||||
| 315 | + | # List tags | |||
| 316 | + | curl -u admin:password https://registry.yourdomain.com/v2/myapp/tags/list | |||
| 317 | + | ``` | |||
| 318 | + | ||||
| 319 | + | ### Multi-Server Setup | |||
| 320 | + | Deploy Gordon on multiple VPS servers with different configs for geographic distribution or separation of concerns. | |||
| 321 | + | ||||
| 322 | + | ## 🤝 Contributing | |||
| 323 | + | ||||
| 324 | + | Gordon is open source and we love contributions! Check out our [issues](https://github.com/yourusername/gordon/issues) or submit a PR. | |||
| 325 | + | ||||
| 326 | + | ## 📄 License | |||
| 327 | + | ||||
| 328 | + | GPL-3.0 - Use freely, contribute back. | |||
| 329 | + | ||||
| 330 | + | --- | |||
| 331 | + | ||||
| 332 | + | <div align="center"> | |||
| 333 | + | <p><strong>Stop overcomplicating container deployment.</strong></p> | |||
| 334 | + | <p>Star ⭐ this repo if Gordon saves you money!</p> | |||
| 335 | + | </div>⏎̸ | |||
D cmd/generate.go +0 -112
| @@ -1,112 +0,0 @@ | |||
| 1 | - | package cmd | |
| 2 | - | ||
| 3 | - | import ( | |
| 4 | - | "fmt" | |
| 5 | - | "time" | |
| 6 | - | ||
| 7 | - | "github.com/spf13/cobra" | |
| 8 | - | "gordon/internal/middleware" | |
| 9 | - | ) | |
| 10 | - | ||
| 11 | - | var generateCmd = &cobra.Command{ | |
| 12 | - | Use: "generate", | |
| 13 | - | Short: "Generate authentication tokens and secrets", | |
| 14 | - | Long: `Generate JWT tokens, API keys, and JWT secrets for Gordon authentication`, | |
| 15 | - | } | |
| 16 | - | ||
| 17 | - | var generateSecretCmd = &cobra.Command{ | |
| 18 | - | Use: "jwt-secret", | |
| 19 | - | Short: "Generate a secure JWT secret", | |
| 20 | - | Long: `Generate a cryptographically secure JWT secret for signing tokens`, | |
| 21 | - | Run: func(cmd *cobra.Command, args []string) { | |
| 22 | - | secret, err := middleware.GenerateJWTSecret() | |
| 23 | - | if err != nil { | |
| 24 | - | fmt.Printf("Error generating JWT secret: %v\n", err) | |
| 25 | - | return | |
| 26 | - | } | |
| 27 | - | ||
| 28 | - | fmt.Printf("Generated JWT Secret:\n") | |
| 29 | - | fmt.Printf("%s\n\n", secret) | |
| 30 | - | fmt.Printf("Add this to your gordon.toml configuration:\n") | |
| 31 | - | fmt.Printf("[auth]\n") | |
| 32 | - | fmt.Printf("enabled = true\n") | |
| 33 | - | fmt.Printf("method = \"jwt\"\n") | |
| 34 | - | fmt.Printf("jwt_secret = \"%s\"\n", secret) | |
| 35 | - | }, | |
| 36 | - | } | |
| 37 | - | ||
| 38 | - | var generateTokenCmd = &cobra.Command{ | |
| 39 | - | Use: "jwt-token", | |
| 40 | - | Short: "Generate a JWT token", | |
| 41 | - | Long: `Generate a JWT token for accessing Gordon's management API`, | |
| 42 | - | Run: func(cmd *cobra.Command, args []string) { | |
| 43 | - | username, _ := cmd.Flags().GetString("username") | |
| 44 | - | roles, _ := cmd.Flags().GetStringSlice("roles") | |
| 45 | - | jwtSecret, _ := cmd.Flags().GetString("secret") | |
| 46 | - | validFor, _ := cmd.Flags().GetDuration("valid-for") | |
| 47 | - | ||
| 48 | - | if username == "" { | |
| 49 | - | fmt.Println("Error: username is required") | |
| 50 | - | return | |
| 51 | - | } | |
| 52 | - | ||
| 53 | - | if jwtSecret == "" { | |
| 54 | - | fmt.Println("Error: JWT secret is required (use --secret flag or generate one with 'gordon generate jwt-secret')") | |
| 55 | - | return | |
| 56 | - | } | |
| 57 | - | ||
| 58 | - | token, err := middleware.GenerateJWTToken(username, roles, jwtSecret, validFor) | |
| 59 | - | if err != nil { | |
| 60 | - | fmt.Printf("Error generating JWT token: %v\n", err) | |
| 61 | - | return | |
| 62 | - | } | |
| 63 | - | ||
| 64 | - | fmt.Printf("Generated JWT Token for user '%s':\n", username) | |
| 65 | - | fmt.Printf("%s\n\n", token) | |
| 66 | - | fmt.Printf("Token expires in: %v\n", validFor) | |
| 67 | - | fmt.Printf("Roles: %v\n\n", roles) | |
| 68 | - | fmt.Printf("Use this token in API requests:\n") | |
| 69 | - | fmt.Printf("curl -H \"Authorization: Bearer %s\" http://localhost:8080/api/status\n", token) | |
| 70 | - | }, | |
| 71 | - | } | |
| 72 | - | ||
| 73 | - | var generateAPIKeyCmd = &cobra.Command{ | |
| 74 | - | Use: "api-key", | |
| 75 | - | Short: "Generate a secure API key", | |
| 76 | - | Long: `Generate a cryptographically secure API key for Gordon authentication`, | |
| 77 | - | Run: func(cmd *cobra.Command, args []string) { | |
| 78 | - | apiKey, err := middleware.GenerateSecureAPIKey() | |
| 79 | - | if err != nil { | |
| 80 | - | fmt.Printf("Error generating API key: %v\n", err) | |
| 81 | - | return | |
| 82 | - | } | |
| 83 | - | ||
| 84 | - | fmt.Printf("Generated API Key:\n") | |
| 85 | - | fmt.Printf("%s\n\n", apiKey) | |
| 86 | - | fmt.Printf("Add this to your gordon.toml configuration:\n") | |
| 87 | - | fmt.Printf("[auth]\n") | |
| 88 | - | fmt.Printf("enabled = true\n") | |
| 89 | - | fmt.Printf("method = \"api_key\"\n") | |
| 90 | - | fmt.Printf("api_key = \"%s\"\n\n", apiKey) | |
| 91 | - | fmt.Printf("Use this API key in requests:\n") | |
| 92 | - | fmt.Printf("curl -H \"X-API-Key: %s\" http://localhost:8080/api/status\n", apiKey) | |
| 93 | - | fmt.Printf("# OR\n") | |
| 94 | - | fmt.Printf("curl \"http://localhost:8080/api/status?api_key=%s\"\n", apiKey) | |
| 95 | - | }, | |
| 96 | - | } | |
| 97 | - | ||
| 98 | - | func init() { | |
| 99 | - | rootCmd.AddCommand(generateCmd) | |
| 100 | - | generateCmd.AddCommand(generateSecretCmd) | |
| 101 | - | generateCmd.AddCommand(generateTokenCmd) | |
| 102 | - | generateCmd.AddCommand(generateAPIKeyCmd) | |
| 103 | - | ||
| 104 | - | // JWT token flags | |
| 105 | - | generateTokenCmd.Flags().StringP("username", "u", "", "Username for the token (required)") | |
| 106 | - | generateTokenCmd.Flags().StringSliceP("roles", "r", []string{"admin"}, "Roles for the token") | |
| 107 | - | generateTokenCmd.Flags().StringP("secret", "s", "", "JWT secret for signing (required)") | |
| 108 | - | generateTokenCmd.Flags().DurationP("valid-for", "t", 24*time.Hour, "Token validity duration") | |
| 109 | - | ||
| 110 | - | generateTokenCmd.MarkFlagRequired("username") | |
| 111 | - | generateTokenCmd.MarkFlagRequired("secret") | |
| 112 | - | }⏎̸ | |
| @@ -1,112 +0,0 @@ | |||||
| 1 | - | package cmd | |||
| 2 | - | ||||
| 3 | - | import ( | |||
| 4 | - | "fmt" | |||
| 5 | - | "time" | |||
| 6 | - | ||||
| 7 | - | "github.com/spf13/cobra" | |||
| 8 | - | "gordon/internal/middleware" | |||
| 9 | - | ) | |||
| 10 | - | ||||
| 11 | - | var generateCmd = &cobra.Command{ | |||
| 12 | - | Use: "generate", | |||
| 13 | - | Short: "Generate authentication tokens and secrets", | |||
| 14 | - | Long: `Generate JWT tokens, API keys, and JWT secrets for Gordon authentication`, | |||
| 15 | - | } | |||
| 16 | - | ||||
| 17 | - | var generateSecretCmd = &cobra.Command{ | |||
| 18 | - | Use: "jwt-secret", | |||
| 19 | - | Short: "Generate a secure JWT secret", | |||
| 20 | - | Long: `Generate a cryptographically secure JWT secret for signing tokens`, | |||
| 21 | - | Run: func(cmd *cobra.Command, args []string) { | |||
| 22 | - | secret, err := middleware.GenerateJWTSecret() | |||
| 23 | - | if err != nil { | |||
| 24 | - | fmt.Printf("Error generating JWT secret: %v\n", err) | |||
| 25 | - | return | |||
| 26 | - | } | |||
| 27 | - | ||||
| 28 | - | fmt.Printf("Generated JWT Secret:\n") | |||
| 29 | - | fmt.Printf("%s\n\n", secret) | |||
| 30 | - | fmt.Printf("Add this to your gordon.toml configuration:\n") | |||
| 31 | - | fmt.Printf("[auth]\n") | |||
| 32 | - | fmt.Printf("enabled = true\n") | |||
| 33 | - | fmt.Printf("method = \"jwt\"\n") | |||
| 34 | - | fmt.Printf("jwt_secret = \"%s\"\n", secret) | |||
| 35 | - | }, | |||
| 36 | - | } | |||
| 37 | - | ||||
| 38 | - | var generateTokenCmd = &cobra.Command{ | |||
| 39 | - | Use: "jwt-token", | |||
| 40 | - | Short: "Generate a JWT token", | |||
| 41 | - | Long: `Generate a JWT token for accessing Gordon's management API`, | |||
| 42 | - | Run: func(cmd *cobra.Command, args []string) { | |||
| 43 | - | username, _ := cmd.Flags().GetString("username") | |||
| 44 | - | roles, _ := cmd.Flags().GetStringSlice("roles") | |||
| 45 | - | jwtSecret, _ := cmd.Flags().GetString("secret") | |||
| 46 | - | validFor, _ := cmd.Flags().GetDuration("valid-for") | |||
| 47 | - | ||||
| 48 | - | if username == "" { | |||
| 49 | - | fmt.Println("Error: username is required") | |||
| 50 | - | return | |||
| 51 | - | } | |||
| 52 | - | ||||
| 53 | - | if jwtSecret == "" { | |||
| 54 | - | fmt.Println("Error: JWT secret is required (use --secret flag or generate one with 'gordon generate jwt-secret')") | |||
| 55 | - | return | |||
| 56 | - | } | |||
| 57 | - | ||||
| 58 | - | token, err := middleware.GenerateJWTToken(username, roles, jwtSecret, validFor) | |||
| 59 | - | if err != nil { | |||
| 60 | - | fmt.Printf("Error generating JWT token: %v\n", err) | |||
| 61 | - | return | |||
| 62 | - | } | |||
| 63 | - | ||||
| 64 | - | fmt.Printf("Generated JWT Token for user '%s':\n", username) | |||
| 65 | - | fmt.Printf("%s\n\n", token) | |||
| 66 | - | fmt.Printf("Token expires in: %v\n", validFor) | |||
| 67 | - | fmt.Printf("Roles: %v\n\n", roles) | |||
| 68 | - | fmt.Printf("Use this token in API requests:\n") | |||
| 69 | - | fmt.Printf("curl -H \"Authorization: Bearer %s\" http://localhost:8080/api/status\n", token) | |||
| 70 | - | }, | |||
| 71 | - | } | |||
| 72 | - | ||||
| 73 | - | var generateAPIKeyCmd = &cobra.Command{ | |||
| 74 | - | Use: "api-key", | |||
| 75 | - | Short: "Generate a secure API key", | |||
| 76 | - | Long: `Generate a cryptographically secure API key for Gordon authentication`, | |||
| 77 | - | Run: func(cmd *cobra.Command, args []string) { | |||
| 78 | - | apiKey, err := middleware.GenerateSecureAPIKey() | |||
| 79 | - | if err != nil { | |||
| 80 | - | fmt.Printf("Error generating API key: %v\n", err) | |||
| 81 | - | return | |||
| 82 | - | } | |||
| 83 | - | ||||
| 84 | - | fmt.Printf("Generated API Key:\n") | |||
| 85 | - | fmt.Printf("%s\n\n", apiKey) | |||
| 86 | - | fmt.Printf("Add this to your gordon.toml configuration:\n") | |||
| 87 | - | fmt.Printf("[auth]\n") | |||
| 88 | - | fmt.Printf("enabled = true\n") | |||
| 89 | - | fmt.Printf("method = \"api_key\"\n") | |||
| 90 | - | fmt.Printf("api_key = \"%s\"\n\n", apiKey) | |||
| 91 | - | fmt.Printf("Use this API key in requests:\n") | |||
| 92 | - | fmt.Printf("curl -H \"X-API-Key: %s\" http://localhost:8080/api/status\n", apiKey) | |||
| 93 | - | fmt.Printf("# OR\n") | |||
| 94 | - | fmt.Printf("curl \"http://localhost:8080/api/status?api_key=%s\"\n", apiKey) | |||
| 95 | - | }, | |||
| 96 | - | } | |||
| 97 | - | ||||
| 98 | - | func init() { | |||
| 99 | - | rootCmd.AddCommand(generateCmd) | |||
| 100 | - | generateCmd.AddCommand(generateSecretCmd) | |||
| 101 | - | generateCmd.AddCommand(generateTokenCmd) | |||
| 102 | - | generateCmd.AddCommand(generateAPIKeyCmd) | |||
| 103 | - | ||||
| 104 | - | // JWT token flags | |||
| 105 | - | generateTokenCmd.Flags().StringP("username", "u", "", "Username for the token (required)") | |||
| 106 | - | generateTokenCmd.Flags().StringSliceP("roles", "r", []string{"admin"}, "Roles for the token") | |||
| 107 | - | generateTokenCmd.Flags().StringP("secret", "s", "", "JWT secret for signing (required)") | |||
| 108 | - | generateTokenCmd.Flags().DurationP("valid-for", "t", 24*time.Hour, "Token validity duration") | |||
| 109 | - | ||||
| 110 | - | generateTokenCmd.MarkFlagRequired("username") | |||
| 111 | - | generateTokenCmd.MarkFlagRequired("secret") | |||
| 112 | - | }⏎̸ | |||
M cmd/root.go +26 -2
| @@ -31,7 +31,27 @@ func initConfig() { | |||
| 31 | 31 | if cfgFile != "" { | |
| 32 | 32 | viper.SetConfigFile(cfgFile) | |
| 33 | 33 | } else { | |
| 34 | - | log.Fatal().Msg("config file not found") | |
| 34 | + | // Search for config file in standard locations | |
| 35 | + | viper.SetConfigName("gordon") | |
| 36 | + | viper.SetConfigType("toml") | |
| 37 | + | ||
| 38 | + | // Current directory (highest priority) | |
| 39 | + | viper.AddConfigPath(".") | |
| 40 | + | ||
| 41 | + | // User config directory | |
| 42 | + | if userConfigDir, err := os.UserConfigDir(); err == nil { | |
| 43 | + | viper.AddConfigPath(userConfigDir + "/gordon") | |
| 44 | + | } | |
| 45 | + | ||
| 46 | + | // User home directory | |
| 47 | + | if homeDir, err := os.UserHomeDir(); err == nil { | |
| 48 | + | viper.AddConfigPath(homeDir + "/.gordon") | |
| 49 | + | viper.AddConfigPath(homeDir) | |
| 50 | + | } | |
| 51 | + | ||
| 52 | + | // System-wide config directories | |
| 53 | + | viper.AddConfigPath("/etc/gordon") | |
| 54 | + | viper.AddConfigPath("/usr/local/etc/gordon") | |
| 35 | 55 | } | |
| 36 | 56 | ||
| 37 | 57 | viper.AutomaticEnv() | |
| @@ -39,6 +59,10 @@ func initConfig() { | |||
| 39 | 59 | if err := viper.ReadInConfig(); err == nil { | |
| 40 | 60 | fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed()) | |
| 41 | 61 | } else { | |
| 42 | - | fmt.Fprintf(os.Stderr, "Error reading config file: %v\n", err) | |
| 62 | + | if cfgFile != "" { | |
| 63 | + | fmt.Fprintf(os.Stderr, "Error reading config file: %v\n", err) | |
| 64 | + | } else { | |
| 65 | + | log.Fatal().Msg("config file not found - please specify with --config flag or ensure gordon.toml exists in current directory") | |
| 66 | + | } | |
| 43 | 67 | } | |
| 44 | 68 | } | |
| @@ -31,7 +31,27 @@ func initConfig() { | |||||
| 31 | if cfgFile != "" { | 31 | if cfgFile != "" { | ||
| 32 | viper.SetConfigFile(cfgFile) | 32 | viper.SetConfigFile(cfgFile) | ||
| 33 | } else { | 33 | } else { | ||
| 34 | - | log.Fatal().Msg("config file not found") | 34 | + | // Search for config file in standard locations |
| 35 | + | viper.SetConfigName("gordon") | |||
| 36 | + | viper.SetConfigType("toml") | |||
| 37 | + | ||||
| 38 | + | // Current directory (highest priority) | |||
| 39 | + | viper.AddConfigPath(".") | |||
| 40 | + | ||||
| 41 | + | // User config directory | |||
| 42 | + | if userConfigDir, err := os.UserConfigDir(); err == nil { | |||
| 43 | + | viper.AddConfigPath(userConfigDir + "/gordon") | |||
| 44 | + | } | |||
| 45 | + | ||||
| 46 | + | // User home directory | |||
| 47 | + | if homeDir, err := os.UserHomeDir(); err == nil { | |||
| 48 | + | viper.AddConfigPath(homeDir + "/.gordon") | |||
| 49 | + | viper.AddConfigPath(homeDir) | |||
| 50 | + | } | |||
| 51 | + | ||||
| 52 | + | // System-wide config directories | |||
| 53 | + | viper.AddConfigPath("/etc/gordon") | |||
| 54 | + | viper.AddConfigPath("/usr/local/etc/gordon") | |||
| 35 | } | 55 | } | ||
| 36 | 56 | ||||
| 37 | viper.AutomaticEnv() | 57 | viper.AutomaticEnv() | ||
| @@ -39,6 +59,10 @@ func initConfig() { | |||||
| 39 | if err := viper.ReadInConfig(); err == nil { | 59 | if err := viper.ReadInConfig(); err == nil { | ||
| 40 | fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed()) | 60 | fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed()) | ||
| 41 | } else { | 61 | } else { | ||
| 42 | - | fmt.Fprintf(os.Stderr, "Error reading config file: %v\n", err) | 62 | + | if cfgFile != "" { |
| 63 | + | fmt.Fprintf(os.Stderr, "Error reading config file: %v\n", err) | |||
| 64 | + | } else { | |||
| 65 | + | log.Fatal().Msg("config file not found - please specify with --config flag or ensure gordon.toml exists in current directory") | |||
| 66 | + | } | |||
| 43 | } | 67 | } | ||
| 44 | } | 68 | } | ||
M cmd/start.go +42 -0
| @@ -8,9 +8,12 @@ import ( | |||
| 8 | 8 | "syscall" | |
| 9 | 9 | "time" | |
| 10 | 10 | ||
| 11 | + | "github.com/fsnotify/fsnotify" | |
| 12 | + | "github.com/google/uuid" | |
| 11 | 13 | "github.com/rs/zerolog" | |
| 12 | 14 | "github.com/rs/zerolog/log" | |
| 13 | 15 | "github.com/spf13/cobra" | |
| 16 | + | "github.com/spf13/viper" | |
| 14 | 17 | "gordon/internal/config" | |
| 15 | 18 | "gordon/internal/container" | |
| 16 | 19 | "gordon/internal/events" | |
| @@ -62,6 +65,45 @@ func runStart(cmd *cobra.Command, args []string) { | |||
| 62 | 65 | log.Fatal().Err(err).Msg("Failed to subscribe container event handler") | |
| 63 | 66 | } | |
| 64 | 67 | ||
| 68 | + | // Setup config file watching for live reload | |
| 69 | + | viper.WatchConfig() | |
| 70 | + | viper.OnConfigChange(func(e fsnotify.Event) { | |
| 71 | + | log.Info(). | |
| 72 | + | Str("file", e.Name). | |
| 73 | + | Str("op", e.Op.String()). | |
| 74 | + | Msg("Configuration file changed, reloading...") | |
| 75 | + | ||
| 76 | + | // Reload configuration | |
| 77 | + | newCfg, err := config.Load() | |
| 78 | + | if err != nil { | |
| 79 | + | log.Error().Err(err).Msg("Failed to reload configuration") | |
| 80 | + | return | |
| 81 | + | } | |
| 82 | + | ||
| 83 | + | // Update the configuration reference | |
| 84 | + | cfg = newCfg | |
| 85 | + | ||
| 86 | + | // Update container handler config | |
| 87 | + | containerHandler = events.NewContainerEventHandler(manager, cfg) | |
| 88 | + | if err := eventBus.Subscribe(containerHandler); err != nil { | |
| 89 | + | log.Error().Err(err).Msg("Failed to re-subscribe container event handler") | |
| 90 | + | return | |
| 91 | + | } | |
| 92 | + | ||
| 93 | + | // Publish config reload event | |
| 94 | + | configReloadEvent := events.Event{ | |
| 95 | + | ID: uuid.New().String(), | |
| 96 | + | Type: events.ConfigReload, | |
| 97 | + | Timestamp: time.Now(), | |
| 98 | + | } | |
| 99 | + | ||
| 100 | + | if err := eventBus.Publish(configReloadEvent); err != nil { | |
| 101 | + | log.Error().Err(err).Msg("Failed to publish config reload event") | |
| 102 | + | } else { | |
| 103 | + | log.Info().Msg("Configuration reloaded successfully") | |
| 104 | + | } | |
| 105 | + | }) | |
| 106 | + | ||
| 65 | 107 | // Sync existing containers | |
| 66 | 108 | ctx, cancel := context.WithCancel(context.Background()) | |
| 67 | 109 | if err := manager.SyncContainers(ctx); err != nil { | |
| @@ -8,9 +8,12 @@ import ( | |||||
| 8 | "syscall" | 8 | "syscall" | ||
| 9 | "time" | 9 | "time" | ||
| 10 | 10 | ||||
| 11 | + | "github.com/fsnotify/fsnotify" | |||
| 12 | + | "github.com/google/uuid" | |||
| 11 | "github.com/rs/zerolog" | 13 | "github.com/rs/zerolog" | ||
| 12 | "github.com/rs/zerolog/log" | 14 | "github.com/rs/zerolog/log" | ||
| 13 | "github.com/spf13/cobra" | 15 | "github.com/spf13/cobra" | ||
| 16 | + | "github.com/spf13/viper" | |||
| 14 | "gordon/internal/config" | 17 | "gordon/internal/config" | ||
| 15 | "gordon/internal/container" | 18 | "gordon/internal/container" | ||
| 16 | "gordon/internal/events" | 19 | "gordon/internal/events" | ||
| @@ -62,6 +65,45 @@ func runStart(cmd *cobra.Command, args []string) { | |||||
| 62 | log.Fatal().Err(err).Msg("Failed to subscribe container event handler") | 65 | log.Fatal().Err(err).Msg("Failed to subscribe container event handler") | ||
| 63 | } | 66 | } | ||
| 64 | 67 | ||||
| 68 | + | // Setup config file watching for live reload | |||
| 69 | + | viper.WatchConfig() | |||
| 70 | + | viper.OnConfigChange(func(e fsnotify.Event) { | |||
| 71 | + | log.Info(). | |||
| 72 | + | Str("file", e.Name). | |||
| 73 | + | Str("op", e.Op.String()). | |||
| 74 | + | Msg("Configuration file changed, reloading...") | |||
| 75 | + | ||||
| 76 | + | // Reload configuration | |||
| 77 | + | newCfg, err := config.Load() | |||
| 78 | + | if err != nil { | |||
| 79 | + | log.Error().Err(err).Msg("Failed to reload configuration") | |||
| 80 | + | return | |||
| 81 | + | } | |||
| 82 | + | ||||
| 83 | + | // Update the configuration reference | |||
| 84 | + | cfg = newCfg | |||
| 85 | + | ||||
| 86 | + | // Update container handler config | |||
| 87 | + | containerHandler = events.NewContainerEventHandler(manager, cfg) | |||
| 88 | + | if err := eventBus.Subscribe(containerHandler); err != nil { | |||
| 89 | + | log.Error().Err(err).Msg("Failed to re-subscribe container event handler") | |||
| 90 | + | return | |||
| 91 | + | } | |||
| 92 | + | ||||
| 93 | + | // Publish config reload event | |||
| 94 | + | configReloadEvent := events.Event{ | |||
| 95 | + | ID: uuid.New().String(), | |||
| 96 | + | Type: events.ConfigReload, | |||
| 97 | + | Timestamp: time.Now(), | |||
| 98 | + | } | |||
| 99 | + | ||||
| 100 | + | if err := eventBus.Publish(configReloadEvent); err != nil { | |||
| 101 | + | log.Error().Err(err).Msg("Failed to publish config reload event") | |||
| 102 | + | } else { | |||
| 103 | + | log.Info().Msg("Configuration reloaded successfully") | |||
| 104 | + | } | |||
| 105 | + | }) | |||
| 106 | + | ||||
| 65 | // Sync existing containers | 107 | // Sync existing containers | ||
| 66 | ctx, cancel := context.WithCancel(context.Background()) | 108 | ctx, cancel := context.WithCancel(context.Background()) | ||
| 67 | if err := manager.SyncContainers(ctx); err != nil { | 109 | if err := manager.SyncContainers(ctx); err != nil { | ||
A examples/README.md +144 -0
| @@ -0,0 +1,144 @@ | |||
| 1 | + | # Gordon v2 Configuration Examples | |
| 2 | + | ||
| 3 | + | This directory contains configuration examples for different use cases and environments. | |
| 4 | + | ||
| 5 | + | ## 📁 Available Examples | |
| 6 | + | ||
| 7 | + | ### 🚀 [`minimal.toml`](minimal.toml) | |
| 8 | + | **Perfect for getting started** | |
| 9 | + | - Single route configuration | |
| 10 | + | - Default settings | |
| 11 | + | - No authentication | |
| 12 | + | - Ideal for testing and learning | |
| 13 | + | ||
| 14 | + | ### 🏠 [`development.toml`](development.toml) | |
| 15 | + | **Local development setup** | |
| 16 | + | - Multiple .local domains | |
| 17 | + | - No authentication for ease of use | |
| 18 | + | - Third-party development tools | |
| 19 | + | - localhost registry | |
| 20 | + | ||
| 21 | + | ### 🧪 [`staging.toml`](staging.toml) | |
| 22 | + | **Staging and preview environments** | |
| 23 | + | - Branch-based deployments | |
| 24 | + | - Feature branch testing | |
| 25 | + | - PR preview environments | |
| 26 | + | - Separate staging registry | |
| 27 | + | ||
| 28 | + | ### 🏭 [`production.toml`](production.toml) | |
| 29 | + | **Production-ready configuration** | |
| 30 | + | - Pinned image versions | |
| 31 | + | - Registry authentication | |
| 32 | + | - Multiple production services | |
| 33 | + | - Monitoring tools included | |
| 34 | + | ||
| 35 | + | ### 🏢 [`saas-multi-tenant.toml`](saas-multi-tenant.toml) | |
| 36 | + | **Multi-tenant SaaS platform** | |
| 37 | + | - Customer subdomains | |
| 38 | + | - Custom domains | |
| 39 | + | - Shared application architecture | |
| 40 | + | - Enterprise features | |
| 41 | + | ||
| 42 | + | ## 🚀 Quick Start | |
| 43 | + | ||
| 44 | + | 1. **Choose an example** that matches your use case | |
| 45 | + | 2. **Copy the config file** to your Gordon directory: | |
| 46 | + | ```bash | |
| 47 | + | cp examples/minimal.toml gordon.toml | |
| 48 | + | ``` | |
| 49 | + | 3. **Edit the configuration** with your domains and settings | |
| 50 | + | 4. **Start Gordon**: | |
| 51 | + | ```bash | |
| 52 | + | gordon start | |
| 53 | + | ``` | |
| 54 | + | ||
| 55 | + | ## 🔧 Customization Tips | |
| 56 | + | ||
| 57 | + | ### Domain Configuration | |
| 58 | + | Update the `[routes]` section with your actual domains: | |
| 59 | + | ```toml | |
| 60 | + | [routes] | |
| 61 | + | "your-domain.com" = "your-app:latest" | |
| 62 | + | "api.your-domain.com" = "your-api:v1.0.0" | |
| 63 | + | ``` | |
| 64 | + | ||
| 65 | + | ### Registry Setup | |
| 66 | + | Configure your registry domain and authentication: | |
| 67 | + | ```toml | |
| 68 | + | [server] | |
| 69 | + | registry_domain = "registry.your-domain.com" | |
| 70 | + | ||
| 71 | + | [registry_auth] | |
| 72 | + | enabled = true | |
| 73 | + | username = "your-username" | |
| 74 | + | password = "your-secure-password" | |
| 75 | + | ``` | |
| 76 | + | ||
| 77 | + | ### Environment Variables | |
| 78 | + | For production, use environment variables for sensitive data: | |
| 79 | + | ```bash | |
| 80 | + | export GORDON_REGISTRY_PASSWORD="your-secure-password" | |
| 81 | + | export GORDON_SSL_EMAIL="admin@your-domain.com" | |
| 82 | + | ``` | |
| 83 | + | ||
| 84 | + | ## 🌐 DNS Configuration | |
| 85 | + | ||
| 86 | + | ### Cloudflare Setup (Recommended) | |
| 87 | + | For all examples except development, set up these DNS records: | |
| 88 | + | ||
| 89 | + | ``` | |
| 90 | + | Type Name Content | |
| 91 | + | A app.your-domain.com YOUR_SERVER_IP | |
| 92 | + | A api.your-domain.com YOUR_SERVER_IP | |
| 93 | + | A registry.your-domain.com YOUR_SERVER_IP | |
| 94 | + | ``` | |
| 95 | + | ||
| 96 | + | Or use a wildcard: | |
| 97 | + | ``` | |
| 98 | + | Type Name Content | |
| 99 | + | A *.your-domain.com YOUR_SERVER_IP | |
| 100 | + | ``` | |
| 101 | + | ||
| 102 | + | ### Local Development | |
| 103 | + | For the development example, add to `/etc/hosts`: | |
| 104 | + | ``` | |
| 105 | + | 127.0.0.1 app.local | |
| 106 | + | 127.0.0.1 api.local | |
| 107 | + | 127.0.0.1 admin.local | |
| 108 | + | ``` | |
| 109 | + | ||
| 110 | + | ## 🔄 Workflow Examples | |
| 111 | + | ||
| 112 | + | ### Simple Deployment | |
| 113 | + | ```bash | |
| 114 | + | # Build your app | |
| 115 | + | docker build -t myapp:latest . | |
| 116 | + | ||
| 117 | + | # Tag for registry | |
| 118 | + | docker tag myapp:latest registry.your-domain.com/myapp:latest | |
| 119 | + | ||
| 120 | + | # Push to deploy | |
| 121 | + | docker push registry.your-domain.com/myapp:latest | |
| 122 | + | ``` | |
| 123 | + | ||
| 124 | + | ### Version Management | |
| 125 | + | ```bash | |
| 126 | + | # Tag with version | |
| 127 | + | docker tag myapp:latest registry.your-domain.com/myapp:v1.2.0 | |
| 128 | + | ||
| 129 | + | # Update config | |
| 130 | + | # "app.your-domain.com" = "myapp:v1.2.0" | |
| 131 | + | ||
| 132 | + | # Push to deploy specific version | |
| 133 | + | docker push registry.your-domain.com/myapp:v1.2.0 | |
| 134 | + | ``` | |
| 135 | + | ||
| 136 | + | ## 🤝 Need Help? | |
| 137 | + | ||
| 138 | + | - 📖 **Read the main [README](../README.md)** for detailed documentation | |
| 139 | + | - 🔍 **Check the logs** for deployment issues | |
| 140 | + | - 🐛 **Open an issue** if you find problems with these examples | |
| 141 | + | - 💡 **Contribute** your own configuration examples! | |
| 142 | + | ||
| 143 | + | --- | |
| 144 | + | *These examples are starting points - customize them for your specific needs!*⏎̸ | |
| @@ -0,0 +1,144 @@ | |||||
| 1 | + | # Gordon v2 Configuration Examples | |||
| 2 | + | ||||
| 3 | + | This directory contains configuration examples for different use cases and environments. | |||
| 4 | + | ||||
| 5 | + | ## 📁 Available Examples | |||
| 6 | + | ||||
| 7 | + | ### 🚀 [`minimal.toml`](minimal.toml) | |||
| 8 | + | **Perfect for getting started** | |||
| 9 | + | - Single route configuration | |||
| 10 | + | - Default settings | |||
| 11 | + | - No authentication | |||
| 12 | + | - Ideal for testing and learning | |||
| 13 | + | ||||
| 14 | + | ### 🏠 [`development.toml`](development.toml) | |||
| 15 | + | **Local development setup** | |||
| 16 | + | - Multiple .local domains | |||
| 17 | + | - No authentication for ease of use | |||
| 18 | + | - Third-party development tools | |||
| 19 | + | - localhost registry | |||
| 20 | + | ||||
| 21 | + | ### 🧪 [`staging.toml`](staging.toml) | |||
| 22 | + | **Staging and preview environments** | |||
| 23 | + | - Branch-based deployments | |||
| 24 | + | - Feature branch testing | |||
| 25 | + | - PR preview environments | |||
| 26 | + | - Separate staging registry | |||
| 27 | + | ||||
| 28 | + | ### 🏭 [`production.toml`](production.toml) | |||
| 29 | + | **Production-ready configuration** | |||
| 30 | + | - Pinned image versions | |||
| 31 | + | - Registry authentication | |||
| 32 | + | - Multiple production services | |||
| 33 | + | - Monitoring tools included | |||
| 34 | + | ||||
| 35 | + | ### 🏢 [`saas-multi-tenant.toml`](saas-multi-tenant.toml) | |||
| 36 | + | **Multi-tenant SaaS platform** | |||
| 37 | + | - Customer subdomains | |||
| 38 | + | - Custom domains | |||
| 39 | + | - Shared application architecture | |||
| 40 | + | - Enterprise features | |||
| 41 | + | ||||
| 42 | + | ## 🚀 Quick Start | |||
| 43 | + | ||||
| 44 | + | 1. **Choose an example** that matches your use case | |||
| 45 | + | 2. **Copy the config file** to your Gordon directory: | |||
| 46 | + | ```bash | |||
| 47 | + | cp examples/minimal.toml gordon.toml | |||
| 48 | + | ``` | |||
| 49 | + | 3. **Edit the configuration** with your domains and settings | |||
| 50 | + | 4. **Start Gordon**: | |||
| 51 | + | ```bash | |||
| 52 | + | gordon start | |||
| 53 | + | ``` | |||
| 54 | + | ||||
| 55 | + | ## 🔧 Customization Tips | |||
| 56 | + | ||||
| 57 | + | ### Domain Configuration | |||
| 58 | + | Update the `[routes]` section with your actual domains: | |||
| 59 | + | ```toml | |||
| 60 | + | [routes] | |||
| 61 | + | "your-domain.com" = "your-app:latest" | |||
| 62 | + | "api.your-domain.com" = "your-api:v1.0.0" | |||
| 63 | + | ``` | |||
| 64 | + | ||||
| 65 | + | ### Registry Setup | |||
| 66 | + | Configure your registry domain and authentication: | |||
| 67 | + | ```toml | |||
| 68 | + | [server] | |||
| 69 | + | registry_domain = "registry.your-domain.com" | |||
| 70 | + | ||||
| 71 | + | [registry_auth] | |||
| 72 | + | enabled = true | |||
| 73 | + | username = "your-username" | |||
| 74 | + | password = "your-secure-password" | |||
| 75 | + | ``` | |||
| 76 | + | ||||
| 77 | + | ### Environment Variables | |||
| 78 | + | For production, use environment variables for sensitive data: | |||
| 79 | + | ```bash | |||
| 80 | + | export GORDON_REGISTRY_PASSWORD="your-secure-password" | |||
| 81 | + | export GORDON_SSL_EMAIL="admin@your-domain.com" | |||
| 82 | + | ``` | |||
| 83 | + | ||||
| 84 | + | ## 🌐 DNS Configuration | |||
| 85 | + | ||||
| 86 | + | ### Cloudflare Setup (Recommended) | |||
| 87 | + | For all examples except development, set up these DNS records: | |||
| 88 | + | ||||
| 89 | + | ``` | |||
| 90 | + | Type Name Content | |||
| 91 | + | A app.your-domain.com YOUR_SERVER_IP | |||
| 92 | + | A api.your-domain.com YOUR_SERVER_IP | |||
| 93 | + | A registry.your-domain.com YOUR_SERVER_IP | |||
| 94 | + | ``` | |||
| 95 | + | ||||
| 96 | + | Or use a wildcard: | |||
| 97 | + | ``` | |||
| 98 | + | Type Name Content | |||
| 99 | + | A *.your-domain.com YOUR_SERVER_IP | |||
| 100 | + | ``` | |||
| 101 | + | ||||
| 102 | + | ### Local Development | |||
| 103 | + | For the development example, add to `/etc/hosts`: | |||
| 104 | + | ``` | |||
| 105 | + | 127.0.0.1 app.local | |||
| 106 | + | 127.0.0.1 api.local | |||
| 107 | + | 127.0.0.1 admin.local | |||
| 108 | + | ``` | |||
| 109 | + | ||||
| 110 | + | ## 🔄 Workflow Examples | |||
| 111 | + | ||||
| 112 | + | ### Simple Deployment | |||
| 113 | + | ```bash | |||
| 114 | + | # Build your app | |||
| 115 | + | docker build -t myapp:latest . | |||
| 116 | + | ||||
| 117 | + | # Tag for registry | |||
| 118 | + | docker tag myapp:latest registry.your-domain.com/myapp:latest | |||
| 119 | + | ||||
| 120 | + | # Push to deploy | |||
| 121 | + | docker push registry.your-domain.com/myapp:latest | |||
| 122 | + | ``` | |||
| 123 | + | ||||
| 124 | + | ### Version Management | |||
| 125 | + | ```bash | |||
| 126 | + | # Tag with version | |||
| 127 | + | docker tag myapp:latest registry.your-domain.com/myapp:v1.2.0 | |||
| 128 | + | ||||
| 129 | + | # Update config | |||
| 130 | + | # "app.your-domain.com" = "myapp:v1.2.0" | |||
| 131 | + | ||||
| 132 | + | # Push to deploy specific version | |||
| 133 | + | docker push registry.your-domain.com/myapp:v1.2.0 | |||
| 134 | + | ``` | |||
| 135 | + | ||||
| 136 | + | ## 🤝 Need Help? | |||
| 137 | + | ||||
| 138 | + | - 📖 **Read the main [README](../README.md)** for detailed documentation | |||
| 139 | + | - 🔍 **Check the logs** for deployment issues | |||
| 140 | + | - 🐛 **Open an issue** if you find problems with these examples | |||
| 141 | + | - 💡 **Contribute** your own configuration examples! | |||
| 142 | + | ||||
| 143 | + | --- | |||
| 144 | + | *These examples are starting points - customize them for your specific needs!*⏎̸ | |||
A examples/development.toml +44 -0
| @@ -0,0 +1,44 @@ | |||
| 1 | + | # Gordon v2 - Development Configuration Example | |
| 2 | + | # For local development and testing | |
| 3 | + | ||
| 4 | + | [server] | |
| 5 | + | port = 8080 | |
| 6 | + | registry_port = 5000 | |
| 7 | + | registry_domain = "localhost:8080" # Use localhost for local development | |
| 8 | + | runtime = "docker" | |
| 9 | + | ssl_email = "dev@localhost" | |
| 10 | + | data_dir = "./dev-data" | |
| 11 | + | ||
| 12 | + | [registry_auth] | |
| 13 | + | enabled = false # Disable auth for easier local development | |
| 14 | + | ||
| 15 | + | # Local development routes using .local domains | |
| 16 | + | [routes] | |
| 17 | + | "app.local" = "myapp:latest" # Main app - latest build | |
| 18 | + | "api.local" = "myapi:latest" # API service | |
| 19 | + | "admin.local" = "admin:latest" # Admin interface | |
| 20 | + | "docs.local" = "docs:latest" # Documentation | |
| 21 | + | ||
| 22 | + | # Test different versions locally | |
| 23 | + | "app-v1.local" = "myapp:v1.0.0" # Test older version | |
| 24 | + | "app-v2.local" = "myapp:v2.0.0" # Test newer version | |
| 25 | + | ||
| 26 | + | # Third-party services for development | |
| 27 | + | "db-admin.local" = "phpmyadmin:latest" # Database admin | |
| 28 | + | "mail.local" = "mailhog/mailhog:latest" # Email testing | |
| 29 | + | ||
| 30 | + | # Local development workflow: | |
| 31 | + | # 1. Add to /etc/hosts: | |
| 32 | + | # 127.0.0.1 app.local | |
| 33 | + | # 127.0.0.1 api.local | |
| 34 | + | # 127.0.0.1 admin.local | |
| 35 | + | # 127.0.0.1 docs.local | |
| 36 | + | # 127.0.0.1 localhost | |
| 37 | + | # | |
| 38 | + | # 2. Build your app: docker build -t myapp:latest . | |
| 39 | + | # 3. Push to local registry: | |
| 40 | + | # docker tag myapp:latest localhost:8080/myapp:latest | |
| 41 | + | # docker push localhost:8080/myapp:latest | |
| 42 | + | # 4. Access at http://app.local:8080 | |
| 43 | + | # | |
| 44 | + | # No Cloudflare needed - everything runs locally!⏎̸ | |
| @@ -0,0 +1,44 @@ | |||||
| 1 | + | # Gordon v2 - Development Configuration Example | |||
| 2 | + | # For local development and testing | |||
| 3 | + | ||||
| 4 | + | [server] | |||
| 5 | + | port = 8080 | |||
| 6 | + | registry_port = 5000 | |||
| 7 | + | registry_domain = "localhost:8080" # Use localhost for local development | |||
| 8 | + | runtime = "docker" | |||
| 9 | + | ssl_email = "dev@localhost" | |||
| 10 | + | data_dir = "./dev-data" | |||
| 11 | + | ||||
| 12 | + | [registry_auth] | |||
| 13 | + | enabled = false # Disable auth for easier local development | |||
| 14 | + | ||||
| 15 | + | # Local development routes using .local domains | |||
| 16 | + | [routes] | |||
| 17 | + | "app.local" = "myapp:latest" # Main app - latest build | |||
| 18 | + | "api.local" = "myapi:latest" # API service | |||
| 19 | + | "admin.local" = "admin:latest" # Admin interface | |||
| 20 | + | "docs.local" = "docs:latest" # Documentation | |||
| 21 | + | ||||
| 22 | + | # Test different versions locally | |||
| 23 | + | "app-v1.local" = "myapp:v1.0.0" # Test older version | |||
| 24 | + | "app-v2.local" = "myapp:v2.0.0" # Test newer version | |||
| 25 | + | ||||
| 26 | + | # Third-party services for development | |||
| 27 | + | "db-admin.local" = "phpmyadmin:latest" # Database admin | |||
| 28 | + | "mail.local" = "mailhog/mailhog:latest" # Email testing | |||
| 29 | + | ||||
| 30 | + | # Local development workflow: | |||
| 31 | + | # 1. Add to /etc/hosts: | |||
| 32 | + | # 127.0.0.1 app.local | |||
| 33 | + | # 127.0.0.1 api.local | |||
| 34 | + | # 127.0.0.1 admin.local | |||
| 35 | + | # 127.0.0.1 docs.local | |||
| 36 | + | # 127.0.0.1 localhost | |||
| 37 | + | # | |||
| 38 | + | # 2. Build your app: docker build -t myapp:latest . | |||
| 39 | + | # 3. Push to local registry: | |||
| 40 | + | # docker tag myapp:latest localhost:8080/myapp:latest | |||
| 41 | + | # docker push localhost:8080/myapp:latest | |||
| 42 | + | # 4. Access at http://app.local:8080 | |||
| 43 | + | # | |||
| 44 | + | # No Cloudflare needed - everything runs locally!⏎̸ | |||
A examples/docker-compose.yml +108 -0
| @@ -0,0 +1,108 @@ | |||
| 1 | + | # Gordon v2 - Docker Compose Deployment Example | |
| 2 | + | # Complete containerized deployment with proper networking | |
| 3 | + | ||
| 4 | + | version: '3.8' | |
| 5 | + | ||
| 6 | + | services: | |
| 7 | + | # Main Gordon service | |
| 8 | + | gordon: | |
| 9 | + | image: gordon:latest | |
| 10 | + | container_name: gordon | |
| 11 | + | restart: unless-stopped | |
| 12 | + | ||
| 13 | + | # Port mapping for Cloudflare → Gordon | |
| 14 | + | ports: | |
| 15 | + | - "80:8080" # HTTP traffic (Cloudflare terminates SSL) | |
| 16 | + | - "443:8080" # HTTPS traffic (Cloudflare → HTTP to Gordon) | |
| 17 | + | ||
| 18 | + | volumes: | |
| 19 | + | # Docker socket access for container management | |
| 20 | + | - /var/run/docker.sock:/var/run/docker.sock | |
| 21 | + | ||
| 22 | + | # Data persistence | |
| 23 | + | - ./gordon-data:/data | |
| 24 | + | ||
| 25 | + | # Configuration | |
| 26 | + | - ./gordon.toml:/app/gordon.toml:ro | |
| 27 | + | ||
| 28 | + | # Optional: Mount Docker registry data | |
| 29 | + | - ./registry-data:/data/registry | |
| 30 | + | ||
| 31 | + | environment: | |
| 32 | + | - GORDON_LOG_LEVEL=info | |
| 33 | + | - GORDON_CONFIG=/app/gordon.toml | |
| 34 | + | ||
| 35 | + | # Health check | |
| 36 | + | healthcheck: | |
| 37 | + | test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:8080/health"] | |
| 38 | + | interval: 30s | |
| 39 | + | timeout: 10s | |
| 40 | + | retries: 3 | |
| 41 | + | start_period: 40s | |
| 42 | + | ||
| 43 | + | networks: | |
| 44 | + | - gordon-network | |
| 45 | + | ||
| 46 | + | # Security | |
| 47 | + | user: "1000:1000" # Run as non-root user | |
| 48 | + | ||
| 49 | + | # Resource limits (optional) | |
| 50 | + | deploy: | |
| 51 | + | resources: | |
| 52 | + | limits: | |
| 53 | + | memory: 512M | |
| 54 | + | cpus: '0.5' | |
| 55 | + | reservations: | |
| 56 | + | memory: 256M | |
| 57 | + | cpus: '0.25' | |
| 58 | + | ||
| 59 | + | # Optional: Monitoring with Prometheus | |
| 60 | + | prometheus: | |
| 61 | + | image: prom/prometheus:latest | |
| 62 | + | container_name: gordon-prometheus | |
| 63 | + | restart: unless-stopped | |
| 64 | + | ports: | |
| 65 | + | - "9090:9090" | |
| 66 | + | volumes: | |
| 67 | + | - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro | |
| 68 | + | - prometheus-data:/prometheus | |
| 69 | + | command: | |
| 70 | + | - '--config.file=/etc/prometheus/prometheus.yml' | |
| 71 | + | - '--storage.tsdb.path=/prometheus' | |
| 72 | + | - '--web.console.libraries=/etc/prometheus/console_libraries' | |
| 73 | + | - '--web.console.templates=/etc/prometheus/consoles' | |
| 74 | + | - '--web.enable-lifecycle' | |
| 75 | + | networks: | |
| 76 | + | - gordon-network | |
| 77 | + | ||
| 78 | + | # Optional: Grafana dashboard | |
| 79 | + | grafana: | |
| 80 | + | image: grafana/grafana:latest | |
| 81 | + | container_name: gordon-grafana | |
| 82 | + | restart: unless-stopped | |
| 83 | + | ports: | |
| 84 | + | - "3000:3000" | |
| 85 | + | volumes: | |
| 86 | + | - grafana-data:/var/lib/grafana | |
| 87 | + | environment: | |
| 88 | + | - GF_SECURITY_ADMIN_PASSWORD=admin123 | |
| 89 | + | networks: | |
| 90 | + | - gordon-network | |
| 91 | + | ||
| 92 | + | networks: | |
| 93 | + | gordon-network: | |
| 94 | + | driver: bridge | |
| 95 | + | ipam: | |
| 96 | + | config: | |
| 97 | + | - subnet: 172.20.0.0/16 | |
| 98 | + | ||
| 99 | + | volumes: | |
| 100 | + | prometheus-data: | |
| 101 | + | grafana-data: | |
| 102 | + | ||
| 103 | + | # Usage: | |
| 104 | + | # 1. Create gordon.toml with your configuration | |
| 105 | + | # 2. Run: docker-compose up -d | |
| 106 | + | # 3. Check health: docker-compose ps | |
| 107 | + | # 4. View logs: docker-compose logs -f gordon | |
| 108 | + | # 5. Scale if needed: docker-compose up -d --scale gordon=2⏎̸ | |
| @@ -0,0 +1,108 @@ | |||||
| 1 | + | # Gordon v2 - Docker Compose Deployment Example | |||
| 2 | + | # Complete containerized deployment with proper networking | |||
| 3 | + | ||||
| 4 | + | version: '3.8' | |||
| 5 | + | ||||
| 6 | + | services: | |||
| 7 | + | # Main Gordon service | |||
| 8 | + | gordon: | |||
| 9 | + | image: gordon:latest | |||
| 10 | + | container_name: gordon | |||
| 11 | + | restart: unless-stopped | |||
| 12 | + | ||||
| 13 | + | # Port mapping for Cloudflare → Gordon | |||
| 14 | + | ports: | |||
| 15 | + | - "80:8080" # HTTP traffic (Cloudflare terminates SSL) | |||
| 16 | + | - "443:8080" # HTTPS traffic (Cloudflare → HTTP to Gordon) | |||
| 17 | + | ||||
| 18 | + | volumes: | |||
| 19 | + | # Docker socket access for container management | |||
| 20 | + | - /var/run/docker.sock:/var/run/docker.sock | |||
| 21 | + | ||||
| 22 | + | # Data persistence | |||
| 23 | + | - ./gordon-data:/data | |||
| 24 | + | ||||
| 25 | + | # Configuration | |||
| 26 | + | - ./gordon.toml:/app/gordon.toml:ro | |||
| 27 | + | ||||
| 28 | + | # Optional: Mount Docker registry data | |||
| 29 | + | - ./registry-data:/data/registry | |||
| 30 | + | ||||
| 31 | + | environment: | |||
| 32 | + | - GORDON_LOG_LEVEL=info | |||
| 33 | + | - GORDON_CONFIG=/app/gordon.toml | |||
| 34 | + | ||||
| 35 | + | # Health check | |||
| 36 | + | healthcheck: | |||
| 37 | + | test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:8080/health"] | |||
| 38 | + | interval: 30s | |||
| 39 | + | timeout: 10s | |||
| 40 | + | retries: 3 | |||
| 41 | + | start_period: 40s | |||
| 42 | + | ||||
| 43 | + | networks: | |||
| 44 | + | - gordon-network | |||
| 45 | + | ||||
| 46 | + | # Security | |||
| 47 | + | user: "1000:1000" # Run as non-root user | |||
| 48 | + | ||||
| 49 | + | # Resource limits (optional) | |||
| 50 | + | deploy: | |||
| 51 | + | resources: | |||
| 52 | + | limits: | |||
| 53 | + | memory: 512M | |||
| 54 | + | cpus: '0.5' | |||
| 55 | + | reservations: | |||
| 56 | + | memory: 256M | |||
| 57 | + | cpus: '0.25' | |||
| 58 | + | ||||
| 59 | + | # Optional: Monitoring with Prometheus | |||
| 60 | + | prometheus: | |||
| 61 | + | image: prom/prometheus:latest | |||
| 62 | + | container_name: gordon-prometheus | |||
| 63 | + | restart: unless-stopped | |||
| 64 | + | ports: | |||
| 65 | + | - "9090:9090" | |||
| 66 | + | volumes: | |||
| 67 | + | - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro | |||
| 68 | + | - prometheus-data:/prometheus | |||
| 69 | + | command: | |||
| 70 | + | - '--config.file=/etc/prometheus/prometheus.yml' | |||
| 71 | + | - '--storage.tsdb.path=/prometheus' | |||
| 72 | + | - '--web.console.libraries=/etc/prometheus/console_libraries' | |||
| 73 | + | - '--web.console.templates=/etc/prometheus/consoles' | |||
| 74 | + | - '--web.enable-lifecycle' | |||
| 75 | + | networks: | |||
| 76 | + | - gordon-network | |||
| 77 | + | ||||
| 78 | + | # Optional: Grafana dashboard | |||
| 79 | + | grafana: | |||
| 80 | + | image: grafana/grafana:latest | |||
| 81 | + | container_name: gordon-grafana | |||
| 82 | + | restart: unless-stopped | |||
| 83 | + | ports: | |||
| 84 | + | - "3000:3000" | |||
| 85 | + | volumes: | |||
| 86 | + | - grafana-data:/var/lib/grafana | |||
| 87 | + | environment: | |||
| 88 | + | - GF_SECURITY_ADMIN_PASSWORD=admin123 | |||
| 89 | + | networks: | |||
| 90 | + | - gordon-network | |||
| 91 | + | ||||
| 92 | + | networks: | |||
| 93 | + | gordon-network: | |||
| 94 | + | driver: bridge | |||
| 95 | + | ipam: | |||
| 96 | + | config: | |||
| 97 | + | - subnet: 172.20.0.0/16 | |||
| 98 | + | ||||
| 99 | + | volumes: | |||
| 100 | + | prometheus-data: | |||
| 101 | + | grafana-data: | |||
| 102 | + | ||||
| 103 | + | # Usage: | |||
| 104 | + | # 1. Create gordon.toml with your configuration | |||
| 105 | + | # 2. Run: docker-compose up -d | |||
| 106 | + | # 3. Check health: docker-compose ps | |||
| 107 | + | # 4. View logs: docker-compose logs -f gordon | |||
| 108 | + | # 5. Scale if needed: docker-compose up -d --scale gordon=2⏎̸ | |||
A examples/minimal.toml +22 -0
| @@ -0,0 +1,22 @@ | |||
| 1 | + | # Gordon v2 - Minimal Configuration Example | |
| 2 | + | # Simplest possible setup to get started | |
| 3 | + | ||
| 4 | + | [server] | |
| 5 | + | runtime = "docker" | |
| 6 | + | ssl_email = "you@example.com" | |
| 7 | + | ||
| 8 | + | # Just one route - perfect for getting started | |
| 9 | + | [routes] | |
| 10 | + | "example.com" = "nginx:latest" | |
| 11 | + | ||
| 12 | + | # That's it! Gordon will use these defaults: | |
| 13 | + | # - port = 8080 (main proxy) | |
| 14 | + | # - registry_port = 5000 (internal registry) | |
| 15 | + | # - registry_auth.enabled = false (no auth) | |
| 16 | + | # - data_dir = "./data" (local storage) | |
| 17 | + | ||
| 18 | + | # Setup: | |
| 19 | + | # 1. Point example.com DNS to your server | |
| 20 | + | # 2. docker tag nginx:latest localhost:5000/nginx:latest | |
| 21 | + | # 3. docker push localhost:5000/nginx:latest | |
| 22 | + | # 4. Visit https://example.com (via Cloudflare) → nginx container!⏎̸ | |
| @@ -0,0 +1,22 @@ | |||||
| 1 | + | # Gordon v2 - Minimal Configuration Example | |||
| 2 | + | # Simplest possible setup to get started | |||
| 3 | + | ||||
| 4 | + | [server] | |||
| 5 | + | runtime = "docker" | |||
| 6 | + | ssl_email = "you@example.com" | |||
| 7 | + | ||||
| 8 | + | # Just one route - perfect for getting started | |||
| 9 | + | [routes] | |||
| 10 | + | "example.com" = "nginx:latest" | |||
| 11 | + | ||||
| 12 | + | # That's it! Gordon will use these defaults: | |||
| 13 | + | # - port = 8080 (main proxy) | |||
| 14 | + | # - registry_port = 5000 (internal registry) | |||
| 15 | + | # - registry_auth.enabled = false (no auth) | |||
| 16 | + | # - data_dir = "./data" (local storage) | |||
| 17 | + | ||||
| 18 | + | # Setup: | |||
| 19 | + | # 1. Point example.com DNS to your server | |||
| 20 | + | # 2. docker tag nginx:latest localhost:5000/nginx:latest | |||
| 21 | + | # 3. docker push localhost:5000/nginx:latest | |||
| 22 | + | # 4. Visit https://example.com (via Cloudflare) → nginx container!⏎̸ | |||
A examples/production.toml +35 -0
| @@ -0,0 +1,35 @@ | |||
| 1 | + | # Gordon v2 - Production Configuration Example | |
| 2 | + | # Optimized for production environments with multiple services | |
| 3 | + | ||
| 4 | + | [server] | |
| 5 | + | port = 8080 | |
| 6 | + | registry_port = 5000 | |
| 7 | + | registry_domain = "registry.company.com" | |
| 8 | + | runtime = "docker" | |
| 9 | + | ssl_email = "devops@company.com" | |
| 10 | + | data_dir = "/var/lib/gordon" | |
| 11 | + | ||
| 12 | + | [registry_auth] | |
| 13 | + | enabled = true | |
| 14 | + | username = "deploy-bot" | |
| 15 | + | password = "CHANGE-THIS-IN-PRODUCTION" # Use environment variables in production! | |
| 16 | + | ||
| 17 | + | # Production service routes | |
| 18 | + | [routes] | |
| 19 | + | "app.company.com" = "company-app:v2.1.0" # Main app with pinned version | |
| 20 | + | "api.company.com" = "company-api:v1.5.2" # API service | |
| 21 | + | "admin.company.com" = "admin-panel:v1.0.1" # Admin interface | |
| 22 | + | "docs.company.com" = "company-docs:latest" # Documentation site | |
| 23 | + | "status.company.com" = "status-page:v0.3.0" # Status page | |
| 24 | + | ||
| 25 | + | # Monitoring and internal tools | |
| 26 | + | "grafana.company.com" = "grafana/grafana:latest" # Metrics dashboard | |
| 27 | + | "prometheus.company.com" = "prom/prometheus:latest" # Metrics collection | |
| 28 | + | ||
| 29 | + | # Production deployment workflow: | |
| 30 | + | # 1. Build and tag: docker build -t company-app:v2.1.1 . | |
| 31 | + | # 2. Tag for registry: docker tag company-app:v2.1.1 registry.company.com/company-app:v2.1.1 | |
| 32 | + | # 3. Update this config: "app.company.com" = "company-app:v2.1.1" | |
| 33 | + | # 4. Restart Gordon to reload config | |
| 34 | + | # 5. Push to deploy: docker push registry.company.com/company-app:v2.1.1 | |
| 35 | + | # 6. Gordon automatically deploys to app.company.com!⏎̸ | |
| @@ -0,0 +1,35 @@ | |||||
| 1 | + | # Gordon v2 - Production Configuration Example | |||
| 2 | + | # Optimized for production environments with multiple services | |||
| 3 | + | ||||
| 4 | + | [server] | |||
| 5 | + | port = 8080 | |||
| 6 | + | registry_port = 5000 | |||
| 7 | + | registry_domain = "registry.company.com" | |||
| 8 | + | runtime = "docker" | |||
| 9 | + | ssl_email = "devops@company.com" | |||
| 10 | + | data_dir = "/var/lib/gordon" | |||
| 11 | + | ||||
| 12 | + | [registry_auth] | |||
| 13 | + | enabled = true | |||
| 14 | + | username = "deploy-bot" | |||
| 15 | + | password = "CHANGE-THIS-IN-PRODUCTION" # Use environment variables in production! | |||
| 16 | + | ||||
| 17 | + | # Production service routes | |||
| 18 | + | [routes] | |||
| 19 | + | "app.company.com" = "company-app:v2.1.0" # Main app with pinned version | |||
| 20 | + | "api.company.com" = "company-api:v1.5.2" # API service | |||
| 21 | + | "admin.company.com" = "admin-panel:v1.0.1" # Admin interface | |||
| 22 | + | "docs.company.com" = "company-docs:latest" # Documentation site | |||
| 23 | + | "status.company.com" = "status-page:v0.3.0" # Status page | |||
| 24 | + | ||||
| 25 | + | # Monitoring and internal tools | |||
| 26 | + | "grafana.company.com" = "grafana/grafana:latest" # Metrics dashboard | |||
| 27 | + | "prometheus.company.com" = "prom/prometheus:latest" # Metrics collection | |||
| 28 | + | ||||
| 29 | + | # Production deployment workflow: | |||
| 30 | + | # 1. Build and tag: docker build -t company-app:v2.1.1 . | |||
| 31 | + | # 2. Tag for registry: docker tag company-app:v2.1.1 registry.company.com/company-app:v2.1.1 | |||
| 32 | + | # 3. Update this config: "app.company.com" = "company-app:v2.1.1" | |||
| 33 | + | # 4. Restart Gordon to reload config | |||
| 34 | + | # 5. Push to deploy: docker push registry.company.com/company-app:v2.1.1 | |||
| 35 | + | # 6. Gordon automatically deploys to app.company.com!⏎̸ | |||
A examples/saas-multi-tenant.toml +51 -0
| @@ -0,0 +1,51 @@ | |||
| 1 | + | # Gordon v2 - Multi-Tenant SaaS Configuration Example | |
| 2 | + | # For SaaS platforms with customer subdomains | |
| 3 | + | ||
| 4 | + | [server] | |
| 5 | + | port = 8080 | |
| 6 | + | registry_port = 5000 | |
| 7 | + | registry_domain = "registry.saas-platform.com" | |
| 8 | + | runtime = "docker" | |
| 9 | + | ssl_email = "ops@saas-platform.com" | |
| 10 | + | data_dir = "/var/lib/gordon" | |
| 11 | + | ||
| 12 | + | [registry_auth] | |
| 13 | + | enabled = true | |
| 14 | + | username = "deploy" | |
| 15 | + | password = "secure-deployment-key" | |
| 16 | + | ||
| 17 | + | # Multi-tenant customer routes | |
| 18 | + | [routes] | |
| 19 | + | # Main platform services | |
| 20 | + | "app.saas-platform.com" = "saas-frontend:v2.1.0" # Marketing site | |
| 21 | + | "admin.saas-platform.com" = "saas-admin:v1.5.0" # Admin dashboard | |
| 22 | + | "api.saas-platform.com" = "saas-api:v3.2.1" # Main API | |
| 23 | + | ||
| 24 | + | # Customer tenant subdomains | |
| 25 | + | "acme-corp.saas-platform.com" = "saas-app:v2.1.0" # Customer: Acme Corp | |
| 26 | + | "beta-inc.saas-platform.com" = "saas-app:v2.1.0" # Customer: Beta Inc | |
| 27 | + | "gamma-llc.saas-platform.com" = "saas-app:v2.1.0" # Customer: Gamma LLC | |
| 28 | + | ||
| 29 | + | # Enterprise customers with custom domains | |
| 30 | + | "portal.acme-corp.com" = "saas-app:v2.1.0" # Acme's custom domain | |
| 31 | + | "app.beta-inc.com" = "saas-app:v2.1.0" # Beta's custom domain | |
| 32 | + | ||
| 33 | + | # Staging/testing tenants | |
| 34 | + | "demo.saas-platform.com" = "saas-app:latest" # Demo environment | |
| 35 | + | "staging-tenant.saas-platform.com" = "saas-app:staging" # Staging tests | |
| 36 | + | ||
| 37 | + | # Support tools | |
| 38 | + | "status.saas-platform.com" = "status-page:v1.0.0" # Status page | |
| 39 | + | "docs.saas-platform.com" = "documentation:v2.0.0" # Customer docs | |
| 40 | + | ||
| 41 | + | # Multi-tenant deployment strategy: | |
| 42 | + | # 1. Single application image serves all tenants | |
| 43 | + | # 2. Tenant identification via subdomain/domain | |
| 44 | + | # 3. Application handles tenant routing internally | |
| 45 | + | # 4. Each tenant gets isolated data/configuration | |
| 46 | + | # 5. Rolling updates deploy to all tenants simultaneously | |
| 47 | + | # | |
| 48 | + | # Scaling strategy: | |
| 49 | + | # - Start with shared containers (same image, tenant routing) | |
| 50 | + | # - Move to dedicated containers per tenant if needed | |
| 51 | + | # - Use Gordon's event system for tenant provisioning automation⏎̸ | |
| @@ -0,0 +1,51 @@ | |||||
| 1 | + | # Gordon v2 - Multi-Tenant SaaS Configuration Example | |||
| 2 | + | # For SaaS platforms with customer subdomains | |||
| 3 | + | ||||
| 4 | + | [server] | |||
| 5 | + | port = 8080 | |||
| 6 | + | registry_port = 5000 | |||
| 7 | + | registry_domain = "registry.saas-platform.com" | |||
| 8 | + | runtime = "docker" | |||
| 9 | + | ssl_email = "ops@saas-platform.com" | |||
| 10 | + | data_dir = "/var/lib/gordon" | |||
| 11 | + | ||||
| 12 | + | [registry_auth] | |||
| 13 | + | enabled = true | |||
| 14 | + | username = "deploy" | |||
| 15 | + | password = "secure-deployment-key" | |||
| 16 | + | ||||
| 17 | + | # Multi-tenant customer routes | |||
| 18 | + | [routes] | |||
| 19 | + | # Main platform services | |||
| 20 | + | "app.saas-platform.com" = "saas-frontend:v2.1.0" # Marketing site | |||
| 21 | + | "admin.saas-platform.com" = "saas-admin:v1.5.0" # Admin dashboard | |||
| 22 | + | "api.saas-platform.com" = "saas-api:v3.2.1" # Main API | |||
| 23 | + | ||||
| 24 | + | # Customer tenant subdomains | |||
| 25 | + | "acme-corp.saas-platform.com" = "saas-app:v2.1.0" # Customer: Acme Corp | |||
| 26 | + | "beta-inc.saas-platform.com" = "saas-app:v2.1.0" # Customer: Beta Inc | |||
| 27 | + | "gamma-llc.saas-platform.com" = "saas-app:v2.1.0" # Customer: Gamma LLC | |||
| 28 | + | ||||
| 29 | + | # Enterprise customers with custom domains | |||
| 30 | + | "portal.acme-corp.com" = "saas-app:v2.1.0" # Acme's custom domain | |||
| 31 | + | "app.beta-inc.com" = "saas-app:v2.1.0" # Beta's custom domain | |||
| 32 | + | ||||
| 33 | + | # Staging/testing tenants | |||
| 34 | + | "demo.saas-platform.com" = "saas-app:latest" # Demo environment | |||
| 35 | + | "staging-tenant.saas-platform.com" = "saas-app:staging" # Staging tests | |||
| 36 | + | ||||
| 37 | + | # Support tools | |||
| 38 | + | "status.saas-platform.com" = "status-page:v1.0.0" # Status page | |||
| 39 | + | "docs.saas-platform.com" = "documentation:v2.0.0" # Customer docs | |||
| 40 | + | ||||
| 41 | + | # Multi-tenant deployment strategy: | |||
| 42 | + | # 1. Single application image serves all tenants | |||
| 43 | + | # 2. Tenant identification via subdomain/domain | |||
| 44 | + | # 3. Application handles tenant routing internally | |||
| 45 | + | # 4. Each tenant gets isolated data/configuration | |||
| 46 | + | # 5. Rolling updates deploy to all tenants simultaneously | |||
| 47 | + | # | |||
| 48 | + | # Scaling strategy: | |||
| 49 | + | # - Start with shared containers (same image, tenant routing) | |||
| 50 | + | # - Move to dedicated containers per tenant if needed | |||
| 51 | + | # - Use Gordon's event system for tenant provisioning automation⏎̸ | |||
A examples/staging.toml +40 -0
| @@ -0,0 +1,40 @@ | |||
| 1 | + | # Gordon v2 - Staging Configuration Example | |
| 2 | + | # For testing and preview environments | |
| 3 | + | ||
| 4 | + | [server] | |
| 5 | + | port = 8080 | |
| 6 | + | registry_port = 5000 | |
| 7 | + | registry_domain = "staging-registry.company.com" | |
| 8 | + | runtime = "docker" | |
| 9 | + | ssl_email = "devops@company.com" | |
| 10 | + | data_dir = "./data" | |
| 11 | + | ||
| 12 | + | [registry_auth] | |
| 13 | + | enabled = true | |
| 14 | + | username = "staging-user" | |
| 15 | + | password = "staging-pass-123" | |
| 16 | + | ||
| 17 | + | # Staging environment routes - using branch-based deployments | |
| 18 | + | [routes] | |
| 19 | + | "staging.company.com" = "company-app:staging" # Main staging branch | |
| 20 | + | "staging-api.company.com" = "company-api:staging" # API staging | |
| 21 | + | ||
| 22 | + | # Feature branch testing | |
| 23 | + | "feature-auth.company.com" = "company-app:feature-auth" # Feature branch: auth improvements | |
| 24 | + | "feature-ui.company.com" = "company-app:feature-ui-redesign" # Feature branch: UI updates | |
| 25 | + | "hotfix-123.company.com" = "company-app:hotfix-user-login" # Hotfix branch | |
| 26 | + | ||
| 27 | + | # PR preview environments | |
| 28 | + | "pr-42.company.com" = "company-app:pr-42" # Pull request #42 preview | |
| 29 | + | "pr-55.company.com" = "company-app:pr-55" # Pull request #55 preview | |
| 30 | + | ||
| 31 | + | # Development tools | |
| 32 | + | "staging-admin.company.com" = "admin-panel:develop" # Admin panel dev version | |
| 33 | + | ||
| 34 | + | # Staging workflow: | |
| 35 | + | # 1. Developer pushes feature branch | |
| 36 | + | # 2. CI builds and tags: company-app:feature-auth | |
| 37 | + | # 3. CI pushes: docker push staging-registry.company.com/company-app:feature-auth | |
| 38 | + | # 4. Gordon auto-deploys to feature-auth.company.com | |
| 39 | + | # 5. Team tests at https://feature-auth.company.com | |
| 40 | + | # 6. Merge to staging → auto-deploy to staging.company.com⏎̸ | |
| @@ -0,0 +1,40 @@ | |||||
| 1 | + | # Gordon v2 - Staging Configuration Example | |||
| 2 | + | # For testing and preview environments | |||
| 3 | + | ||||
| 4 | + | [server] | |||
| 5 | + | port = 8080 | |||
| 6 | + | registry_port = 5000 | |||
| 7 | + | registry_domain = "staging-registry.company.com" | |||
| 8 | + | runtime = "docker" | |||
| 9 | + | ssl_email = "devops@company.com" | |||
| 10 | + | data_dir = "./data" | |||
| 11 | + | ||||
| 12 | + | [registry_auth] | |||
| 13 | + | enabled = true | |||
| 14 | + | username = "staging-user" | |||
| 15 | + | password = "staging-pass-123" | |||
| 16 | + | ||||
| 17 | + | # Staging environment routes - using branch-based deployments | |||
| 18 | + | [routes] | |||
| 19 | + | "staging.company.com" = "company-app:staging" # Main staging branch | |||
| 20 | + | "staging-api.company.com" = "company-api:staging" # API staging | |||
| 21 | + | ||||
| 22 | + | # Feature branch testing | |||
| 23 | + | "feature-auth.company.com" = "company-app:feature-auth" # Feature branch: auth improvements | |||
| 24 | + | "feature-ui.company.com" = "company-app:feature-ui-redesign" # Feature branch: UI updates | |||
| 25 | + | "hotfix-123.company.com" = "company-app:hotfix-user-login" # Hotfix branch | |||
| 26 | + | ||||
| 27 | + | # PR preview environments | |||
| 28 | + | "pr-42.company.com" = "company-app:pr-42" # Pull request #42 preview | |||
| 29 | + | "pr-55.company.com" = "company-app:pr-55" # Pull request #55 preview | |||
| 30 | + | ||||
| 31 | + | # Development tools | |||
| 32 | + | "staging-admin.company.com" = "admin-panel:develop" # Admin panel dev version | |||
| 33 | + | ||||
| 34 | + | # Staging workflow: | |||
| 35 | + | # 1. Developer pushes feature branch | |||
| 36 | + | # 2. CI builds and tags: company-app:feature-auth | |||
| 37 | + | # 3. CI pushes: docker push staging-registry.company.com/company-app:feature-auth | |||
| 38 | + | # 4. Gordon auto-deploys to feature-auth.company.com | |||
| 39 | + | # 5. Team tests at https://feature-auth.company.com | |||
| 40 | + | # 6. Merge to staging → auto-deploy to staging.company.com⏎̸ | |||
M go.mod +2 -5
| @@ -9,9 +9,6 @@ require ( | |||
| 9 | 9 | github.com/spf13/cobra v1.9.1 | |
| 10 | 10 | github.com/spf13/viper v1.20.1 | |
| 11 | 11 | ) | |
| 12 | - | ||
| 13 | - | require github.com/google/uuid v1.6.0 // indirect | |
| 14 | - | ||
| 15 | 12 | require ( | |
| 16 | 13 | github.com/Microsoft/go-winio v0.4.14 // indirect | |
| 17 | 14 | github.com/containerd/errdefs v1.0.0 // indirect | |
| @@ -20,12 +17,12 @@ require ( | |||
| 20 | 17 | github.com/distribution/reference v0.6.0 // indirect | |
| 21 | 18 | github.com/docker/go-units v0.5.0 // indirect | |
| 22 | 19 | github.com/felixge/httpsnoop v1.0.4 // indirect | |
| 23 | - | github.com/fsnotify/fsnotify v1.8.0 // indirect | |
| 20 | + | github.com/fsnotify/fsnotify v1.8.0 // direct | |
| 24 | 21 | github.com/go-logr/logr v1.4.2 // indirect | |
| 25 | 22 | github.com/go-logr/stdr v1.2.2 // indirect | |
| 26 | 23 | github.com/go-viper/mapstructure/v2 v2.2.1 // indirect | |
| 27 | 24 | github.com/gogo/protobuf v1.3.2 // indirect | |
| 28 | - | github.com/golang-jwt/jwt/v5 v5.2.2 // direct | |
| 25 | + | github.com/google/uuid v1.6.0 // direct | |
| 29 | 26 | github.com/inconshreveable/mousetrap v1.1.0 // indirect | |
| 30 | 27 | github.com/mattn/go-colorable v0.1.13 // indirect | |
| 31 | 28 | github.com/mattn/go-isatty v0.0.19 // indirect | |
| @@ -9,9 +9,6 @@ require ( | |||||
| 9 | github.com/spf13/cobra v1.9.1 | 9 | github.com/spf13/cobra v1.9.1 | ||
| 10 | github.com/spf13/viper v1.20.1 | 10 | github.com/spf13/viper v1.20.1 | ||
| 11 | ) | 11 | ) | ||
| 12 | - | ||||
| 13 | - | require github.com/google/uuid v1.6.0 // indirect | |||
| 14 | - | ||||
| 15 | require ( | 12 | require ( | ||
| 16 | github.com/Microsoft/go-winio v0.4.14 // indirect | 13 | github.com/Microsoft/go-winio v0.4.14 // indirect | ||
| 17 | github.com/containerd/errdefs v1.0.0 // indirect | 14 | github.com/containerd/errdefs v1.0.0 // indirect | ||
| @@ -20,12 +17,12 @@ require ( | |||||
| 20 | github.com/distribution/reference v0.6.0 // indirect | 17 | github.com/distribution/reference v0.6.0 // indirect | ||
| 21 | github.com/docker/go-units v0.5.0 // indirect | 18 | github.com/docker/go-units v0.5.0 // indirect | ||
| 22 | github.com/felixge/httpsnoop v1.0.4 // indirect | 19 | github.com/felixge/httpsnoop v1.0.4 // indirect | ||
| 23 | - | github.com/fsnotify/fsnotify v1.8.0 // indirect | 20 | + | github.com/fsnotify/fsnotify v1.8.0 // direct |
| 24 | github.com/go-logr/logr v1.4.2 // indirect | 21 | github.com/go-logr/logr v1.4.2 // indirect | ||
| 25 | github.com/go-logr/stdr v1.2.2 // indirect | 22 | github.com/go-logr/stdr v1.2.2 // indirect | ||
| 26 | github.com/go-viper/mapstructure/v2 v2.2.1 // indirect | 23 | github.com/go-viper/mapstructure/v2 v2.2.1 // indirect | ||
| 27 | github.com/gogo/protobuf v1.3.2 // indirect | 24 | github.com/gogo/protobuf v1.3.2 // indirect | ||
| 28 | - | github.com/golang-jwt/jwt/v5 v5.2.2 // direct | 25 | + | github.com/google/uuid v1.6.0 // direct |
| 29 | github.com/inconshreveable/mousetrap v1.1.0 // indirect | 26 | github.com/inconshreveable/mousetrap v1.1.0 // indirect | ||
| 30 | github.com/mattn/go-colorable v0.1.13 // indirect | 27 | github.com/mattn/go-colorable v0.1.13 // indirect | ||
| 31 | github.com/mattn/go-isatty v0.0.19 // indirect | 28 | github.com/mattn/go-isatty v0.0.19 // indirect | ||
M gordon.hcl +31 -12
| @@ -1,19 +1,38 @@ | |||
| 1 | + | # Gordon v2 Configuration (HCL format) | |
| 2 | + | # Complete example with registry domain routing via Cloudflare | |
| 3 | + | ||
| 1 | 4 | server { | |
| 2 | - | port = 8080 | |
| 3 | - | registry_port = 5000 | |
| 4 | - | runtime = "docker" | |
| 5 | - | ssl_email = "admin@example.com" | |
| 6 | - | data_dir = "./data" | |
| 5 | + | port = 8080 # Main proxy port | |
| 6 | + | registry_port = 5000 # Internal registry port | |
| 7 | + | registry_domain = "registry.example.com" # Registry subdomain (NEW!) | |
| 8 | + | runtime = "docker" # Container runtime | |
| 9 | + | ssl_email = "admin@example.com" # Your email for future SSL features | |
| 10 | + | data_dir = "./data" # Data storage directory | |
| 7 | 11 | } | |
| 8 | 12 | ||
| 9 | 13 | registry_auth { | |
| 10 | - | enabled = true | |
| 11 | - | username = "gordon" | |
| 12 | - | password = "secret123" | |
| 14 | + | enabled = true # Enable registry authentication | |
| 15 | + | username = "admin" # Registry username | |
| 16 | + | password = "your-secure-password-123" # Registry password (change this!) | |
| 13 | 17 | } | |
| 14 | 18 | ||
| 19 | + | # Domain to container image mapping | |
| 20 | + | # When you push an image matching these routes, Gordon auto-deploys | |
| 15 | 21 | routes = { | |
| 16 | - | "example.com" = "nginx:latest" | |
| 17 | - | "api.example.com" = "myapi:latest" | |
| 18 | - | "http://dev.example.com" = "myblog:latest" | |
| 19 | - | }⏎̸ | |
| 22 | + | "app.example.com" = "myapp:latest" # Main application | |
| 23 | + | "api.example.com" = "myapi:v2.1.0" # API service with version | |
| 24 | + | "blog.example.com" = "wordpress:latest" # Blog/CMS | |
| 25 | + | "staging.example.com" = "myapp:staging" # Staging environment | |
| 26 | + | } | |
| 27 | + | ||
| 28 | + | # Cloudflare DNS Setup: | |
| 29 | + | # A app.example.com → YOUR_SERVER_IP | |
| 30 | + | # A api.example.com → YOUR_SERVER_IP | |
| 31 | + | # A blog.example.com → YOUR_SERVER_IP | |
| 32 | + | # A staging.example.com → YOUR_SERVER_IP | |
| 33 | + | # A registry.example.com → YOUR_SERVER_IP | |
| 34 | + | ||
| 35 | + | # Docker workflow: | |
| 36 | + | # docker tag myapp:latest registry.example.com/myapp:latest | |
| 37 | + | # docker push registry.example.com/myapp:latest | |
| 38 | + | # → Gordon automatically deploys to app.example.com! 🚀⏎̸ | |
| @@ -1,19 +1,38 @@ | |||||
| 1 | + | # Gordon v2 Configuration (HCL format) | |||
| 2 | + | # Complete example with registry domain routing via Cloudflare | |||
| 3 | + | ||||
| 1 | server { | 4 | server { | ||
| 2 | - | port = 8080 | 5 | + | port = 8080 # Main proxy port |
| 3 | - | registry_port = 5000 | 6 | + | registry_port = 5000 # Internal registry port |
| 4 | - | runtime = "docker" | 7 | + | registry_domain = "registry.example.com" # Registry subdomain (NEW!) |
| 5 | - | ssl_email = "admin@example.com" | 8 | + | runtime = "docker" # Container runtime |
| 6 | - | data_dir = "./data" | 9 | + | ssl_email = "admin@example.com" # Your email for future SSL features |
| 10 | + | data_dir = "./data" # Data storage directory | |||
| 7 | } | 11 | } | ||
| 8 | 12 | ||||
| 9 | registry_auth { | 13 | registry_auth { | ||
| 10 | - | enabled = true | 14 | + | enabled = true # Enable registry authentication |
| 11 | - | username = "gordon" | 15 | + | username = "admin" # Registry username |
| 12 | - | password = "secret123" | 16 | + | password = "your-secure-password-123" # Registry password (change this!) |
| 13 | } | 17 | } | ||
| 14 | 18 | ||||
| 19 | + | # Domain to container image mapping | |||
| 20 | + | # When you push an image matching these routes, Gordon auto-deploys | |||
| 15 | routes = { | 21 | routes = { | ||
| 16 | - | "example.com" = "nginx:latest" | 22 | + | "app.example.com" = "myapp:latest" # Main application |
| 17 | - | "api.example.com" = "myapi:latest" | 23 | + | "api.example.com" = "myapi:v2.1.0" # API service with version |
| 18 | - | "http://dev.example.com" = "myblog:latest" | 24 | + | "blog.example.com" = "wordpress:latest" # Blog/CMS |
| 19 | - | }⏎̸ | 25 | + | "staging.example.com" = "myapp:staging" # Staging environment |
| 26 | + | } | |||
| 27 | + | ||||
| 28 | + | # Cloudflare DNS Setup: | |||
| 29 | + | # A app.example.com → YOUR_SERVER_IP | |||
| 30 | + | # A api.example.com → YOUR_SERVER_IP | |||
| 31 | + | # A blog.example.com → YOUR_SERVER_IP | |||
| 32 | + | # A staging.example.com → YOUR_SERVER_IP | |||
| 33 | + | # A registry.example.com → YOUR_SERVER_IP | |||
| 34 | + | ||||
| 35 | + | # Docker workflow: | |||
| 36 | + | # docker tag myapp:latest registry.example.com/myapp:latest | |||
| 37 | + | # docker push registry.example.com/myapp:latest | |||
| 38 | + | # → Gordon automatically deploys to app.example.com! 🚀⏎̸ | |||
A gordon.test.hcl +17 -0
| @@ -0,0 +1,17 @@ | |||
| 1 | + | server { | |
| 2 | + | port = 80 | |
| 3 | + | registry_port = 5000 | |
| 4 | + | runtime = "docker" | |
| 5 | + | ssl_email = "admin@example.com" | |
| 6 | + | data_dir = "./data" | |
| 7 | + | } | |
| 8 | + | ||
| 9 | + | registry_auth { | |
| 10 | + | enabled = true | |
| 11 | + | username = "gordon" | |
| 12 | + | password = "secret123" | |
| 13 | + | } | |
| 14 | + | ||
| 15 | + | routes = { | |
| 16 | + | "test.local.tld" = "ghcr.io/bnema/go-hello-world-http:latest" | |
| 17 | + | }⏎̸ | |
| @@ -0,0 +1,17 @@ | |||||
| 1 | + | server { | |||
| 2 | + | port = 80 | |||
| 3 | + | registry_port = 5000 | |||
| 4 | + | runtime = "docker" | |||
| 5 | + | ssl_email = "admin@example.com" | |||
| 6 | + | data_dir = "./data" | |||
| 7 | + | } | |||
| 8 | + | ||||
| 9 | + | registry_auth { | |||
| 10 | + | enabled = true | |||
| 11 | + | username = "gordon" | |||
| 12 | + | password = "secret123" | |||
| 13 | + | } | |||
| 14 | + | ||||
| 15 | + | routes = { | |||
| 16 | + | "test.local.tld" = "ghcr.io/bnema/go-hello-world-http:latest" | |||
| 17 | + | }⏎̸ | |||
M gordon.toml +30 -11
| @@ -1,16 +1,35 @@ | |||
| 1 | + | # Gordon v2 Configuration (TOML format) | |
| 2 | + | # Complete example with registry domain routing via Cloudflare | |
| 3 | + | ||
| 1 | 4 | [server] | |
| 2 | - | port = 8080 | |
| 3 | - | registry_port = 5000 | |
| 4 | - | runtime = "docker" | |
| 5 | - | ssl_email = "admin@example.com" | |
| 6 | - | data_dir = "./data" | |
| 5 | + | port = 8080 # Main proxy port | |
| 6 | + | registry_port = 5000 # Internal registry port | |
| 7 | + | registry_domain = "registry.example.com" # Registry subdomain (NEW!) | |
| 8 | + | runtime = "docker" # Container runtime | |
| 9 | + | ssl_email = "admin@example.com" # Your email for future SSL features | |
| 10 | + | data_dir = "./data" # Data storage directory | |
| 7 | 11 | ||
| 8 | 12 | [registry_auth] | |
| 9 | - | enabled = true | |
| 10 | - | username = "gordon" | |
| 11 | - | password = "secret123" | |
| 13 | + | enabled = true # Enable registry authentication | |
| 14 | + | username = "admin" # Registry username | |
| 15 | + | password = "your-secure-password-123" # Registry password (change this!) | |
| 12 | 16 | ||
| 17 | + | # Domain to container image mapping | |
| 18 | + | # When you push an image matching these routes, Gordon auto-deploys | |
| 13 | 19 | [routes] | |
| 14 | - | "example.com" = "nginx:latest" | |
| 15 | - | "api.example.com" = "myapi:latest" | |
| 16 | - | "http://dev.example.com" = "myblog:latest"⏎̸ | |
| 20 | + | "app.example.com" = "myapp:latest" # Main application | |
| 21 | + | "api.example.com" = "myapi:v2.1.0" # API service with version | |
| 22 | + | "blog.example.com" = "wordpress:latest" # Blog/CMS | |
| 23 | + | "staging.example.com" = "myapp:staging" # Staging environment | |
| 24 | + | ||
| 25 | + | # Cloudflare DNS Setup: | |
| 26 | + | # A app.example.com → YOUR_SERVER_IP | |
| 27 | + | # A api.example.com → YOUR_SERVER_IP | |
| 28 | + | # A blog.example.com → YOUR_SERVER_IP | |
| 29 | + | # A staging.example.com → YOUR_SERVER_IP | |
| 30 | + | # A registry.example.com → YOUR_SERVER_IP | |
| 31 | + | ||
| 32 | + | # Docker workflow: | |
| 33 | + | # docker tag myapp:latest registry.example.com/myapp:latest | |
| 34 | + | # docker push registry.example.com/myapp:latest | |
| 35 | + | # → Gordon automatically deploys to app.example.com! 🚀⏎̸ | |
| @@ -1,16 +1,35 @@ | |||||
| 1 | + | # Gordon v2 Configuration (TOML format) | |||
| 2 | + | # Complete example with registry domain routing via Cloudflare | |||
| 3 | + | ||||
| 1 | [server] | 4 | [server] | ||
| 2 | - | port = 8080 | 5 | + | port = 8080 # Main proxy port |
| 3 | - | registry_port = 5000 | 6 | + | registry_port = 5000 # Internal registry port |
| 4 | - | runtime = "docker" | 7 | + | registry_domain = "registry.example.com" # Registry subdomain (NEW!) |
| 5 | - | ssl_email = "admin@example.com" | 8 | + | runtime = "docker" # Container runtime |
| 6 | - | data_dir = "./data" | 9 | + | ssl_email = "admin@example.com" # Your email for future SSL features |
| 10 | + | data_dir = "./data" # Data storage directory | |||
| 7 | 11 | ||||
| 8 | [registry_auth] | 12 | [registry_auth] | ||
| 9 | - | enabled = true | 13 | + | enabled = true # Enable registry authentication |
| 10 | - | username = "gordon" | 14 | + | username = "admin" # Registry username |
| 11 | - | password = "secret123" | 15 | + | password = "your-secure-password-123" # Registry password (change this!) |
| 12 | 16 | ||||
| 17 | + | # Domain to container image mapping | |||
| 18 | + | # When you push an image matching these routes, Gordon auto-deploys | |||
| 13 | [routes] | 19 | [routes] | ||
| 14 | - | "example.com" = "nginx:latest" | 20 | + | "app.example.com" = "myapp:latest" # Main application |
| 15 | - | "api.example.com" = "myapi:latest" | 21 | + | "api.example.com" = "myapi:v2.1.0" # API service with version |
| 16 | - | "http://dev.example.com" = "myblog:latest"⏎̸ | 22 | + | "blog.example.com" = "wordpress:latest" # Blog/CMS |
| 23 | + | "staging.example.com" = "myapp:staging" # Staging environment | |||
| 24 | + | ||||
| 25 | + | # Cloudflare DNS Setup: | |||
| 26 | + | # A app.example.com → YOUR_SERVER_IP | |||
| 27 | + | # A api.example.com → YOUR_SERVER_IP | |||
| 28 | + | # A blog.example.com → YOUR_SERVER_IP | |||
| 29 | + | # A staging.example.com → YOUR_SERVER_IP | |||
| 30 | + | # A registry.example.com → YOUR_SERVER_IP | |||
| 31 | + | ||||
| 32 | + | # Docker workflow: | |||
| 33 | + | # docker tag myapp:latest registry.example.com/myapp:latest | |||
| 34 | + | # docker push registry.example.com/myapp:latest | |||
| 35 | + | # → Gordon automatically deploys to app.example.com! 🚀⏎̸ | |||
M gordon.yaml +30 -11
| @@ -1,16 +1,35 @@ | |||
| 1 | + | # Gordon v2 Configuration (YAML format) | |
| 2 | + | # Complete example with registry domain routing via Cloudflare | |
| 3 | + | ||
| 1 | 4 | server: | |
| 2 | - | port: 8080 | |
| 3 | - | registry_port: 5000 | |
| 4 | - | runtime: docker | |
| 5 | - | ssl_email: admin@example.com | |
| 6 | - | data_dir: ./data | |
| 5 | + | port: 8080 # Main proxy port | |
| 6 | + | registry_port: 5000 # Internal registry port | |
| 7 | + | registry_domain: "registry.example.com" # Registry subdomain (NEW!) | |
| 8 | + | runtime: docker # Container runtime | |
| 9 | + | ssl_email: admin@example.com # Your email for future SSL features | |
| 10 | + | data_dir: ./data # Data storage directory | |
| 7 | 11 | ||
| 8 | 12 | registry_auth: | |
| 9 | - | enabled: true | |
| 10 | - | username: gordon | |
| 11 | - | password: secret123 | |
| 13 | + | enabled: true # Enable registry authentication | |
| 14 | + | username: admin # Registry username | |
| 15 | + | password: your-secure-password-123 # Registry password (change this!) | |
| 12 | 16 | ||
| 17 | + | # Domain to container image mapping | |
| 18 | + | # When you push an image matching these routes, Gordon auto-deploys | |
| 13 | 19 | routes: | |
| 14 | - | "example.com": "nginx:latest" | |
| 15 | - | "api.example.com": "myapi:latest" | |
| 16 | - | "http://dev.example.com": "myblog:latest"⏎̸ | |
| 20 | + | "app.example.com": "myapp:latest" # Main application | |
| 21 | + | "api.example.com": "myapi:v2.1.0" # API service with version | |
| 22 | + | "blog.example.com": "wordpress:latest" # Blog/CMS | |
| 23 | + | "staging.example.com": "myapp:staging" # Staging environment | |
| 24 | + | ||
| 25 | + | # Cloudflare DNS Setup: | |
| 26 | + | # A app.example.com → YOUR_SERVER_IP | |
| 27 | + | # A api.example.com → YOUR_SERVER_IP | |
| 28 | + | # A blog.example.com → YOUR_SERVER_IP | |
| 29 | + | # A staging.example.com → YOUR_SERVER_IP | |
| 30 | + | # A registry.example.com → YOUR_SERVER_IP | |
| 31 | + | ||
| 32 | + | # Docker workflow: | |
| 33 | + | # docker tag myapp:latest registry.example.com/myapp:latest | |
| 34 | + | # docker push registry.example.com/myapp:latest | |
| 35 | + | # → Gordon automatically deploys to app.example.com! 🚀⏎̸ | |
| @@ -1,16 +1,35 @@ | |||||
| 1 | + | # Gordon v2 Configuration (YAML format) | |||
| 2 | + | # Complete example with registry domain routing via Cloudflare | |||
| 3 | + | ||||
| 1 | server: | 4 | server: | ||
| 2 | - | port: 8080 | 5 | + | port: 8080 # Main proxy port |
| 3 | - | registry_port: 5000 | 6 | + | registry_port: 5000 # Internal registry port |
| 4 | - | runtime: docker | 7 | + | registry_domain: "registry.example.com" # Registry subdomain (NEW!) |
| 5 | - | ssl_email: admin@example.com | 8 | + | runtime: docker # Container runtime |
| 6 | - | data_dir: ./data | 9 | + | ssl_email: admin@example.com # Your email for future SSL features |
| 10 | + | data_dir: ./data # Data storage directory | |||
| 7 | 11 | ||||
| 8 | registry_auth: | 12 | registry_auth: | ||
| 9 | - | enabled: true | 13 | + | enabled: true # Enable registry authentication |
| 10 | - | username: gordon | 14 | + | username: admin # Registry username |
| 11 | - | password: secret123 | 15 | + | password: your-secure-password-123 # Registry password (change this!) |
| 12 | 16 | ||||
| 17 | + | # Domain to container image mapping | |||
| 18 | + | # When you push an image matching these routes, Gordon auto-deploys | |||
| 13 | routes: | 19 | routes: | ||
| 14 | - | "example.com": "nginx:latest" | 20 | + | "app.example.com": "myapp:latest" # Main application |
| 15 | - | "api.example.com": "myapi:latest" | 21 | + | "api.example.com": "myapi:v2.1.0" # API service with version |
| 16 | - | "http://dev.example.com": "myblog:latest"⏎̸ | 22 | + | "blog.example.com": "wordpress:latest" # Blog/CMS |
| 23 | + | "staging.example.com": "myapp:staging" # Staging environment | |||
| 24 | + | ||||
| 25 | + | # Cloudflare DNS Setup: | |||
| 26 | + | # A app.example.com → YOUR_SERVER_IP | |||
| 27 | + | # A api.example.com → YOUR_SERVER_IP | |||
| 28 | + | # A blog.example.com → YOUR_SERVER_IP | |||
| 29 | + | # A staging.example.com → YOUR_SERVER_IP | |||
| 30 | + | # A registry.example.com → YOUR_SERVER_IP | |||
| 31 | + | ||||
| 32 | + | # Docker workflow: | |||
| 33 | + | # docker tag myapp:latest registry.example.com/myapp:latest | |||
| 34 | + | # docker push registry.example.com/myapp:latest | |||
| 35 | + | # → Gordon automatically deploys to app.example.com! 🚀⏎̸ | |||
M internal/config/config.go +14 -5
| @@ -14,11 +14,12 @@ type Config struct { | |||
| 14 | 14 | } | |
| 15 | 15 | ||
| 16 | 16 | type ServerConfig struct { | |
| 17 | - | Port int `mapstructure:"port"` | |
| 18 | - | RegistryPort int `mapstructure:"registry_port"` | |
| 19 | - | Runtime string `mapstructure:"runtime"` | |
| 20 | - | SSLEmail string `mapstructure:"ssl_email"` | |
| 21 | - | DataDir string `mapstructure:"data_dir"` | |
| 17 | + | Port int `mapstructure:"port"` | |
| 18 | + | RegistryPort int `mapstructure:"registry_port"` | |
| 19 | + | RegistryDomain string `mapstructure:"registry_domain"` | |
| 20 | + | Runtime string `mapstructure:"runtime"` | |
| 21 | + | SSLEmail string `mapstructure:"ssl_email"` | |
| 22 | + | DataDir string `mapstructure:"data_dir"` | |
| 22 | 23 | } | |
| 23 | 24 | ||
| 24 | 25 | type RegistryAuthConfig struct { | |
| @@ -84,6 +85,14 @@ func Load() (*Config, error) { | |||
| 84 | 85 | } | |
| 85 | 86 | } | |
| 86 | 87 | ||
| 88 | + | // Validate registry domain if provided | |
| 89 | + | if cfg.Server.RegistryDomain != "" { | |
| 90 | + | // Basic domain validation - should not contain protocol or paths | |
| 91 | + | if strings.Contains(cfg.Server.RegistryDomain, "://") || strings.Contains(cfg.Server.RegistryDomain, "/") { | |
| 92 | + | return nil, fmt.Errorf("registry_domain should be just the domain name (e.g. 'registry.example.com')") | |
| 93 | + | } | |
| 94 | + | } | |
| 95 | + | ||
| 87 | 96 | return &cfg, nil | |
| 88 | 97 | } | |
| 89 | 98 | ||
| @@ -14,11 +14,12 @@ type Config struct { | |||||
| 14 | } | 14 | } | ||
| 15 | 15 | ||||
| 16 | type ServerConfig struct { | 16 | type ServerConfig struct { | ||
| 17 | - | Port int `mapstructure:"port"` | 17 | + | Port int `mapstructure:"port"` |
| 18 | - | RegistryPort int `mapstructure:"registry_port"` | 18 | + | RegistryPort int `mapstructure:"registry_port"` |
| 19 | - | Runtime string `mapstructure:"runtime"` | 19 | + | RegistryDomain string `mapstructure:"registry_domain"` |
| 20 | - | SSLEmail string `mapstructure:"ssl_email"` | 20 | + | Runtime string `mapstructure:"runtime"` |
| 21 | - | DataDir string `mapstructure:"data_dir"` | 21 | + | SSLEmail string `mapstructure:"ssl_email"` |
| 22 | + | DataDir string `mapstructure:"data_dir"` | |||
| 22 | } | 23 | } | ||
| 23 | 24 | ||||
| 24 | type RegistryAuthConfig struct { | 25 | type RegistryAuthConfig struct { | ||
| @@ -84,6 +85,14 @@ func Load() (*Config, error) { | |||||
| 84 | } | 85 | } | ||
| 85 | } | 86 | } | ||
| 86 | 87 | ||||
| 88 | + | // Validate registry domain if provided | |||
| 89 | + | if cfg.Server.RegistryDomain != "" { | |||
| 90 | + | // Basic domain validation - should not contain protocol or paths | |||
| 91 | + | if strings.Contains(cfg.Server.RegistryDomain, "://") || strings.Contains(cfg.Server.RegistryDomain, "/") { | |||
| 92 | + | return nil, fmt.Errorf("registry_domain should be just the domain name (e.g. 'registry.example.com')") | |||
| 93 | + | } | |||
| 94 | + | } | |||
| 95 | + | ||||
| 87 | return &cfg, nil | 96 | return &cfg, nil | ||
| 88 | } | 97 | } | ||
| 89 | 98 | ||||
M internal/container/docker.go +112 -0
| @@ -310,4 +310,116 @@ func (d *DockerRuntime) GetContainerPort(ctx context.Context, containerID string | |||
| 310 | 310 | } | |
| 311 | 311 | ||
| 312 | 312 | return hostPort, nil | |
| 313 | + | } | |
| 314 | + | ||
| 315 | + | // GetImageExposedPorts gets the exposed ports from an image | |
| 316 | + | func (d *DockerRuntime) GetImageExposedPorts(ctx context.Context, imageRef string) ([]int, error) { | |
| 317 | + | imageInspect, err := d.client.ImageInspect(ctx, imageRef) | |
| 318 | + | if err != nil { | |
| 319 | + | return nil, fmt.Errorf("failed to inspect image %s: %w", imageRef, err) | |
| 320 | + | } | |
| 321 | + | ||
| 322 | + | var ports []int | |
| 323 | + | if imageInspect.Config != nil && imageInspect.Config.ExposedPorts != nil { | |
| 324 | + | for portSpec := range imageInspect.Config.ExposedPorts { | |
| 325 | + | // Parse port from format like "80/tcp" | |
| 326 | + | portStr := strings.Split(string(portSpec), "/")[0] | |
| 327 | + | if port, err := strconv.Atoi(portStr); err == nil { | |
| 328 | + | ports = append(ports, port) | |
| 329 | + | } | |
| 330 | + | } | |
| 331 | + | } | |
| 332 | + | ||
| 333 | + | // If no exposed ports found, return common web ports as fallback | |
| 334 | + | if len(ports) == 0 { | |
| 335 | + | log.Warn().Str("image", imageRef).Msg("No EXPOSE directives found in image, using common web ports") | |
| 336 | + | return []int{80, 8080, 3000}, nil | |
| 337 | + | } | |
| 338 | + | ||
| 339 | + | log.Info().Str("image", imageRef).Ints("exposed_ports", ports).Msg("Found exposed ports in image") | |
| 340 | + | return ports, nil | |
| 341 | + | } | |
| 342 | + | ||
| 343 | + | // GetContainerExposedPorts gets all exposed ports from a running container | |
| 344 | + | func (d *DockerRuntime) GetContainerExposedPorts(ctx context.Context, containerID string) ([]int, error) { | |
| 345 | + | resp, err := d.client.ContainerInspect(ctx, containerID) | |
| 346 | + | if err != nil { | |
| 347 | + | return nil, fmt.Errorf("failed to inspect container %s: %w", containerID, err) | |
| 348 | + | } | |
| 349 | + | ||
| 350 | + | var ports []int | |
| 351 | + | if resp.NetworkSettings != nil && resp.NetworkSettings.Ports != nil { | |
| 352 | + | for portSpec := range resp.NetworkSettings.Ports { | |
| 353 | + | // Parse port from format like "80/tcp" | |
| 354 | + | portStr := strings.Split(string(portSpec), "/")[0] | |
| 355 | + | if port, err := strconv.Atoi(portStr); err == nil { | |
| 356 | + | ports = append(ports, port) | |
| 357 | + | } | |
| 358 | + | } | |
| 359 | + | } | |
| 360 | + | ||
| 361 | + | return ports, nil | |
| 362 | + | } | |
| 363 | + | ||
| 364 | + | // GetContainerNetworkInfo gets container's internal IP and automatically detects the best port to use | |
| 365 | + | func (d *DockerRuntime) GetContainerNetworkInfo(ctx context.Context, containerID string) (string, int, error) { | |
| 366 | + | resp, err := d.client.ContainerInspect(ctx, containerID) | |
| 367 | + | if err != nil { | |
| 368 | + | return "", 0, fmt.Errorf("failed to inspect container %s: %w", containerID, err) | |
| 369 | + | } | |
| 370 | + | ||
| 371 | + | // Get container's internal IP address | |
| 372 | + | var containerIP string | |
| 373 | + | if resp.NetworkSettings != nil && resp.NetworkSettings.Networks != nil { | |
| 374 | + | // Use the first available network (usually bridge or custom network) | |
| 375 | + | for _, network := range resp.NetworkSettings.Networks { | |
| 376 | + | if network.IPAddress != "" { | |
| 377 | + | containerIP = network.IPAddress | |
| 378 | + | break | |
| 379 | + | } | |
| 380 | + | } | |
| 381 | + | } | |
| 382 | + | ||
| 383 | + | if containerIP == "" { | |
| 384 | + | return "", 0, fmt.Errorf("no IP address found for container %s", containerID) | |
| 385 | + | } | |
| 386 | + | ||
| 387 | + | // Get exposed ports and select the best one (Traefik-style logic) | |
| 388 | + | var exposedPorts []int | |
| 389 | + | if resp.NetworkSettings != nil && resp.NetworkSettings.Ports != nil { | |
| 390 | + | for portSpec := range resp.NetworkSettings.Ports { | |
| 391 | + | // Parse port from format like "80/tcp" | |
| 392 | + | portStr := strings.Split(string(portSpec), "/")[0] | |
| 393 | + | if port, err := strconv.Atoi(portStr); err == nil { | |
| 394 | + | exposedPorts = append(exposedPorts, port) | |
| 395 | + | } | |
| 396 | + | } | |
| 397 | + | } | |
| 398 | + | ||
| 399 | + | if len(exposedPorts) == 0 { | |
| 400 | + | return "", 0, fmt.Errorf("no exposed ports found for container %s", containerID) | |
| 401 | + | } | |
| 402 | + | ||
| 403 | + | // Sort ports and select the lowest one (Traefik strategy) | |
| 404 | + | var selectedPort int | |
| 405 | + | if len(exposedPorts) == 1 { | |
| 406 | + | selectedPort = exposedPorts[0] | |
| 407 | + | } else { | |
| 408 | + | // Find the lowest port number | |
| 409 | + | selectedPort = exposedPorts[0] | |
| 410 | + | for _, port := range exposedPorts { | |
| 411 | + | if port < selectedPort { | |
| 412 | + | selectedPort = port | |
| 413 | + | } | |
| 414 | + | } | |
| 415 | + | } | |
| 416 | + | ||
| 417 | + | log.Info(). | |
| 418 | + | Str("container_id", containerID). | |
| 419 | + | Str("ip", containerIP). | |
| 420 | + | Int("selected_port", selectedPort). | |
| 421 | + | Ints("available_ports", exposedPorts). | |
| 422 | + | Msg("Container network info detected") | |
| 423 | + | ||
| 424 | + | return containerIP, selectedPort, nil | |
| 313 | 425 | }⏎̸ | |
| @@ -310,4 +310,116 @@ func (d *DockerRuntime) GetContainerPort(ctx context.Context, containerID string | |||||
| 310 | } | 310 | } | ||
| 311 | 311 | ||||
| 312 | return hostPort, nil | 312 | return hostPort, nil | ||
| 313 | + | } | |||
| 314 | + | ||||
| 315 | + | // GetImageExposedPorts gets the exposed ports from an image | |||
| 316 | + | func (d *DockerRuntime) GetImageExposedPorts(ctx context.Context, imageRef string) ([]int, error) { | |||
| 317 | + | imageInspect, err := d.client.ImageInspect(ctx, imageRef) | |||
| 318 | + | if err != nil { | |||
| 319 | + | return nil, fmt.Errorf("failed to inspect image %s: %w", imageRef, err) | |||
| 320 | + | } | |||
| 321 | + | ||||
| 322 | + | var ports []int | |||
| 323 | + | if imageInspect.Config != nil && imageInspect.Config.ExposedPorts != nil { | |||
| 324 | + | for portSpec := range imageInspect.Config.ExposedPorts { | |||
| 325 | + | // Parse port from format like "80/tcp" | |||
| 326 | + | portStr := strings.Split(string(portSpec), "/")[0] | |||
| 327 | + | if port, err := strconv.Atoi(portStr); err == nil { | |||
| 328 | + | ports = append(ports, port) | |||
| 329 | + | } | |||
| 330 | + | } | |||
| 331 | + | } | |||
| 332 | + | ||||
| 333 | + | // If no exposed ports found, return common web ports as fallback | |||
| 334 | + | if len(ports) == 0 { | |||
| 335 | + | log.Warn().Str("image", imageRef).Msg("No EXPOSE directives found in image, using common web ports") | |||
| 336 | + | return []int{80, 8080, 3000}, nil | |||
| 337 | + | } | |||
| 338 | + | ||||
| 339 | + | log.Info().Str("image", imageRef).Ints("exposed_ports", ports).Msg("Found exposed ports in image") | |||
| 340 | + | return ports, nil | |||
| 341 | + | } | |||
| 342 | + | ||||
| 343 | + | // GetContainerExposedPorts gets all exposed ports from a running container | |||
| 344 | + | func (d *DockerRuntime) GetContainerExposedPorts(ctx context.Context, containerID string) ([]int, error) { | |||
| 345 | + | resp, err := d.client.ContainerInspect(ctx, containerID) | |||
| 346 | + | if err != nil { | |||
| 347 | + | return nil, fmt.Errorf("failed to inspect container %s: %w", containerID, err) | |||
| 348 | + | } | |||
| 349 | + | ||||
| 350 | + | var ports []int | |||
| 351 | + | if resp.NetworkSettings != nil && resp.NetworkSettings.Ports != nil { | |||
| 352 | + | for portSpec := range resp.NetworkSettings.Ports { | |||
| 353 | + | // Parse port from format like "80/tcp" | |||
| 354 | + | portStr := strings.Split(string(portSpec), "/")[0] | |||
| 355 | + | if port, err := strconv.Atoi(portStr); err == nil { | |||
| 356 | + | ports = append(ports, port) | |||
| 357 | + | } | |||
| 358 | + | } | |||
| 359 | + | } | |||
| 360 | + | ||||
| 361 | + | return ports, nil | |||
| 362 | + | } | |||
| 363 | + | ||||
| 364 | + | // GetContainerNetworkInfo gets container's internal IP and automatically detects the best port to use | |||
| 365 | + | func (d *DockerRuntime) GetContainerNetworkInfo(ctx context.Context, containerID string) (string, int, error) { | |||
| 366 | + | resp, err := d.client.ContainerInspect(ctx, containerID) | |||
| 367 | + | if err != nil { | |||
| 368 | + | return "", 0, fmt.Errorf("failed to inspect container %s: %w", containerID, err) | |||
| 369 | + | } | |||
| 370 | + | ||||
| 371 | + | // Get container's internal IP address | |||
| 372 | + | var containerIP string | |||
| 373 | + | if resp.NetworkSettings != nil && resp.NetworkSettings.Networks != nil { | |||
| 374 | + | // Use the first available network (usually bridge or custom network) | |||
| 375 | + | for _, network := range resp.NetworkSettings.Networks { | |||
| 376 | + | if network.IPAddress != "" { | |||
| 377 | + | containerIP = network.IPAddress | |||
| 378 | + | break | |||
| 379 | + | } | |||
| 380 | + | } | |||
| 381 | + | } | |||
| 382 | + | ||||
| 383 | + | if containerIP == "" { | |||
| 384 | + | return "", 0, fmt.Errorf("no IP address found for container %s", containerID) | |||
| 385 | + | } | |||
| 386 | + | ||||
| 387 | + | // Get exposed ports and select the best one (Traefik-style logic) | |||
| 388 | + | var exposedPorts []int | |||
| 389 | + | if resp.NetworkSettings != nil && resp.NetworkSettings.Ports != nil { | |||
| 390 | + | for portSpec := range resp.NetworkSettings.Ports { | |||
| 391 | + | // Parse port from format like "80/tcp" | |||
| 392 | + | portStr := strings.Split(string(portSpec), "/")[0] | |||
| 393 | + | if port, err := strconv.Atoi(portStr); err == nil { | |||
| 394 | + | exposedPorts = append(exposedPorts, port) | |||
| 395 | + | } | |||
| 396 | + | } | |||
| 397 | + | } | |||
| 398 | + | ||||
| 399 | + | if len(exposedPorts) == 0 { | |||
| 400 | + | return "", 0, fmt.Errorf("no exposed ports found for container %s", containerID) | |||
| 401 | + | } | |||
| 402 | + | ||||
| 403 | + | // Sort ports and select the lowest one (Traefik strategy) | |||
| 404 | + | var selectedPort int | |||
| 405 | + | if len(exposedPorts) == 1 { | |||
| 406 | + | selectedPort = exposedPorts[0] | |||
| 407 | + | } else { | |||
| 408 | + | // Find the lowest port number | |||
| 409 | + | selectedPort = exposedPorts[0] | |||
| 410 | + | for _, port := range exposedPorts { | |||
| 411 | + | if port < selectedPort { | |||
| 412 | + | selectedPort = port | |||
| 413 | + | } | |||
| 414 | + | } | |||
| 415 | + | } | |||
| 416 | + | ||||
| 417 | + | log.Info(). | |||
| 418 | + | Str("container_id", containerID). | |||
| 419 | + | Str("ip", containerIP). | |||
| 420 | + | Int("selected_port", selectedPort). | |||
| 421 | + | Ints("available_ports", exposedPorts). | |||
| 422 | + | Msg("Container network info detected") | |||
| 423 | + | ||||
| 424 | + | return containerIP, selectedPort, nil | |||
| 313 | }⏎̸ | 425 | }⏎̸ | ||
M internal/container/manager.go +71 -20
| @@ -84,15 +84,23 @@ func (m *Manager) DeployContainer(ctx context.Context, route config.Route) (*run | |||
| 84 | 84 | return nil, fmt.Errorf("failed to pull image %s: %w", route.Image, err) | |
| 85 | 85 | } | |
| 86 | 86 | ||
| 87 | + | // Get exposed ports from the image | |
| 88 | + | exposedPorts, err := m.runtime.GetImageExposedPorts(ctx, route.Image) | |
| 89 | + | if err != nil { | |
| 90 | + | log.Warn().Err(err).Str("image", route.Image).Msg("Failed to get exposed ports from image, using defaults") | |
| 91 | + | exposedPorts = []int{80, 8080, 3000} // Fallback to common web server ports | |
| 92 | + | } | |
| 93 | + | ||
| 87 | 94 | // Create container configuration | |
| 88 | 95 | containerConfig := &runtime.ContainerConfig{ | |
| 89 | 96 | Image: route.Image, | |
| 90 | 97 | Name: fmt.Sprintf("gordon-%s", route.Domain), | |
| 91 | - | Ports: []int{80, 8080, 3000}, // Common web server ports | |
| 98 | + | Ports: exposedPorts, | |
| 92 | 99 | Labels: map[string]string{ | |
| 93 | 100 | "gordon.domain": route.Domain, | |
| 94 | 101 | "gordon.image": route.Image, | |
| 95 | 102 | "gordon.managed": "true", | |
| 103 | + | "gordon.route": route.Domain, | |
| 96 | 104 | }, | |
| 97 | 105 | AutoRemove: false, // Keep containers for inspection | |
| 98 | 106 | } | |
| @@ -164,43 +172,86 @@ func (m *Manager) ListContainers() map[string]*runtime.Container { | |||
| 164 | 172 | return result | |
| 165 | 173 | } | |
| 166 | 174 | ||
| 167 | - | // StopContainer stops a container for a domain | |
| 168 | - | func (m *Manager) StopContainer(ctx context.Context, domain string) error { | |
| 169 | - | m.mu.Lock() | |
| 170 | - | defer m.mu.Unlock() | |
| 175 | + | // FindContainerByDomain returns the container ID for a domain | |
| 176 | + | func (m *Manager) FindContainerByDomain(domain string) (string, bool) { | |
| 177 | + | m.mu.RLock() | |
| 178 | + | defer m.mu.RUnlock() | |
| 171 | 179 | ||
| 172 | - | container, exists := m.containers[domain] | |
| 173 | - | if !exists { | |
| 174 | - | return fmt.Errorf("no container found for domain %s", domain) | |
| 180 | + | if container, exists := m.containers[domain]; exists { | |
| 181 | + | return container.ID, true | |
| 182 | + | } | |
| 183 | + | return "", false | |
| 184 | + | } | |
| 185 | + | ||
| 186 | + | // FindDomainByContainerID returns the domain for a container ID | |
| 187 | + | func (m *Manager) FindDomainByContainerID(containerID string) (string, bool) { | |
| 188 | + | m.mu.RLock() | |
| 189 | + | defer m.mu.RUnlock() | |
| 190 | + | ||
| 191 | + | for domain, container := range m.containers { | |
| 192 | + | if container.ID == containerID { | |
| 193 | + | return domain, true | |
| 194 | + | } | |
| 175 | 195 | } | |
| 196 | + | return "", false | |
| 197 | + | } | |
| 176 | 198 | ||
| 177 | - | if err := m.runtime.StopContainer(ctx, container.ID); err != nil { | |
| 178 | - | return fmt.Errorf("failed to stop container for %s: %w", domain, err) | |
| 199 | + | // StopContainer stops a container by ID | |
| 200 | + | func (m *Manager) StopContainer(ctx context.Context, containerID string) error { | |
| 201 | + | if err := m.runtime.StopContainer(ctx, containerID); err != nil { | |
| 202 | + | return fmt.Errorf("failed to stop container %s: %w", containerID, err) | |
| 179 | 203 | } | |
| 180 | 204 | ||
| 181 | - | log.Info().Str("domain", domain).Str("container", container.ID).Msg("Container stopped") | |
| 205 | + | log.Info().Str("container", containerID).Msg("Container stopped") | |
| 182 | 206 | return nil | |
| 183 | 207 | } | |
| 184 | 208 | ||
| 185 | - | // RemoveContainer removes a container for a domain | |
| 186 | - | func (m *Manager) RemoveContainer(ctx context.Context, domain string, force bool) error { | |
| 187 | - | m.mu.Lock() | |
| 188 | - | defer m.mu.Unlock() | |
| 189 | - | ||
| 209 | + | // StopContainerByDomain stops a container for a domain | |
| 210 | + | func (m *Manager) StopContainerByDomain(ctx context.Context, domain string) error { | |
| 211 | + | m.mu.RLock() | |
| 190 | 212 | container, exists := m.containers[domain] | |
| 213 | + | m.mu.RUnlock() | |
| 214 | + | ||
| 191 | 215 | if !exists { | |
| 192 | 216 | return fmt.Errorf("no container found for domain %s", domain) | |
| 193 | 217 | } | |
| 194 | 218 | ||
| 195 | - | if err := m.runtime.RemoveContainer(ctx, container.ID, force); err != nil { | |
| 196 | - | return fmt.Errorf("failed to remove container for %s: %w", domain, err) | |
| 219 | + | return m.StopContainer(ctx, container.ID) | |
| 220 | + | } | |
| 221 | + | ||
| 222 | + | // RemoveContainer removes a container by ID | |
| 223 | + | func (m *Manager) RemoveContainer(ctx context.Context, containerID string, force bool) error { | |
| 224 | + | if err := m.runtime.RemoveContainer(ctx, containerID, force); err != nil { | |
| 225 | + | return fmt.Errorf("failed to remove container %s: %w", containerID, err) | |
| 197 | 226 | } | |
| 198 | 227 | ||
| 199 | - | delete(m.containers, domain) | |
| 200 | - | log.Info().Str("domain", domain).Str("container", container.ID).Msg("Container removed") | |
| 228 | + | // Remove from our tracking map | |
| 229 | + | m.mu.Lock() | |
| 230 | + | defer m.mu.Unlock() | |
| 231 | + | for domain, container := range m.containers { | |
| 232 | + | if container.ID == containerID { | |
| 233 | + | delete(m.containers, domain) | |
| 234 | + | log.Info().Str("domain", domain).Str("container", containerID).Msg("Container removed") | |
| 235 | + | break | |
| 236 | + | } | |
| 237 | + | } | |
| 238 | + | ||
| 201 | 239 | return nil | |
| 202 | 240 | } | |
| 203 | 241 | ||
| 242 | + | // RemoveContainerByDomain removes a container for a domain | |
| 243 | + | func (m *Manager) RemoveContainerByDomain(ctx context.Context, domain string, force bool) error { | |
| 244 | + | m.mu.RLock() | |
| 245 | + | container, exists := m.containers[domain] | |
| 246 | + | m.mu.RUnlock() | |
| 247 | + | ||
| 248 | + | if !exists { | |
| 249 | + | return fmt.Errorf("no container found for domain %s", domain) | |
| 250 | + | } | |
| 251 | + | ||
| 252 | + | return m.RemoveContainer(ctx, container.ID, force) | |
| 253 | + | } | |
| 254 | + | ||
| 204 | 255 | // SyncContainers synchronizes the internal state with the actual running containers | |
| 205 | 256 | func (m *Manager) SyncContainers(ctx context.Context) error { | |
| 206 | 257 | m.mu.Lock() | |
| @@ -84,15 +84,23 @@ func (m *Manager) DeployContainer(ctx context.Context, route config.Route) (*run | |||||
| 84 | return nil, fmt.Errorf("failed to pull image %s: %w", route.Image, err) | 84 | return nil, fmt.Errorf("failed to pull image %s: %w", route.Image, err) | ||
| 85 | } | 85 | } | ||
| 86 | 86 | ||||
| 87 | + | // Get exposed ports from the image | |||
| 88 | + | exposedPorts, err := m.runtime.GetImageExposedPorts(ctx, route.Image) | |||
| 89 | + | if err != nil { | |||
| 90 | + | log.Warn().Err(err).Str("image", route.Image).Msg("Failed to get exposed ports from image, using defaults") | |||
| 91 | + | exposedPorts = []int{80, 8080, 3000} // Fallback to common web server ports | |||
| 92 | + | } | |||
| 93 | + | ||||
| 87 | // Create container configuration | 94 | // Create container configuration | ||
| 88 | containerConfig := &runtime.ContainerConfig{ | 95 | containerConfig := &runtime.ContainerConfig{ | ||
| 89 | Image: route.Image, | 96 | Image: route.Image, | ||
| 90 | Name: fmt.Sprintf("gordon-%s", route.Domain), | 97 | Name: fmt.Sprintf("gordon-%s", route.Domain), | ||
| 91 | - | Ports: []int{80, 8080, 3000}, // Common web server ports | 98 | + | Ports: exposedPorts, |
| 92 | Labels: map[string]string{ | 99 | Labels: map[string]string{ | ||
| 93 | "gordon.domain": route.Domain, | 100 | "gordon.domain": route.Domain, | ||
| 94 | "gordon.image": route.Image, | 101 | "gordon.image": route.Image, | ||
| 95 | "gordon.managed": "true", | 102 | "gordon.managed": "true", | ||
| 103 | + | "gordon.route": route.Domain, | |||
| 96 | }, | 104 | }, | ||
| 97 | AutoRemove: false, // Keep containers for inspection | 105 | AutoRemove: false, // Keep containers for inspection | ||
| 98 | } | 106 | } | ||
| @@ -164,43 +172,86 @@ func (m *Manager) ListContainers() map[string]*runtime.Container { | |||||
| 164 | return result | 172 | return result | ||
| 165 | } | 173 | } | ||
| 166 | 174 | ||||
| 167 | - | // StopContainer stops a container for a domain | 175 | + | // FindContainerByDomain returns the container ID for a domain |
| 168 | - | func (m *Manager) StopContainer(ctx context.Context, domain string) error { | 176 | + | func (m *Manager) FindContainerByDomain(domain string) (string, bool) { |
| 169 | - | m.mu.Lock() | 177 | + | m.mu.RLock() |
| 170 | - | defer m.mu.Unlock() | 178 | + | defer m.mu.RUnlock() |
| 171 | 179 | ||||
| 172 | - | container, exists := m.containers[domain] | 180 | + | if container, exists := m.containers[domain]; exists { |
| 173 | - | if !exists { | 181 | + | return container.ID, true |
| 174 | - | return fmt.Errorf("no container found for domain %s", domain) | 182 | + | } |
| 183 | + | return "", false | |||
| 184 | + | } | |||
| 185 | + | ||||
| 186 | + | // FindDomainByContainerID returns the domain for a container ID | |||
| 187 | + | func (m *Manager) FindDomainByContainerID(containerID string) (string, bool) { | |||
| 188 | + | m.mu.RLock() | |||
| 189 | + | defer m.mu.RUnlock() | |||
| 190 | + | ||||
| 191 | + | for domain, container := range m.containers { | |||
| 192 | + | if container.ID == containerID { | |||
| 193 | + | return domain, true | |||
| 194 | + | } | |||
| 175 | } | 195 | } | ||
| 196 | + | return "", false | |||
| 197 | + | } | |||
| 176 | 198 | ||||
| 177 | - | if err := m.runtime.StopContainer(ctx, container.ID); err != nil { | 199 | + | // StopContainer stops a container by ID |
| 178 | - | return fmt.Errorf("failed to stop container for %s: %w", domain, err) | 200 | + | func (m *Manager) StopContainer(ctx context.Context, containerID string) error { |
| 201 | + | if err := m.runtime.StopContainer(ctx, containerID); err != nil { | |||
| 202 | + | return fmt.Errorf("failed to stop container %s: %w", containerID, err) | |||
| 179 | } | 203 | } | ||
| 180 | 204 | ||||
| 181 | - | log.Info().Str("domain", domain).Str("container", container.ID).Msg("Container stopped") | 205 | + | log.Info().Str("container", containerID).Msg("Container stopped") |
| 182 | return nil | 206 | return nil | ||
| 183 | } | 207 | } | ||
| 184 | 208 | ||||
| 185 | - | // RemoveContainer removes a container for a domain | 209 | + | // StopContainerByDomain stops a container for a domain |
| 186 | - | func (m *Manager) RemoveContainer(ctx context.Context, domain string, force bool) error { | 210 | + | func (m *Manager) StopContainerByDomain(ctx context.Context, domain string) error { |
| 187 | - | m.mu.Lock() | 211 | + | m.mu.RLock() |
| 188 | - | defer m.mu.Unlock() | |||
| 189 | - | ||||
| 190 | container, exists := m.containers[domain] | 212 | container, exists := m.containers[domain] | ||
| 213 | + | m.mu.RUnlock() | |||
| 214 | + | ||||
| 191 | if !exists { | 215 | if !exists { | ||
| 192 | return fmt.Errorf("no container found for domain %s", domain) | 216 | return fmt.Errorf("no container found for domain %s", domain) | ||
| 193 | } | 217 | } | ||
| 194 | 218 | ||||
| 195 | - | if err := m.runtime.RemoveContainer(ctx, container.ID, force); err != nil { | 219 | + | return m.StopContainer(ctx, container.ID) |
| 196 | - | return fmt.Errorf("failed to remove container for %s: %w", domain, err) | 220 | + | } |
| 221 | + | ||||
| 222 | + | // RemoveContainer removes a container by ID | |||
| 223 | + | func (m *Manager) RemoveContainer(ctx context.Context, containerID string, force bool) error { | |||
| 224 | + | if err := m.runtime.RemoveContainer(ctx, containerID, force); err != nil { | |||
| 225 | + | return fmt.Errorf("failed to remove container %s: %w", containerID, err) | |||
| 197 | } | 226 | } | ||
| 198 | 227 | ||||
| 199 | - | delete(m.containers, domain) | 228 | + | // Remove from our tracking map |
| 200 | - | log.Info().Str("domain", domain).Str("container", container.ID).Msg("Container removed") | 229 | + | m.mu.Lock() |
| 230 | + | defer m.mu.Unlock() | |||
| 231 | + | for domain, container := range m.containers { | |||
| 232 | + | if container.ID == containerID { | |||
| 233 | + | delete(m.containers, domain) | |||
| 234 | + | log.Info().Str("domain", domain).Str("container", containerID).Msg("Container removed") | |||
| 235 | + | break | |||
| 236 | + | } | |||
| 237 | + | } | |||
| 238 | + | ||||
| 201 | return nil | 239 | return nil | ||
| 202 | } | 240 | } | ||
| 203 | 241 | ||||
| 242 | + | // RemoveContainerByDomain removes a container for a domain | |||
| 243 | + | func (m *Manager) RemoveContainerByDomain(ctx context.Context, domain string, force bool) error { | |||
| 244 | + | m.mu.RLock() | |||
| 245 | + | container, exists := m.containers[domain] | |||
| 246 | + | m.mu.RUnlock() | |||
| 247 | + | ||||
| 248 | + | if !exists { | |||
| 249 | + | return fmt.Errorf("no container found for domain %s", domain) | |||
| 250 | + | } | |||
| 251 | + | ||||
| 252 | + | return m.RemoveContainer(ctx, container.ID, force) | |||
| 253 | + | } | |||
| 254 | + | ||||
| 204 | // SyncContainers synchronizes the internal state with the actual running containers | 255 | // SyncContainers synchronizes the internal state with the actual running containers | ||
| 205 | func (m *Manager) SyncContainers(ctx context.Context) error { | 256 | func (m *Manager) SyncContainers(ctx context.Context) error { | ||
| 206 | m.mu.Lock() | 257 | m.mu.Lock() | ||
M internal/events/handlers.go +12 -29
| @@ -95,10 +95,7 @@ func (h *ContainerEventHandler) handleConfigReload(event Event) error { | |||
| 95 | 95 | ctx := context.Background() | |
| 96 | 96 | ||
| 97 | 97 | // Get current containers managed by Gordon | |
| 98 | - | currentContainers, err := h.manager.ListContainers(ctx, false) | |
| 99 | - | if err != nil { | |
| 100 | - | return fmt.Errorf("failed to list containers: %w", err) | |
| 101 | - | } | |
| 98 | + | currentContainers := h.manager.ListContainers() | |
| 102 | 99 | ||
| 103 | 100 | // Track which routes are currently active | |
| 104 | 101 | activeRoutes := make(map[string]*runtime.Container) | |
| @@ -200,12 +197,9 @@ func (h *ContainerEventHandler) handleContainerStart(event Event) error { | |||
| 200 | 197 | Str("event_id", event.ID). | |
| 201 | 198 | Msg("Processing container start event") | |
| 202 | 199 | ||
| 203 | - | ctx := context.Background() | |
| 204 | - | if err := h.manager.StartContainer(ctx, containerID); err != nil { | |
| 205 | - | return fmt.Errorf("failed to start container %s: %w", containerID, err) | |
| 206 | - | } | |
| 207 | - | ||
| 208 | - | return nil | |
| 200 | + | // StartContainer method doesn't exist on Manager - containers are started via DeployContainer | |
| 201 | + | // For manual start, we'd need to add this method or use the runtime directly | |
| 202 | + | return fmt.Errorf("container start not implemented - use automatic deployment instead") | |
| 209 | 203 | } | |
| 210 | 204 | ||
| 211 | 205 | func (h *ContainerEventHandler) findRoutesForImage(imageName string) []string { | |
| @@ -223,10 +217,7 @@ func (h *ContainerEventHandler) findRoutesForImage(imageName string) []string { | |||
| 223 | 217 | func (h *ContainerEventHandler) deployContainerForRoute(route, imageName string) error { | |
| 224 | 218 | ctx := context.Background() | |
| 225 | 219 | ||
| 226 | - | containers, err := h.manager.ListContainers(ctx, false) | |
| 227 | - | if err != nil { | |
| 228 | - | return fmt.Errorf("failed to list containers: %w", err) | |
| 229 | - | } | |
| 220 | + | containers := h.manager.ListContainers() | |
| 230 | 221 | ||
| 231 | 222 | containerName := fmt.Sprintf("gordon-%s", strings.ReplaceAll(route, ".", "-")) | |
| 232 | 223 | ||
| @@ -254,24 +245,16 @@ func (h *ContainerEventHandler) deployContainerForRoute(route, imageName string) | |||
| 254 | 245 | } | |
| 255 | 246 | } | |
| 256 | 247 | ||
| 257 | - | containerConfig := &runtime.ContainerConfig{ | |
| 258 | - | Image: imageName, | |
| 259 | - | Name: containerName, | |
| 260 | - | AutoRemove: false, | |
| 261 | - | Labels: map[string]string{ | |
| 262 | - | "gordon.route": route, | |
| 263 | - | "gordon.image": imageName, | |
| 264 | - | "gordon.managed": "true", | |
| 265 | - | }, | |
| 248 | + | // Use DeployContainer which handles creation and starting | |
| 249 | + | routeConfig := config.Route{ | |
| 250 | + | Domain: route, | |
| 251 | + | Image: imageName, | |
| 252 | + | HTTPS: true, | |
| 266 | 253 | } | |
| 267 | 254 | ||
| 268 | - | container, err := h.manager.CreateContainer(ctx, containerConfig) | |
| 255 | + | container, err := h.manager.DeployContainer(ctx, routeConfig) | |
| 269 | 256 | if err != nil { | |
| 270 | - | return fmt.Errorf("failed to create container for route %s: %w", route, err) | |
| 271 | - | } | |
| 272 | - | ||
| 273 | - | if err := h.manager.StartContainer(ctx, container.ID); err != nil { | |
| 274 | - | return fmt.Errorf("failed to start container for route %s: %w", route, err) | |
| 257 | + | return fmt.Errorf("failed to deploy container for route %s: %w", route, err) | |
| 275 | 258 | } | |
| 276 | 259 | ||
| 277 | 260 | log.Info(). | |
| @@ -95,10 +95,7 @@ func (h *ContainerEventHandler) handleConfigReload(event Event) error { | |||||
| 95 | ctx := context.Background() | 95 | ctx := context.Background() | ||
| 96 | 96 | ||||
| 97 | // Get current containers managed by Gordon | 97 | // Get current containers managed by Gordon | ||
| 98 | - | currentContainers, err := h.manager.ListContainers(ctx, false) | 98 | + | currentContainers := h.manager.ListContainers() |
| 99 | - | if err != nil { | |||
| 100 | - | return fmt.Errorf("failed to list containers: %w", err) | |||
| 101 | - | } | |||
| 102 | 99 | ||||
| 103 | // Track which routes are currently active | 100 | // Track which routes are currently active | ||
| 104 | activeRoutes := make(map[string]*runtime.Container) | 101 | activeRoutes := make(map[string]*runtime.Container) | ||
| @@ -200,12 +197,9 @@ func (h *ContainerEventHandler) handleContainerStart(event Event) error { | |||||
| 200 | Str("event_id", event.ID). | 197 | Str("event_id", event.ID). | ||
| 201 | Msg("Processing container start event") | 198 | Msg("Processing container start event") | ||
| 202 | 199 | ||||
| 203 | - | ctx := context.Background() | 200 | + | // StartContainer method doesn't exist on Manager - containers are started via DeployContainer |
| 204 | - | if err := h.manager.StartContainer(ctx, containerID); err != nil { | 201 | + | // For manual start, we'd need to add this method or use the runtime directly |
| 205 | - | return fmt.Errorf("failed to start container %s: %w", containerID, err) | 202 | + | return fmt.Errorf("container start not implemented - use automatic deployment instead") |
| 206 | - | } | |||
| 207 | - | ||||
| 208 | - | return nil | |||
| 209 | } | 203 | } | ||
| 210 | 204 | ||||
| 211 | func (h *ContainerEventHandler) findRoutesForImage(imageName string) []string { | 205 | func (h *ContainerEventHandler) findRoutesForImage(imageName string) []string { | ||
| @@ -223,10 +217,7 @@ func (h *ContainerEventHandler) findRoutesForImage(imageName string) []string { | |||||
| 223 | func (h *ContainerEventHandler) deployContainerForRoute(route, imageName string) error { | 217 | func (h *ContainerEventHandler) deployContainerForRoute(route, imageName string) error { | ||
| 224 | ctx := context.Background() | 218 | ctx := context.Background() | ||
| 225 | 219 | ||||
| 226 | - | containers, err := h.manager.ListContainers(ctx, false) | 220 | + | containers := h.manager.ListContainers() |
| 227 | - | if err != nil { | |||
| 228 | - | return fmt.Errorf("failed to list containers: %w", err) | |||
| 229 | - | } | |||
| 230 | 221 | ||||
| 231 | containerName := fmt.Sprintf("gordon-%s", strings.ReplaceAll(route, ".", "-")) | 222 | containerName := fmt.Sprintf("gordon-%s", strings.ReplaceAll(route, ".", "-")) | ||
| 232 | 223 | ||||
| @@ -254,24 +245,16 @@ func (h *ContainerEventHandler) deployContainerForRoute(route, imageName string) | |||||
| 254 | } | 245 | } | ||
| 255 | } | 246 | } | ||
| 256 | 247 | ||||
| 257 | - | containerConfig := &runtime.ContainerConfig{ | 248 | + | // Use DeployContainer which handles creation and starting |
| 258 | - | Image: imageName, | 249 | + | routeConfig := config.Route{ |
| 259 | - | Name: containerName, | 250 | + | Domain: route, |
| 260 | - | AutoRemove: false, | 251 | + | Image: imageName, |
| 261 | - | Labels: map[string]string{ | 252 | + | HTTPS: true, |
| 262 | - | "gordon.route": route, | |||
| 263 | - | "gordon.image": imageName, | |||
| 264 | - | "gordon.managed": "true", | |||
| 265 | - | }, | |||
| 266 | } | 253 | } | ||
| 267 | 254 | ||||
| 268 | - | container, err := h.manager.CreateContainer(ctx, containerConfig) | 255 | + | container, err := h.manager.DeployContainer(ctx, routeConfig) |
| 269 | if err != nil { | 256 | if err != nil { | ||
| 270 | - | return fmt.Errorf("failed to create container for route %s: %w", route, err) | 257 | + | return fmt.Errorf("failed to deploy container for route %s: %w", route, err) |
| 271 | - | } | |||
| 272 | - | ||||
| 273 | - | if err := h.manager.StartContainer(ctx, container.ID); err != nil { | |||
| 274 | - | return fmt.Errorf("failed to start container for route %s: %w", route, err) | |||
| 275 | } | 258 | } | ||
| 276 | 259 | ||||
| 277 | log.Info(). | 260 | log.Info(). | ||
M internal/middleware/auth.go +0 -1
| @@ -3,7 +3,6 @@ package middleware | |||
| 3 | 3 | import ( | |
| 4 | 4 | "crypto/subtle" | |
| 5 | 5 | "net/http" | |
| 6 | - | "strings" | |
| 7 | 6 | ||
| 8 | 7 | "github.com/rs/zerolog/log" | |
| 9 | 8 | ) | |
| @@ -3,7 +3,6 @@ package middleware | |||||
| 3 | import ( | 3 | import ( | ||
| 4 | "crypto/subtle" | 4 | "crypto/subtle" | ||
| 5 | "net/http" | 5 | "net/http" | ||
| 6 | - | "strings" | |||
| 7 | 6 | ||||
| 8 | "github.com/rs/zerolog/log" | 7 | "github.com/rs/zerolog/log" | ||
| 9 | ) | 8 | ) | ||
M internal/proxy/server.go +50 -152
| @@ -33,49 +33,20 @@ func NewServer(cfg *config.Config, manager *container.Manager) *Server { | |||
| 33 | 33 | } | |
| 34 | 34 | ||
| 35 | 35 | func (s *Server) setupRoutes() { | |
| 36 | - | // Protected Management API endpoints | |
| 37 | - | if s.config.Auth.Enabled { | |
| 38 | - | // Create protected subrouter for API endpoints | |
| 39 | - | apiMux := http.NewServeMux() | |
| 40 | - | apiMux.HandleFunc("GET /api/status", s.handleStatus) | |
| 41 | - | apiMux.HandleFunc("POST /api/reload", s.handleReload) | |
| 42 | - | apiMux.HandleFunc("GET /api/containers", s.handleListContainers) | |
| 43 | - | apiMux.HandleFunc("POST /api/deploy/{route}", s.handleDeploy) | |
| 44 | - | ||
| 45 | - | // Apply authentication middleware to API routes | |
| 46 | - | var authHandler http.Handler = apiMux | |
| 47 | - | ||
| 48 | - | switch s.config.Auth.Method { | |
| 49 | - | case "jwt": | |
| 50 | - | authHandler = middleware.JWTAuth(s.config.Auth.JWTSecret)(authHandler) | |
| 51 | - | case "api_key": | |
| 52 | - | authHandler = middleware.APIKeyAuth(s.config.Auth.APIKey)(authHandler) | |
| 53 | - | case "basic": | |
| 54 | - | authHandler = middleware.BasicAuth(s.config.Auth.Username, s.config.Auth.Password)(authHandler) | |
| 55 | - | } | |
| 56 | - | ||
| 57 | - | // Add IP whitelist if configured | |
| 58 | - | if len(s.config.Auth.AllowedIPs) > 0 { | |
| 59 | - | authHandler = middleware.IPWhitelist(s.config.Auth.AllowedIPs)(authHandler) | |
| 60 | - | } | |
| 61 | - | ||
| 62 | - | s.mux.Handle("/api/", authHandler) | |
| 63 | - | } else { | |
| 64 | - | // Unprotected API endpoints (not recommended for production) | |
| 65 | - | s.mux.HandleFunc("GET /api/status", s.handleStatus) | |
| 66 | - | s.mux.HandleFunc("POST /api/reload", s.handleReload) | |
| 67 | - | s.mux.HandleFunc("GET /api/containers", s.handleListContainers) | |
| 68 | - | s.mux.HandleFunc("POST /api/deploy/{route}", s.handleDeploy) | |
| 69 | - | } | |
| 70 | - | ||
| 71 | - | // Catch-all handler for domain-based routing | |
| 72 | - | // Since Go's ServeMux doesn't support Host matching, we'll handle it in the handler | |
| 36 | + | // Domain-based routing handler | |
| 73 | 37 | s.mux.HandleFunc("/", s.handleDomainRouting) | |
| 74 | 38 | } | |
| 75 | 39 | ||
| 76 | 40 | func (s *Server) handleDomainRouting(w http.ResponseWriter, r *http.Request) { | |
| 77 | 41 | host := r.Host | |
| 78 | 42 | ||
| 43 | + | // Check if this is the registry domain | |
| 44 | + | if s.config.Server.RegistryDomain != "" && host == s.config.Server.RegistryDomain { | |
| 45 | + | log.Info().Str("domain", host).Msg("Routing request to registry") | |
| 46 | + | s.proxyToRegistry(w, r) | |
| 47 | + | return | |
| 48 | + | } | |
| 49 | + | ||
| 79 | 50 | // Find matching route for this domain | |
| 80 | 51 | for _, route := range s.routes { | |
| 81 | 52 | if route.Domain == host { | |
| @@ -96,37 +67,20 @@ func (s *Server) proxyToContainer(w http.ResponseWriter, r *http.Request, route | |||
| 96 | 67 | // Check if container exists and is running | |
| 97 | 68 | container, exists := s.manager.GetContainer(route.Domain) | |
| 98 | 69 | if !exists { | |
| 99 | - | log.Info().Str("domain", route.Domain).Msg("Container not found, deploying") | |
| 100 | - | ||
| 101 | - | // Deploy the container | |
| 102 | - | var err error | |
| 103 | - | container, err = s.manager.DeployContainer(ctx, route) | |
| 104 | - | if err != nil { | |
| 105 | - | log.Error().Err(err).Str("domain", route.Domain).Msg("Failed to deploy container") | |
| 106 | - | http.Error(w, "Service Unavailable", http.StatusServiceUnavailable) | |
| 107 | - | return | |
| 108 | - | } | |
| 70 | + | log.Warn().Str("domain", route.Domain).Msg("No container found for route - container should be auto-deployed on image push") | |
| 71 | + | http.Error(w, "Service Unavailable - Container not deployed", http.StatusServiceUnavailable) | |
| 72 | + | return | |
| 109 | 73 | } | |
| 110 | 74 | ||
| 111 | - | // Try to get container port (try common web ports) | |
| 112 | - | var targetPort int | |
| 113 | - | var err error | |
| 114 | - | ||
| 115 | - | // Try common web ports in order: 80, 8080, 3000 | |
| 116 | - | for _, port := range []int{80, 8080, 3000} { | |
| 117 | - | targetPort, err = s.manager.GetContainerPort(ctx, route.Domain, port) | |
| 118 | - | if err == nil { | |
| 119 | - | break | |
| 120 | - | } | |
| 121 | - | } | |
| 122 | - | ||
| 75 | + | // Get container's internal network info (Traefik-style automatic detection) | |
| 76 | + | containerIP, containerPort, err := s.manager.Runtime().GetContainerNetworkInfo(ctx, container.ID) | |
| 123 | 77 | if err != nil { | |
| 124 | - | log.Error().Err(err).Str("domain", route.Domain).Msg("No accessible port found for container") | |
| 78 | + | log.Error().Err(err).Str("domain", route.Domain).Str("container_id", container.ID).Msg("Failed to get container network info") | |
| 125 | 79 | http.Error(w, "Service Unavailable", http.StatusServiceUnavailable) | |
| 126 | 80 | return | |
| 127 | 81 | } | |
| 128 | 82 | ||
| 129 | - | target, err := url.Parse(fmt.Sprintf("http://localhost:%d", targetPort)) | |
| 83 | + | target, err := url.Parse(fmt.Sprintf("http://%s:%d", containerIP, containerPort)) | |
| 130 | 84 | if err != nil { | |
| 131 | 85 | log.Error().Err(err).Msg("Failed to parse target URL") | |
| 132 | 86 | http.Error(w, "Internal Server Error", http.StatusInternalServerError) | |
| @@ -137,7 +91,7 @@ func (s *Server) proxyToContainer(w http.ResponseWriter, r *http.Request, route | |||
| 137 | 91 | ||
| 138 | 92 | // Customize the proxy to handle errors | |
| 139 | 93 | proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) { | |
| 140 | - | log.Error().Err(err).Str("domain", route.Domain).Int("port", targetPort).Msg("Proxy error") | |
| 94 | + | log.Error().Err(err).Str("domain", route.Domain).Str("target", target.String()).Msg("Proxy error") | |
| 141 | 95 | http.Error(w, "Service Unavailable", http.StatusServiceUnavailable) | |
| 142 | 96 | } | |
| 143 | 97 | ||
| @@ -158,6 +112,40 @@ func (s *Server) proxyToContainer(w http.ResponseWriter, r *http.Request, route | |||
| 158 | 112 | proxy.ServeHTTP(w, r) | |
| 159 | 113 | } | |
| 160 | 114 | ||
| 115 | + | func (s *Server) proxyToRegistry(w http.ResponseWriter, r *http.Request) { | |
| 116 | + | // Proxy to the internal registry server | |
| 117 | + | target, err := url.Parse(fmt.Sprintf("http://localhost:%d", s.config.Server.RegistryPort)) | |
| 118 | + | if err != nil { | |
| 119 | + | log.Error().Err(err).Msg("Failed to parse registry target URL") | |
| 120 | + | http.Error(w, "Internal Server Error", http.StatusInternalServerError) | |
| 121 | + | return | |
| 122 | + | } | |
| 123 | + | ||
| 124 | + | proxy := httputil.NewSingleHostReverseProxy(target) | |
| 125 | + | ||
| 126 | + | // Customize the proxy to handle errors | |
| 127 | + | proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) { | |
| 128 | + | log.Error().Err(err).Int("registry_port", s.config.Server.RegistryPort).Msg("Registry proxy error") | |
| 129 | + | http.Error(w, "Registry Unavailable", http.StatusServiceUnavailable) | |
| 130 | + | } | |
| 131 | + | ||
| 132 | + | // Add custom headers | |
| 133 | + | proxy.ModifyResponse = func(resp *http.Response) error { | |
| 134 | + | resp.Header.Set("X-Proxied-By", "Gordon") | |
| 135 | + | resp.Header.Set("X-Registry-Backend", "gordon-registry") | |
| 136 | + | return nil | |
| 137 | + | } | |
| 138 | + | ||
| 139 | + | log.Debug(). | |
| 140 | + | Str("method", r.Method). | |
| 141 | + | Str("path", r.URL.Path). | |
| 142 | + | Str("domain", s.config.Server.RegistryDomain). | |
| 143 | + | Str("target", target.String()). | |
| 144 | + | Msg("Proxying request to registry") | |
| 145 | + | ||
| 146 | + | proxy.ServeHTTP(w, r) | |
| 147 | + | } | |
| 148 | + | ||
| 161 | 149 | func (s *Server) Start(ctx context.Context) error { | |
| 162 | 150 | addr := ":" + strconv.Itoa(s.config.Server.Port) | |
| 163 | 151 | ||
| @@ -194,93 +182,3 @@ func (s *Server) Start(ctx context.Context) error { | |||
| 194 | 182 | } | |
| 195 | 183 | } | |
| 196 | 184 | ||
| 197 | - | func (s *Server) handleStatus(w http.ResponseWriter, r *http.Request) { | |
| 198 | - | ctx := r.Context() | |
| 199 | - | w.Header().Set("Content-Type", "application/json") | |
| 200 | - | ||
| 201 | - | // Get container health status | |
| 202 | - | health := s.manager.HealthCheck(ctx) | |
| 203 | - | containers := s.manager.ListContainers() | |
| 204 | - | ||
| 205 | - | status := map[string]interface{}{ | |
| 206 | - | "status": "running", | |
| 207 | - | "routes": len(s.routes), | |
| 208 | - | "runtime": s.config.Server.Runtime, | |
| 209 | - | "containers": len(containers), | |
| 210 | - | "health": health, | |
| 211 | - | } | |
| 212 | - | ||
| 213 | - | w.WriteHeader(http.StatusOK) | |
| 214 | - | fmt.Fprintf(w, `{"status":"%s","routes":%d,"runtime":"%s","containers":%d}`, | |
| 215 | - | status["status"], status["routes"], status["runtime"], status["containers"]) | |
| 216 | - | } | |
| 217 | - | ||
| 218 | - | func (s *Server) handleReload(w http.ResponseWriter, r *http.Request) { | |
| 219 | - | log.Info().Msg("Configuration reload requested") | |
| 220 | - | ||
| 221 | - | // TODO: Implement configuration reload | |
| 222 | - | // This should reload the config file and update routes | |
| 223 | - | ||
| 224 | - | w.WriteHeader(http.StatusOK) | |
| 225 | - | fmt.Fprint(w, `{"message":"Configuration reloaded"}`) | |
| 226 | - | } | |
| 227 | - | ||
| 228 | - | func (s *Server) handleListContainers(w http.ResponseWriter, r *http.Request) { | |
| 229 | - | log.Info().Msg("Container list requested") | |
| 230 | - | ||
| 231 | - | containers := s.manager.ListContainers() | |
| 232 | - | ||
| 233 | - | w.Header().Set("Content-Type", "application/json") | |
| 234 | - | w.WriteHeader(http.StatusOK) | |
| 235 | - | ||
| 236 | - | // Simple JSON response - in production would use proper JSON marshaling | |
| 237 | - | fmt.Fprint(w, `{"containers":[`) | |
| 238 | - | first := true | |
| 239 | - | for domain, container := range containers { | |
| 240 | - | if !first { | |
| 241 | - | fmt.Fprint(w, ",") | |
| 242 | - | } | |
| 243 | - | fmt.Fprintf(w, `{"domain":"%s","id":"%s","image":"%s","status":"%s"}`, | |
| 244 | - | domain, container.ID, container.Image, container.Status) | |
| 245 | - | first = false | |
| 246 | - | } | |
| 247 | - | fmt.Fprint(w, `]}`) | |
| 248 | - | } | |
| 249 | - | ||
| 250 | - | func (s *Server) handleDeploy(w http.ResponseWriter, r *http.Request) { | |
| 251 | - | routeName := r.PathValue("route") | |
| 252 | - | if routeName == "" { | |
| 253 | - | http.Error(w, "Route name required", http.StatusBadRequest) | |
| 254 | - | return | |
| 255 | - | } | |
| 256 | - | ||
| 257 | - | log.Info().Str("route", routeName).Msg("Manual deployment requested") | |
| 258 | - | ||
| 259 | - | // Find the route | |
| 260 | - | var targetRoute *config.Route | |
| 261 | - | for _, route := range s.routes { | |
| 262 | - | if route.Domain == routeName { | |
| 263 | - | targetRoute = &route | |
| 264 | - | break | |
| 265 | - | } | |
| 266 | - | } | |
| 267 | - | ||
| 268 | - | if targetRoute == nil { | |
| 269 | - | http.Error(w, "Route not found", http.StatusNotFound) | |
| 270 | - | return | |
| 271 | - | } | |
| 272 | - | ||
| 273 | - | // Deploy the container | |
| 274 | - | ctx := r.Context() | |
| 275 | - | container, err := s.manager.DeployContainer(ctx, *targetRoute) | |
| 276 | - | if err != nil { | |
| 277 | - | log.Error().Err(err).Str("route", routeName).Msg("Deployment failed") | |
| 278 | - | http.Error(w, "Deployment failed", http.StatusInternalServerError) | |
| 279 | - | return | |
| 280 | - | } | |
| 281 | - | ||
| 282 | - | w.Header().Set("Content-Type", "application/json") | |
| 283 | - | w.WriteHeader(http.StatusOK) | |
| 284 | - | fmt.Fprintf(w, `{"message":"Deployed successfully","container":"%s","domain":"%s","image":"%s"}`, | |
| 285 | - | container.ID, targetRoute.Domain, targetRoute.Image) | |
| 286 | - | }⏎̸ | |
| @@ -33,49 +33,20 @@ func NewServer(cfg *config.Config, manager *container.Manager) *Server { | |||||
| 33 | } | 33 | } | ||
| 34 | 34 | ||||
| 35 | func (s *Server) setupRoutes() { | 35 | func (s *Server) setupRoutes() { | ||
| 36 | - | // Protected Management API endpoints | 36 | + | // Domain-based routing handler |
| 37 | - | if s.config.Auth.Enabled { | |||
| 38 | - | // Create protected subrouter for API endpoints | |||
| 39 | - | apiMux := http.NewServeMux() | |||
| 40 | - | apiMux.HandleFunc("GET /api/status", s.handleStatus) | |||
| 41 | - | apiMux.HandleFunc("POST /api/reload", s.handleReload) | |||
| 42 | - | apiMux.HandleFunc("GET /api/containers", s.handleListContainers) | |||
| 43 | - | apiMux.HandleFunc("POST /api/deploy/{route}", s.handleDeploy) | |||
| 44 | - | ||||
| 45 | - | // Apply authentication middleware to API routes | |||
| 46 | - | var authHandler http.Handler = apiMux | |||
| 47 | - | ||||
| 48 | - | switch s.config.Auth.Method { | |||
| 49 | - | case "jwt": | |||
| 50 | - | authHandler = middleware.JWTAuth(s.config.Auth.JWTSecret)(authHandler) | |||
| 51 | - | case "api_key": | |||
| 52 | - | authHandler = middleware.APIKeyAuth(s.config.Auth.APIKey)(authHandler) | |||
| 53 | - | case "basic": | |||
| 54 | - | authHandler = middleware.BasicAuth(s.config.Auth.Username, s.config.Auth.Password)(authHandler) | |||
| 55 | - | } | |||
| 56 | - | ||||
| 57 | - | // Add IP whitelist if configured | |||
| 58 | - | if len(s.config.Auth.AllowedIPs) > 0 { | |||
| 59 | - | authHandler = middleware.IPWhitelist(s.config.Auth.AllowedIPs)(authHandler) | |||
| 60 | - | } | |||
| 61 | - | ||||
| 62 | - | s.mux.Handle("/api/", authHandler) | |||
| 63 | - | } else { | |||
| 64 | - | // Unprotected API endpoints (not recommended for production) | |||
| 65 | - | s.mux.HandleFunc("GET /api/status", s.handleStatus) | |||
| 66 | - | s.mux.HandleFunc("POST /api/reload", s.handleReload) | |||
| 67 | - | s.mux.HandleFunc("GET /api/containers", s.handleListContainers) | |||
| 68 | - | s.mux.HandleFunc("POST /api/deploy/{route}", s.handleDeploy) | |||
| 69 | - | } | |||
| 70 | - | ||||
| 71 | - | // Catch-all handler for domain-based routing | |||
| 72 | - | // Since Go's ServeMux doesn't support Host matching, we'll handle it in the handler | |||
| 73 | s.mux.HandleFunc("/", s.handleDomainRouting) | 37 | s.mux.HandleFunc("/", s.handleDomainRouting) | ||
| 74 | } | 38 | } | ||
| 75 | 39 | ||||
| 76 | func (s *Server) handleDomainRouting(w http.ResponseWriter, r *http.Request) { | 40 | func (s *Server) handleDomainRouting(w http.ResponseWriter, r *http.Request) { | ||
| 77 | host := r.Host | 41 | host := r.Host | ||
| 78 | 42 | ||||
| 43 | + | // Check if this is the registry domain | |||
| 44 | + | if s.config.Server.RegistryDomain != "" && host == s.config.Server.RegistryDomain { | |||
| 45 | + | log.Info().Str("domain", host).Msg("Routing request to registry") | |||
| 46 | + | s.proxyToRegistry(w, r) | |||
| 47 | + | return | |||
| 48 | + | } | |||
| 49 | + | ||||
| 79 | // Find matching route for this domain | 50 | // Find matching route for this domain | ||
| 80 | for _, route := range s.routes { | 51 | for _, route := range s.routes { | ||
| 81 | if route.Domain == host { | 52 | if route.Domain == host { | ||
| @@ -96,37 +67,20 @@ func (s *Server) proxyToContainer(w http.ResponseWriter, r *http.Request, route | |||||
| 96 | // Check if container exists and is running | 67 | // Check if container exists and is running | ||
| 97 | container, exists := s.manager.GetContainer(route.Domain) | 68 | container, exists := s.manager.GetContainer(route.Domain) | ||
| 98 | if !exists { | 69 | if !exists { | ||
| 99 | - | log.Info().Str("domain", route.Domain).Msg("Container not found, deploying") | 70 | + | log.Warn().Str("domain", route.Domain).Msg("No container found for route - container should be auto-deployed on image push") |
| 100 | - | 71 | + | http.Error(w, "Service Unavailable - Container not deployed", http.StatusServiceUnavailable) | |
| 101 | - | // Deploy the container | 72 | + | return |
| 102 | - | var err error | |||
| 103 | - | container, err = s.manager.DeployContainer(ctx, route) | |||
| 104 | - | if err != nil { | |||
| 105 | - | log.Error().Err(err).Str("domain", route.Domain).Msg("Failed to deploy container") | |||
| 106 | - | http.Error(w, "Service Unavailable", http.StatusServiceUnavailable) | |||
| 107 | - | return | |||
| 108 | - | } | |||
| 109 | } | 73 | } | ||
| 110 | 74 | ||||
| 111 | - | // Try to get container port (try common web ports) | 75 | + | // Get container's internal network info (Traefik-style automatic detection) |
| 112 | - | var targetPort int | 76 | + | containerIP, containerPort, err := s.manager.Runtime().GetContainerNetworkInfo(ctx, container.ID) |
| 113 | - | var err error | |||
| 114 | - | ||||
| 115 | - | // Try common web ports in order: 80, 8080, 3000 | |||
| 116 | - | for _, port := range []int{80, 8080, 3000} { | |||
| 117 | - | targetPort, err = s.manager.GetContainerPort(ctx, route.Domain, port) | |||
| 118 | - | if err == nil { | |||
| 119 | - | break | |||
| 120 | - | } | |||
| 121 | - | } | |||
| 122 | - | ||||
| 123 | if err != nil { | 77 | if err != nil { | ||
| 124 | - | log.Error().Err(err).Str("domain", route.Domain).Msg("No accessible port found for container") | 78 | + | log.Error().Err(err).Str("domain", route.Domain).Str("container_id", container.ID).Msg("Failed to get container network info") |
| 125 | http.Error(w, "Service Unavailable", http.StatusServiceUnavailable) | 79 | http.Error(w, "Service Unavailable", http.StatusServiceUnavailable) | ||
| 126 | return | 80 | return | ||
| 127 | } | 81 | } | ||
| 128 | 82 | ||||
| 129 | - | target, err := url.Parse(fmt.Sprintf("http://localhost:%d", targetPort)) | 83 | + | target, err := url.Parse(fmt.Sprintf("http://%s:%d", containerIP, containerPort)) |
| 130 | if err != nil { | 84 | if err != nil { | ||
| 131 | log.Error().Err(err).Msg("Failed to parse target URL") | 85 | log.Error().Err(err).Msg("Failed to parse target URL") | ||
| 132 | http.Error(w, "Internal Server Error", http.StatusInternalServerError) | 86 | http.Error(w, "Internal Server Error", http.StatusInternalServerError) | ||
| @@ -137,7 +91,7 @@ func (s *Server) proxyToContainer(w http.ResponseWriter, r *http.Request, route | |||||
| 137 | 91 | ||||
| 138 | // Customize the proxy to handle errors | 92 | // Customize the proxy to handle errors | ||
| 139 | proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) { | 93 | proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) { | ||
| 140 | - | log.Error().Err(err).Str("domain", route.Domain).Int("port", targetPort).Msg("Proxy error") | 94 | + | log.Error().Err(err).Str("domain", route.Domain).Str("target", target.String()).Msg("Proxy error") |
| 141 | http.Error(w, "Service Unavailable", http.StatusServiceUnavailable) | 95 | http.Error(w, "Service Unavailable", http.StatusServiceUnavailable) | ||
| 142 | } | 96 | } | ||
| 143 | 97 | ||||
| @@ -158,6 +112,40 @@ func (s *Server) proxyToContainer(w http.ResponseWriter, r *http.Request, route | |||||
| 158 | proxy.ServeHTTP(w, r) | 112 | proxy.ServeHTTP(w, r) | ||
| 159 | } | 113 | } | ||
| 160 | 114 | ||||
| 115 | + | func (s *Server) proxyToRegistry(w http.ResponseWriter, r *http.Request) { | |||
| 116 | + | // Proxy to the internal registry server | |||
| 117 | + | target, err := url.Parse(fmt.Sprintf("http://localhost:%d", s.config.Server.RegistryPort)) | |||
| 118 | + | if err != nil { | |||
| 119 | + | log.Error().Err(err).Msg("Failed to parse registry target URL") | |||
| 120 | + | http.Error(w, "Internal Server Error", http.StatusInternalServerError) | |||
| 121 | + | return | |||
| 122 | + | } | |||
| 123 | + | ||||
| 124 | + | proxy := httputil.NewSingleHostReverseProxy(target) | |||
| 125 | + | ||||
| 126 | + | // Customize the proxy to handle errors | |||
| 127 | + | proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) { | |||
| 128 | + | log.Error().Err(err).Int("registry_port", s.config.Server.RegistryPort).Msg("Registry proxy error") | |||
| 129 | + | http.Error(w, "Registry Unavailable", http.StatusServiceUnavailable) | |||
| 130 | + | } | |||
| 131 | + | ||||
| 132 | + | // Add custom headers | |||
| 133 | + | proxy.ModifyResponse = func(resp *http.Response) error { | |||
| 134 | + | resp.Header.Set("X-Proxied-By", "Gordon") | |||
| 135 | + | resp.Header.Set("X-Registry-Backend", "gordon-registry") | |||
| 136 | + | return nil | |||
| 137 | + | } | |||
| 138 | + | ||||
| 139 | + | log.Debug(). | |||
| 140 | + | Str("method", r.Method). | |||
| 141 | + | Str("path", r.URL.Path). | |||
| 142 | + | Str("domain", s.config.Server.RegistryDomain). | |||
| 143 | + | Str("target", target.String()). | |||
| 144 | + | Msg("Proxying request to registry") | |||
| 145 | + | ||||
| 146 | + | proxy.ServeHTTP(w, r) | |||
| 147 | + | } | |||
| 148 | + | ||||
| 161 | func (s *Server) Start(ctx context.Context) error { | 149 | func (s *Server) Start(ctx context.Context) error { | ||
| 162 | addr := ":" + strconv.Itoa(s.config.Server.Port) | 150 | addr := ":" + strconv.Itoa(s.config.Server.Port) | ||
| 163 | 151 | ||||
| @@ -194,93 +182,3 @@ func (s *Server) Start(ctx context.Context) error { | |||||
| 194 | } | 182 | } | ||
| 195 | } | 183 | } | ||
| 196 | 184 | ||||
| 197 | - | func (s *Server) handleStatus(w http.ResponseWriter, r *http.Request) { | |||
| 198 | - | ctx := r.Context() | |||
| 199 | - | w.Header().Set("Content-Type", "application/json") | |||
| 200 | - | ||||
| 201 | - | // Get container health status | |||
| 202 | - | health := s.manager.HealthCheck(ctx) | |||
| 203 | - | containers := s.manager.ListContainers() | |||
| 204 | - | ||||
| 205 | - | status := map[string]interface{}{ | |||
| 206 | - | "status": "running", | |||
| 207 | - | "routes": len(s.routes), | |||
| 208 | - | "runtime": s.config.Server.Runtime, | |||
| 209 | - | "containers": len(containers), | |||
| 210 | - | "health": health, | |||
| 211 | - | } | |||
| 212 | - | ||||
| 213 | - | w.WriteHeader(http.StatusOK) | |||
| 214 | - | fmt.Fprintf(w, `{"status":"%s","routes":%d,"runtime":"%s","containers":%d}`, | |||
| 215 | - | status["status"], status["routes"], status["runtime"], status["containers"]) | |||
| 216 | - | } | |||
| 217 | - | ||||
| 218 | - | func (s *Server) handleReload(w http.ResponseWriter, r *http.Request) { | |||
| 219 | - | log.Info().Msg("Configuration reload requested") | |||
| 220 | - | ||||
| 221 | - | // TODO: Implement configuration reload | |||
| 222 | - | // This should reload the config file and update routes | |||
| 223 | - | ||||
| 224 | - | w.WriteHeader(http.StatusOK) | |||
| 225 | - | fmt.Fprint(w, `{"message":"Configuration reloaded"}`) | |||
| 226 | - | } | |||
| 227 | - | ||||
| 228 | - | func (s *Server) handleListContainers(w http.ResponseWriter, r *http.Request) { | |||
| 229 | - | log.Info().Msg("Container list requested") | |||
| 230 | - | ||||
| 231 | - | containers := s.manager.ListContainers() | |||
| 232 | - | ||||
| 233 | - | w.Header().Set("Content-Type", "application/json") | |||
| 234 | - | w.WriteHeader(http.StatusOK) | |||
| 235 | - | ||||
| 236 | - | // Simple JSON response - in production would use proper JSON marshaling | |||
| 237 | - | fmt.Fprint(w, `{"containers":[`) | |||
| 238 | - | first := true | |||
| 239 | - | for domain, container := range containers { | |||
| 240 | - | if !first { | |||
| 241 | - | fmt.Fprint(w, ",") | |||
| 242 | - | } | |||
| 243 | - | fmt.Fprintf(w, `{"domain":"%s","id":"%s","image":"%s","status":"%s"}`, | |||
| 244 | - | domain, container.ID, container.Image, container.Status) | |||
| 245 | - | first = false | |||
| 246 | - | } | |||
| 247 | - | fmt.Fprint(w, `]}`) | |||
| 248 | - | } | |||
| 249 | - | ||||
| 250 | - | func (s *Server) handleDeploy(w http.ResponseWriter, r *http.Request) { | |||
| 251 | - | routeName := r.PathValue("route") | |||
| 252 | - | if routeName == "" { | |||
| 253 | - | http.Error(w, "Route name required", http.StatusBadRequest) | |||
| 254 | - | return | |||
| 255 | - | } | |||
| 256 | - | ||||
| 257 | - | log.Info().Str("route", routeName).Msg("Manual deployment requested") | |||
| 258 | - | ||||
| 259 | - | // Find the route | |||
| 260 | - | var targetRoute *config.Route | |||
| 261 | - | for _, route := range s.routes { | |||
| 262 | - | if route.Domain == routeName { | |||
| 263 | - | targetRoute = &route | |||
| 264 | - | break | |||
| 265 | - | } | |||
| 266 | - | } | |||
| 267 | - | ||||
| 268 | - | if targetRoute == nil { | |||
| 269 | - | http.Error(w, "Route not found", http.StatusNotFound) | |||
| 270 | - | return | |||
| 271 | - | } | |||
| 272 | - | ||||
| 273 | - | // Deploy the container | |||
| 274 | - | ctx := r.Context() | |||
| 275 | - | container, err := s.manager.DeployContainer(ctx, *targetRoute) | |||
| 276 | - | if err != nil { | |||
| 277 | - | log.Error().Err(err).Str("route", routeName).Msg("Deployment failed") | |||
| 278 | - | http.Error(w, "Deployment failed", http.StatusInternalServerError) | |||
| 279 | - | return | |||
| 280 | - | } | |||
| 281 | - | ||||
| 282 | - | w.Header().Set("Content-Type", "application/json") | |||
| 283 | - | w.WriteHeader(http.StatusOK) | |||
| 284 | - | fmt.Fprintf(w, `{"message":"Deployed successfully","container":"%s","domain":"%s","image":"%s"}`, | |||
| 285 | - | container.ID, targetRoute.Domain, targetRoute.Image) | |||
| 286 | - | }⏎̸ | |||
M pkg/runtime/interface.go +5 -0
| @@ -53,4 +53,9 @@ type Runtime interface { | |||
| 53 | 53 | // Health and status | |
| 54 | 54 | IsContainerRunning(ctx context.Context, containerID string) (bool, error) | |
| 55 | 55 | GetContainerPort(ctx context.Context, containerID string, internalPort int) (int, error) | |
| 56 | + | ||
| 57 | + | // Image and port inspection | |
| 58 | + | GetImageExposedPorts(ctx context.Context, imageRef string) ([]int, error) | |
| 59 | + | GetContainerExposedPorts(ctx context.Context, containerID string) ([]int, error) | |
| 60 | + | GetContainerNetworkInfo(ctx context.Context, containerID string) (string, int, error) | |
| 56 | 61 | }⏎̸ | |
| @@ -53,4 +53,9 @@ type Runtime interface { | |||||
| 53 | // Health and status | 53 | // Health and status | ||
| 54 | IsContainerRunning(ctx context.Context, containerID string) (bool, error) | 54 | IsContainerRunning(ctx context.Context, containerID string) (bool, error) | ||
| 55 | GetContainerPort(ctx context.Context, containerID string, internalPort int) (int, error) | 55 | GetContainerPort(ctx context.Context, containerID string, internalPort int) (int, error) | ||
| 56 | + | ||||
| 57 | + | // Image and port inspection | |||
| 58 | + | GetImageExposedPorts(ctx context.Context, imageRef string) ([]int, error) | |||
| 59 | + | GetContainerExposedPorts(ctx context.Context, containerID string) ([]int, error) | |||
| 60 | + | GetContainerNetworkInfo(ctx context.Context, containerID string) (string, int, error) | |||
| 56 | }⏎̸ | 61 | }⏎̸ | ||