Each server runs on a separate TCP port with its own http.Server instance and request multiplexer (http.ServeMux). This provides complete isolation:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// In internal/daemon/http_server.gofunc(s*HTTPServer)Start(ctxcontext.Context)error{// Bind separate portsdocsListener:=net.Listen("tcp",":8080")webhookListener:=net.Listen("tcp",":8081")adminListener:=net.Listen("tcp",":8082")// Start independent serverss.docsServer=&http.Server{Handler:docsHandler}s.webhookServer=&http.Server{Handler:webhookHandler}s.adminServer=&http.Server{Handler:adminHandler}gos.docsServer.Serve(docsListener)gos.webhookServer.Serve(webhookListener)gos.adminServer.Serve(adminListener)}
Collision Probability: 0% - Requests to different ports go to completely different HTTP servers.
Layer 2: Path Prefixing (Secondary Defense)
Even if servers were combined (they’re not), webhook paths use reserved prefixes:
/webhooks/github
/webhooks/gitlab
/webhooks/forgejo
/webhook (generic)
These paths are unlikely to exist in Hugo documentation because:
Hugo content typically lives in /docs/, /blog/, etc.
The /webhooks/ prefix is API-specific, not documentation content
Hugo wouldn’t generate these exact paths without explicit configuration
Layer 3: HTTP Method Filtering (Tertiary Defense)
Webhook handlers only accept POST requests:
1
2
3
4
5
6
7
func(h*WebhookHandlers)HandleGitHubWebhook(whttp.ResponseWriter,r*http.Request){ifr.Method!=http.MethodPost{// Return 405 Method Not Allowedreturn}// Process webhook...}
Documentation requests use GET, so even if a collision occurred:
GET /webhooks/github → Documentation server (404 or docs file)
POST /webhooks/github → Webhook server (webhook handler)
Layer 4: Configuration Validation (Preventive)
Port binding validation happens at startup:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Pre-bind all ports before starting any serversbinds:=[]preBind{{name:"docs",port:config.Daemon.HTTP.DocsPort},{name:"webhook",port:config.Daemon.HTTP.WebhookPort},{name:"admin",port:config.Daemon.HTTP.AdminPort},}for_,bind:=rangebinds{ln,err:=net.Listen("tcp",fmt.Sprintf(":%d",bind.port))iferr!=nil{returnfmt.Errorf("%s port %d: %w",bind.name,bind.port,err)}bind.ln=ln}
If any port is already in use or if two services try to use the same port, the daemon fails to start with a clear error message.
Additional Safeguards
1. Port Conflict Detection
DocBuilder validates that all configured ports are unique:
1
2
3
4
5
6
daemon:http:docs_port:8080webhook_port:8081# Must differ from docs_portadmin_port:8082# Must differ from both abovelivereload_port:8083# Must differ from all above
If you accidentally configure the same port twice, the daemon will fail to start.
2. Firewall Recommendations
For production deployments, use firewall rules to restrict access:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Allow public access to docsiptables -A INPUT -p tcp --dport 8080 -j ACCEPT
# Restrict webhook port to forge IPs onlyiptables -A INPUT -p tcp --dport 8081 -s 140.82.112.0/20 -j ACCEPT # GitHubiptables -A INPUT -p tcp --dport 8081 -s 192.30.252.0/22 -j ACCEPT # GitHub# ... add GitLab/Forgejo IPs# Restrict admin port to internal networkiptables -A INPUT -p tcp --dport 8082 -s 10.0.0.0/8 -j ACCEPT
# Block all other access to webhook/adminiptables -A INPUT -p tcp --dport 8081 -j DROP
iptables -A INPUT -p tcp --dport 8082 -j DROP
3. Reverse Proxy Path Segregation
When using a reverse proxy (nginx, Traefik, Caddy), use different subdomains or paths:
server{server_nameexample.com;# Documentation at root
location/{proxy_passhttp://localhost:8080;}# Webhooks at /api/webhooks
location/api/webhooks/{proxy_passhttp://localhost:8081/webhooks/;}# Admin at /api/admin
location/api/admin/{allow10.0.0.0/8;denyall;proxy_passhttp://localhost:8082/api/;}}
4. Content Security Policy
For defense in depth, the docs server could set CSP headers to prevent accidental form submissions to webhook paths:
# Start daemon./docbuilder daemon
# Verify docs server respondscurl http://localhost:8080/
# Expected: 200 OK (documentation)# Verify webhook server responds (POST only)curl http://localhost:8081/webhooks/github
# Expected: 405 Method Not Allowedcurl -X POST http://localhost:8081/webhooks/github
# Expected: 400 Bad Request (no payload) or 401 (no signature)# Verify docs server doesn't handle webhookscurl -X POST http://localhost:8080/webhooks/github
# Expected: 404 Not Found (no such documentation page)
Port Conflict Testing
1
2
3
4
5
6
# Occupy port 8081nc -l 8081&# Try to start DocBuilder./docbuilder daemon
# Expected: Error: "webhook port 8081: address already in use"
Configuration Best Practices
✅ Recommended: Default Ports
1
2
3
4
5
6
daemon:http:docs_port:8080# Standard HTTP alternative portwebhook_port:8081# Sequential, clearly webhook-relatedadmin_port:8082# Sequential, clearly admin-relatedlivereload_port:8083# Sequential, optional feature
✅ Recommended: Custom Ports with Separation
1
2
3
4
5
6
daemon:http:docs_port:3000# Custom docs portwebhook_port:3001# Different from docsadmin_port:3002# Different from bothlivereload_port:3003# Different from all
❌ Never: Same Ports
1
2
3
4
5
daemon:http:docs_port:8080webhook_port:8080# ❌ WILL FAIL TO STARTadmin_port:8080# ❌ WILL FAIL TO START
⚠️ Caution: Non-Sequential Ports
1
2
3
4
5
daemon:http:docs_port:8080webhook_port:9443# ⚠️ Works but non-obvious relationshipadmin_port:3000# ⚠️ Works but confusing
Monitoring and Validation
Startup Validation
Watch daemon logs for port binding confirmation:
INFO HTTP servers binding to ports docs_port=8080 webhook_port=8081 admin_port=8082
INFO Documentation server started on :8080
INFO Webhook server started on :8081
INFO Admin server started on :8082
If you see errors:
ERROR http startup failed: webhook port 8081: address already in use
This indicates a port conflict that must be resolved before the daemon can start.
Runtime Health Checks
1
2
3
4
5
6
7
8
# Check all servers are respondingcurl -f http://localhost:8080/health # Docs servercurl -f http://localhost:8082/health # Admin server# Webhook server doesn't have health endpoint (POST-only)# Use netstat instead:netstat -an | grep :8081
# Expected: LISTEN state