Routes & HTTPS
HTTP vs. L4 Routing
GateControl supports two fundamentally different routing types:
- HTTP Routing (Layer 7): Caddy analyzes the HTTP request, matches by domain name, and forwards to the backend. Full access to all HTTP features.
- L4 Routing (Layer 4): Caddy forwards raw TCP/UDP traffic without inspecting the content. Matching by port, not by domain.
HTTP Routing
Client → https://app.example.com → Caddy (matches domain "app.example.com") → Backend 10.8.0.3:8080
Client → https://api.example.com → Caddy (matches domain "api.example.com") → Backend 10.8.0.4:3000
↑ Both on port 443, distinguished by domain
L4 Routing
Client → server.example.com:25565 → Caddy (matches port 25565) → Backend 10.8.0.5:25565
Client → server.example.com:2222 → Caddy (matches port 2222) → Backend 10.8.0.6:22
↑ Different ports, domain doesn't matter
HTTP Routing (Layer 7) in Detail
HTTP routing is the default mode. Caddy terminates TLS, reads HTTP headers, and matches based on the Host header.
How it works:
- Client connects to port 443 (or 80)
- TLS handshake — Caddy selects the correct certificate via SNI
- Caddy reads the HTTP
Hostheader - Match on configured domain → forward to backend
- Full HTTP processing: header manipulation, compression, auth, etc.
All route features are available:
- Force HTTPS with Let's Encrypt
- Backend HTTPS
- Compression (Gzip/Zstd)
- Rate Limiting
- Basic Auth / Route Auth
- Peer ACL
- IP Access Control
- Request Mirroring
- Retry on Error
- Circuit Breaker
- Custom Headers
- Load Balancing (multiple backends)
- Uptime Monitoring (HTTP Check)
- Sticky Sessions
L4 Routing (Layer 4) in Detail
L4 routing forwards raw TCP or UDP traffic. Caddy opens a dedicated port and tunnels traffic to the backend.
Protocol: TCP or UDP
| Protocol | Use Cases |
|---|---|
| TCP | SSH, Minecraft, SMTP, databases, most services |
| UDP | DNS, game servers (some), VoIP, WireGuard |
Listen Port
The port Caddy opens on the GateControl server. This is the port clients connect to.
- Not the target port (the port on the backend)
- Can be the same or different from the target port
- Must be available on the GateControl server
TLS Mode
| Mode | Description | Caddy Behavior |
|---|---|---|
| None | No TLS | Caddy forwards raw TCP/UDP traffic |
| Passthrough | TLS pass-through | Caddy matches via SNI, forwards encrypted traffic without decrypting |
| Terminate | TLS termination | Caddy decrypts TLS (with LE certificate), then forwards unencrypted TCP to backend |
Target Port vs. Listen Port
| Field | Applies to | Description |
|---|---|---|
| Target Port | All routes (HTTP + L4) | The port on the backend peer where the service runs |
| Listen Port | L4 routes only | The port Caddy opens on the GateControl server |
Example 1: Same ports
Listen Port 25565 (GateControl) → Target Port 25565 (Minecraft on Peer 10.8.0.4)
Client connects to: server.example.com:25565
Example 2: Different ports
Listen Port 8022 (GateControl) → Target Port 22 (SSH on Peer 10.8.0.2)
Client connects to: server.example.com:8022
SSH command: ssh -p 8022 user@server.example.com
Example 3: Multiple services, different ports
Listen Port 25565 → Target Port 25565 (Minecraft on 10.8.0.4)
Listen Port 2222 → Target Port 22 (SSH on 10.8.0.2)
Listen Port 5433 → Target Port 5432 (PostgreSQL on 10.8.0.3)
HTTP routes don't have a listen port — all HTTP routes share ports 80/443 and are distinguished by domain.
TLS Modes in Detail
None — No TLS
Client ──TCP/UDP──→ Caddy:25565 ──TCP/UDP──→ Backend:25565
unencrypted unencrypted
- No TLS handshake, no SNI
- Simplest setup, no certificate needed
- Use cases: Minecraft, game servers, DNS, plain SMTP, databases in VPN
Passthrough — TLS Pass-through
Client ──TLS──→ Caddy:443 ──TLS──→ Backend:443
encrypted encrypted (same connection)
- Caddy only reads the SNI (Server Name) from the TLS ClientHello
- The TLS tunnel is not broken — end-to-end encryption
- The backend must have its own valid certificate
- Use cases: Backend with its own LE certificate, strict E2E encryption requirements
Terminate — TLS Termination
Client ──TLS──→ Caddy:993 ──TCP──→ Backend:143
encrypted unencrypted
(Let's Encrypt)
- Caddy terminates TLS with a Let's Encrypt certificate
- Traffic to the backend is unencrypted (but within the VPN)
- Use cases: TLS for services that don't natively support it, IMAPS/SMTPS in front of plaintext backend
Blocked Ports
| Port | Usage |
|---|---|
| 80 | Caddy HTTP (ACME Challenge + Redirect) |
| 443 | Caddy HTTPS (HTTP routes) |
| 2019 | Caddy Admin API |
| 3000 | GateControl Web UI |
| 51820 | WireGuard VPN |
Use Cases
Minecraft Server (TCP, Port 25565, TLS: None)
Player connects to: mc.example.com:25565
L4 Route: Listen Port 25565 → Peer "Gaming Server" Target Port 25565
Protocol: TCP, TLS: None
SSH Access (TCP, Port 2222 → 22, TLS: None)
ssh -p 2222 admin@server.example.com
L4 Route: Listen Port 2222 → Peer "Home Server" Target Port 22
Protocol: TCP, TLS: None
Port 22 is not used as listen port to avoid conflicts with the GateControl server's SSH.
Database (TCP, Port 5433 → 5432, TLS: None)
psql -h server.example.com -p 5433 -U myuser mydb
L4 Route: Listen Port 5433 → Peer "DB Server" Target Port 5432
Protocol: TCP, TLS: None
Game Server (UDP)
Player connects to: game.example.com:27015
L4 Route: Listen Port 27015 → Peer "Game Server" Target Port 27015
Protocol: UDP, TLS: None
Feature Comparison: HTTP vs. L4
| Feature | HTTP (Layer 7) | L4 (Layer 4) |
|---|---|---|
| Routing method | Domain-based | Port-based |
| HTTPS / Let's Encrypt | Yes | Only with TLS Terminate |
| Compression (Gzip/Zstd) | Yes | No |
| Rate Limiting | Yes | No |
| Custom Headers | Yes | No |
| Basic Auth | Yes | No |
| Route Auth | Yes | No |
| Peer ACL | Yes | No |
| IP Access Control | Yes | No |
| Request Mirroring | Yes | No |
| Retry on Error | Yes | No |
| Circuit Breaker | Yes | No |
| Uptime Monitoring | HTTP Check | TCP Check |
| Multiple Backends | Yes (Load Balancing) | No |
| Sticky Sessions | Yes | No |
| WebSocket | Yes (automatic) | Yes (as TCP) |
| Protocol | HTTP/HTTPS | TCP / UDP |
WebSocket with HTTP Routes: WebSocket connections start as a regular HTTP request with a special Connection: Upgrade header. Caddy automatically detects this header and switches the connection to a persistent WebSocket connection. No additional configuration is needed — this works out-of-the-box with every HTTP route.
WebSocket with L4 Routes: Since L4 forwards the raw TCP stream without inspecting its content, WebSocket also works here — Caddy only sees TCP packets and forwards them 1:1.
Setup
Create HTTP Route (UI)
- Route Type: HTTP (default)
- Enter domain (e.g.
app.example.com) - Select target peer
- Enter target port (e.g. 8080)
- Configure features (HTTPS, Auth, etc.)
- Save
Create L4 Route (UI)
- Route Type: Switch to L4
- Enter domain (required for TLS Passthrough/Terminate, optional for TLS None)
- Select target peer
- Target Port enter (port on the backend)
- Protocol: Select TCP or UDP
- Listen Port enter (port on the GateControl server)
- TLS Mode select (None, Passthrough, Terminate)
- Save
Via API
# Create HTTP route
curl -X POST https://gatecontrol.example.com/api/v1/routes \
-H "Authorization: Bearer gc_..." \
-H "Content-Type: application/json" \
-d '{"route_type":"http","domain":"app.example.com","peer_id":1,"target_port":8080,"https_enabled":true}'
# Create L4 route (Minecraft)
curl -X POST https://gatecontrol.example.com/api/v1/routes \
-H "Authorization: Bearer gc_..." \
-H "Content-Type: application/json" \
-d '{"route_type":"l4","domain":"mc.example.com","peer_id":2,"target_port":25565,"l4_protocol":"tcp","l4_listen_port":"25565","l4_tls_mode":"none"}'
Important Notes
- L4 routes use exclusive ports. Each L4 route (without TLS) needs its own listen port.
- Multiple L4 routes with TLS (Passthrough or Terminate) can share the same port — Caddy distinguishes them via SNI.
- Port ranges are possible (e.g.
25565-25575for multiple Minecraft servers). - UDP routes don't support TLS (TLS runs over TCP).
- L4 routes have no HTTP features.
Force HTTPS
Enables automatic TLS encryption with Let's Encrypt certificates — HTTP requests are redirected to HTTPS via 301, certificates are automatically renewed.
How does it work?
Without Force HTTPS:
Client → http://app.example.com:80 → Caddy → Backend
↑ unencrypted, data in plaintext
With Force HTTPS:
Client → http://app.example.com:80 → 301 Redirect → https://...
Client → https://app.example.com:443 → Caddy (TLS) → Backend
↑ encrypted with Let's Encrypt certificate
When https_enabled is active:
- Listener: Caddy listens on port
:443instead of:80 - TLS Certificate: Caddy uses the ACME HTTP-01 challenge
- HTTP → HTTPS Redirect: Caddy automatically redirects all HTTP requests via 301 to HTTPS
- Auto-Renewal: Caddy renews certificates automatically (30 days before expiry)
TLS Configuration (if email is set):
{
"apps": {
"tls": {
"automation": {
"policies": [{
"issuers": [{
"module": "acme",
"email": "admin@example.com"
}]
}]
}
}
}
}
Custom ACME CA: Use the GC_CADDY_ACME_CA environment variable to configure an alternative ACME CA.
Configure ACME Email
# In docker-compose.yml or .env
GC_CADDY_EMAIL=admin@example.com
# Optional alternative ACME CA
GC_CADDY_ACME_CA=https://acme-staging-v02.api.letsencrypt.org/directory
Troubleshooting
| Problem | Cause | Solution |
|---|---|---|
| Certificate not issued | DNS not pointing to server | Check A record, use dig or nslookup |
| ACME challenge failed | Port 80 blocked | Check firewall/router, open port 80 |
| Too many certificates | Let's Encrypt rate limit | Wait 1 hour, then retry |
| Certificate expired | Caddy could not renew | Check Caddy logs, verify DNS and port 80 |
Important Notes on Force HTTPS
- DNS must point correctly. The domain must point to the GateControl server's public IP via A/AAAA record.
- Ports 80 and 443 must be open.
- No Cloudflare Proxy. Use DNS Only (grey cloud).
- Mind rate limits. Max 50 certificates per registered domain per week.
- Certificates are stored in
/data/caddy/and survive container restarts.
Backend HTTPS
Connects Caddy to the backend via HTTPS — for services that use self-signed certificates and enforce HTTPS (e.g. Synology DSM, Proxmox, UniFi Controller).
Without Backend HTTPS (backend enforces HTTPS):
Client → Caddy → http://10.8.0.3:5001 → Backend rejects HTTP ✕
With Backend HTTPS:
Client → Caddy (Let's Encrypt) → https://10.8.0.3:5001 → Backend (Self-Signed) ✓
↑ valid certificate ↑ insecure_skip_verify: true
Technical Details
{
"handler": "reverse_proxy",
"upstreams": [{ "dial": "10.8.0.3:5001" }],
"transport": {
"protocol": "http",
"tls": { "insecure_skip_verify": true }
}
}
Typical Use Cases
| Service | Port | Description |
|---|---|---|
| Synology DSM | 5001 | NAS Web UI with self-signed certificate |
| Proxmox VE | 8006 | Hypervisor Web UI |
| UniFi Controller | 8443 | Network management |
| Portainer | 9443 | Docker management UI |
Important Notes on Backend HTTPS
- Only enable when the backend enforces HTTPS. If HTTP also works, it's unnecessary.
insecure_skip_verifytrusts any certificate — acceptable in VPN context.- Backend HTTPS only affects the Caddy → backend connection.
- With load balancing: all backends must support HTTPS.
- Only available for HTTP routes, not L4.
Compression
Compresses HTTP responses with Gzip and Zstd — reduces transferred data by 60-80% for text content.
Without Compression:
Client ← 500 KB HTML ← Caddy ← 500 KB HTML ← Backend
With Compression:
Client ← 120 KB gzip ← Caddy (compresses) ← 500 KB HTML ← Backend
76% saved
Algorithms
| Algorithm | Browser Support | Compression | Speed |
|---|---|---|---|
| Zstd | Chrome 123+, Firefox 112+ | Better | Faster |
| Gzip | All browsers | Good | Standard |
Caddy selects Zstd when the client supports it, otherwise Gzip.
Typical Savings
| Content-Type | Uncompressed | Gzip | Zstd | Savings |
|---|---|---|---|---|
| HTML | 100 KB | 25 KB | 20 KB | 75-80% |
| CSS | 200 KB | 35 KB | 28 KB | 82-86% |
| JavaScript | 500 KB | 120 KB | 95 KB | 76-81% |
| JSON | 1 MB | 150 KB | 110 KB | 85-89% |
| PNG (image) | 300 KB | 295 KB | 295 KB | ~2% |
Caddy JSON Configuration
{
"handler": "encode",
"encodings": {
"zstd": {},
"gzip": {}
}
}
Important Notes on Compression
- Not recommended for already compressed content. Images (JPEG, PNG, WebP), videos, archives, and fonts (WOFF2) are already compressed.
- Caddy only compresses when the client sends
Accept-Encoding. - If the backend already serves compressed responses (
Content-Encoding: gzip), Caddy does not double-compress. - Only available for HTTP routes, not L4.