detached d99e74e

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() {
3131 if cfgFile != "" {
3232 viper.SetConfigFile(cfgFile)
3333 } 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")
3555 }
3656
3757 viper.AutomaticEnv()
@@ -39,6 +59,10 @@ func initConfig() {
3959 if err := viper.ReadInConfig(); err == nil {
4060 fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed())
4161 } 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+ }
4367 }
4468 }
@@ -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 (
88 "syscall"
99 "time"
1010
11+ "github.com/fsnotify/fsnotify"
12+ "github.com/google/uuid"
1113 "github.com/rs/zerolog"
1214 "github.com/rs/zerolog/log"
1315 "github.com/spf13/cobra"
16+ "github.com/spf13/viper"
1417 "gordon/internal/config"
1518 "gordon/internal/container"
1619 "gordon/internal/events"
@@ -62,6 +65,45 @@ func runStart(cmd *cobra.Command, args []string) {
6265 log.Fatal().Err(err).Msg("Failed to subscribe container event handler")
6366 }
6467
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+
65107 // Sync existing containers
66108 ctx, cancel := context.WithCancel(context.Background())
67109 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 containers107 // 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 (
99 github.com/spf13/cobra v1.9.1
1010 github.com/spf13/viper v1.20.1
1111 )
12-
13-require github.com/google/uuid v1.6.0 // indirect
14-
1512 require (
1613 github.com/Microsoft/go-winio v0.4.14 // indirect
1714 github.com/containerd/errdefs v1.0.0 // indirect
@@ -20,12 +17,12 @@ require (
2017 github.com/distribution/reference v0.6.0 // indirect
2118 github.com/docker/go-units v0.5.0 // indirect
2219 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
2421 github.com/go-logr/logr v1.4.2 // indirect
2522 github.com/go-logr/stdr v1.2.2 // indirect
2623 github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
2724 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
2926 github.com/inconshreveable/mousetrap v1.1.0 // indirect
3027 github.com/mattn/go-colorable v0.1.13 // indirect
3128 github.com/mattn/go-isatty v0.0.19 // indirect
@@ -9,9 +9,6 @@ require (
9 github.com/spf13/cobra v1.9.19 github.com/spf13/cobra v1.9.1
10 github.com/spf13/viper v1.20.110 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 // indirect13 github.com/Microsoft/go-winio v0.4.14 // indirect
17 github.com/containerd/errdefs v1.0.0 // indirect14 github.com/containerd/errdefs v1.0.0 // indirect
@@ -20,12 +17,12 @@ require (
20 github.com/distribution/reference v0.6.0 // indirect17 github.com/distribution/reference v0.6.0 // indirect
21 github.com/docker/go-units v0.5.0 // indirect18 github.com/docker/go-units v0.5.0 // indirect
22 github.com/felixge/httpsnoop v1.0.4 // indirect19 github.com/felixge/httpsnoop v1.0.4 // indirect
23- github.com/fsnotify/fsnotify v1.8.0 // indirect20+ github.com/fsnotify/fsnotify v1.8.0 // direct
24 github.com/go-logr/logr v1.4.2 // indirect21 github.com/go-logr/logr v1.4.2 // indirect
25 github.com/go-logr/stdr v1.2.2 // indirect22 github.com/go-logr/stdr v1.2.2 // indirect
26 github.com/go-viper/mapstructure/v2 v2.2.1 // indirect23 github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
27 github.com/gogo/protobuf v1.3.2 // indirect24 github.com/gogo/protobuf v1.3.2 // indirect
28- github.com/golang-jwt/jwt/v5 v5.2.2 // direct25+ github.com/google/uuid v1.6.0 // direct
29 github.com/inconshreveable/mousetrap v1.1.0 // indirect26 github.com/inconshreveable/mousetrap v1.1.0 // indirect
30 github.com/mattn/go-colorable v0.1.13 // indirect27 github.com/mattn/go-colorable v0.1.13 // indirect
31 github.com/mattn/go-isatty v0.0.19 // indirect28 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+
14 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
711 }
812
913 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!)
1317 }
1418
19+# Domain to container image mapping
20+# When you push an image matching these routes, Gordon auto-deploys
1521 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 = 80805+ port = 8080 # Main proxy port
3- registry_port = 50006+ 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 = true14+ 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+
14 [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
711
812 [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!)
1216
17+# Domain to container image mapping
18+# When you push an image matching these routes, Gordon auto-deploys
1319 [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 = 80805+port = 8080 # Main proxy port
3-registry_port = 50006+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 = true13+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+
14 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
711
812 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!)
1216
17+# Domain to container image mapping
18+# When you push an image matching these routes, Gordon auto-deploys
1319 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: 80805+ port: 8080 # Main proxy port
3- registry_port: 50006+ registry_port: 5000 # Internal registry port
4- runtime: docker7+ registry_domain: "registry.example.com" # Registry subdomain (NEW!)
5- ssl_email: admin@example.com8+ runtime: docker # Container runtime
6- data_dir: ./data9+ 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: true13+ enabled: true # Enable registry authentication
10- username: gordon14+ username: admin # Registry username
11- password: secret12315+ 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 {
1414 }
1515
1616 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"`
2223 }
2324
2425 type RegistryAuthConfig struct {
@@ -84,6 +85,14 @@ func Load() (*Config, error) {
8485 }
8586 }
8687
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+
8796 return &cfg, nil
8897 }
8998
@@ -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, nil96 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
310310 }
311311
312312 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
313425 }⏎̸
@@ -310,4 +310,116 @@ func (d *DockerRuntime) GetContainerPort(ctx context.Context, containerID string
310 }310 }
311 311
312 return hostPort, nil312 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
8484 return nil, fmt.Errorf("failed to pull image %s: %w", route.Image, err)
8585 }
8686
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+
8794 // Create container configuration
8895 containerConfig := &runtime.ContainerConfig{
8996 Image: route.Image,
9097 Name: fmt.Sprintf("gordon-%s", route.Domain),
91- Ports: []int{80, 8080, 3000}, // Common web server ports
98+ Ports: exposedPorts,
9299 Labels: map[string]string{
93100 "gordon.domain": route.Domain,
94101 "gordon.image": route.Image,
95102 "gordon.managed": "true",
103+ "gordon.route": route.Domain,
96104 },
97105 AutoRemove: false, // Keep containers for inspection
98106 }
@@ -164,43 +172,86 @@ func (m *Manager) ListContainers() map[string]*runtime.Container {
164172 return result
165173 }
166174
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()
171179
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+ }
175195 }
196+ return "", false
197+}
176198
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)
179203 }
180204
181- log.Info().Str("domain", domain).Str("container", container.ID).Msg("Container stopped")
205+ log.Info().Str("container", containerID).Msg("Container stopped")
182206 return nil
183207 }
184208
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()
190212 container, exists := m.containers[domain]
213+ m.mu.RUnlock()
214+
191215 if !exists {
192216 return fmt.Errorf("no container found for domain %s", domain)
193217 }
194218
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)
197226 }
198227
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+
201239 return nil
202240 }
203241
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+
204255 // SyncContainers synchronizes the internal state with the actual running containers
205256 func (m *Manager) SyncContainers(ctx context.Context) error {
206257 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 configuration94 // 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 ports98+ 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 inspection105 AutoRemove: false, // Keep containers for inspection
98 }106 }
@@ -164,43 +172,86 @@ func (m *Manager) ListContainers() map[string]*runtime.Container {
164 return result172 return result
165 }173 }
166 174
167-// StopContainer stops a container for a domain175+// 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 nil206 return nil
183 }207 }
184 208
185-// RemoveContainer removes a container for a domain209+// 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 nil239 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 containers255 // 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 {
9595 ctx := context.Background()
9696
9797 // 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()
10299
103100 // Track which routes are currently active
104101 activeRoutes := make(map[string]*runtime.Container)
@@ -200,12 +197,9 @@ func (h *ContainerEventHandler) handleContainerStart(event Event) error {
200197 Str("event_id", event.ID).
201198 Msg("Processing container start event")
202199
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")
209203 }
210204
211205 func (h *ContainerEventHandler) findRoutesForImage(imageName string) []string {
@@ -223,10 +217,7 @@ func (h *ContainerEventHandler) findRoutesForImage(imageName string) []string {
223217 func (h *ContainerEventHandler) deployContainerForRoute(route, imageName string) error {
224218 ctx := context.Background()
225219
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()
230221
231222 containerName := fmt.Sprintf("gordon-%s", strings.ReplaceAll(route, ".", "-"))
232223
@@ -254,24 +245,16 @@ func (h *ContainerEventHandler) deployContainerForRoute(route, imageName string)
254245 }
255246 }
256247
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,
266253 }
267254
268- container, err := h.manager.CreateContainer(ctx, containerConfig)
255+ container, err := h.manager.DeployContainer(ctx, routeConfig)
269256 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)
275258 }
276259
277260 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 Gordon97 // 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 active100 // 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
33 import (
44 "crypto/subtle"
55 "net/http"
6- "strings"
76
87 "github.com/rs/zerolog/log"
98 )
@@ -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 {
3333 }
3434
3535 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
7337 s.mux.HandleFunc("/", s.handleDomainRouting)
7438 }
7539
7640 func (s *Server) handleDomainRouting(w http.ResponseWriter, r *http.Request) {
7741 host := r.Host
7842
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+
7950 // Find matching route for this domain
8051 for _, route := range s.routes {
8152 if route.Domain == host {
@@ -96,37 +67,20 @@ func (s *Server) proxyToContainer(w http.ResponseWriter, r *http.Request, route
9667 // Check if container exists and is running
9768 container, exists := s.manager.GetContainer(route.Domain)
9869 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
10973 }
11074
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)
12377 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")
12579 http.Error(w, "Service Unavailable", http.StatusServiceUnavailable)
12680 return
12781 }
12882
129- target, err := url.Parse(fmt.Sprintf("http://localhost:%d", targetPort))
83+ target, err := url.Parse(fmt.Sprintf("http://%s:%d", containerIP, containerPort))
13084 if err != nil {
13185 log.Error().Err(err).Msg("Failed to parse target URL")
13286 http.Error(w, "Internal Server Error", http.StatusInternalServerError)
@@ -137,7 +91,7 @@ func (s *Server) proxyToContainer(w http.ResponseWriter, r *http.Request, route
13791
13892 // Customize the proxy to handle errors
13993 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")
14195 http.Error(w, "Service Unavailable", http.StatusServiceUnavailable)
14296 }
14397
@@ -158,6 +112,40 @@ func (s *Server) proxyToContainer(w http.ResponseWriter, r *http.Request, route
158112 proxy.ServeHTTP(w, r)
159113 }
160114
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+
161149 func (s *Server) Start(ctx context.Context) error {
162150 addr := ":" + strconv.Itoa(s.config.Server.Port)
163151
@@ -194,93 +182,3 @@ func (s *Server) Start(ctx context.Context) error {
194182 }
195183 }
196184
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 endpoints36+ // 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.Host41 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 domain50 // 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 running67 // 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 container72+ 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 int76+ 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 return80 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 errors92 // 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 {
5353 // Health and status
5454 IsContainerRunning(ctx context.Context, containerID string) (bool, error)
5555 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)
5661 }⏎̸
@@ -53,4 +53,9 @@ type Runtime interface {
53 // Health and status53 // 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 }⏎̸

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close