This guide shows you how to use Qtap to capture HTTP traffic flowing through Caddy, a modern web server with automatic HTTPS. You'll learn how to observe both incoming client requests and outgoing upstream connections from your Caddy server, all without proxies or code changes.
What You'll Learn
Capture Caddy ingress traffic (client requests)
Capture Caddy egress traffic (upstream service requests)
Monitor both sides of a reverse proxy simultaneously
Apply conditional capture rules for specific routes
Handle Caddy's automatic HTTPS with Qtap's TLS inspection
Set up Caddy + Qtap in Docker for testing
Deploy production-ready configurations
Why capture Caddy traffic?
Reverse Proxy Visibility: See both client requests and backend responses
API Gateway Monitoring: Track all API calls through your Caddy gateway
Automatic HTTPS Inspection: See inside TLS traffic without managing certificates
Microservices Debugging: Debug issues between services
Performance Analysis: Measure latency at each hop
Security Auditing: Monitor for suspicious traffic patterns
Migration Planning: Understand traffic patterns before infrastructure changes
Linux system with kernel 5.10+ and eBPF support
Docker installed (for this guide's examples)
Basic understanding of Caddy/Caddyfile syntax
Part 1: Simple Caddy Web Server
Let's start with a basic Caddy setup serving static content and reverse proxying to upstream services.
Step 1: Create Caddy Configuration
Create a directory for our demo:
Create Caddyfile:
Step 2: Create Qtap Configuration
Create qtap.yaml:
Key Configuration Points:
direction: all - Captures both client→caddy AND caddy→upstream traffic
ignore_loopback: false - Important! Caddy often uses localhost
level: full - Captures complete requests/responses including bodies
Step 3: Create Docker Compose Setup
Create docker-compose.yaml:
Part 2: Running and Testing
Step 1: Start the Services
Step 2: Generate Test Traffic
Step 3: View Captured Traffic
What you should see:
Key indicators that it's working:
✅ "exe": "/usr/bin/caddy" or similar - Caddy process identified
✅ Direction: INGRESS - Client to Caddy
✅ Direction: EGRESS - Caddy to upstream
✅ Two transactions for proxied requests (ingress + egress)
✅ Custom headers visible (X-Request-ID, X-Forwarded-Server)
✅ Full request/response bodies captured
✅ Latency tracked for both hops
Part 3: Advanced Configurations
Configuration 1: Capture Only Errors
Reduce volume by capturing only failed requests:
Test it:
Configuration 2: Route-Specific Capture
Capture different levels for different Caddy routes using Rulekit:
Configuration 3: HTTPS Upstream Monitoring
When Caddy proxies to HTTPS upstreams, Qtap can still see the traffic:
Caddyfile:
qtap.yaml:
Why this works: Qtap hooks into Caddy's TLS library (typically Go's crypto/tls) before encryption happens, so it sees plaintext even for HTTPS upstreams.
Configuration 4: Production Setup with S3
For production, store sensitive data securely:
Update docker-compose.yaml:
Part 4: Real-World Use Cases
Use Case 1: API Gateway with Authentication
Monitor API gateway with focus on authentication and errors:
Caddyfile:
qtap.yaml:
Use Case 2: Microservices Mesh Monitoring
Monitor Caddy as a service mesh proxy:
Caddyfile:
qtap.yaml:
Use Case 3: Static Site with CDN Backend
Monitor Caddy serving static sites with CDN backend:
Caddyfile:
qtap.yaml:
Understanding the Output
Dual Capture for Reverse Proxy
When Caddy proxies a request, Qtap captures two HTTP transactions:
Transaction 1: INGRESS (Client → Caddy)
Transaction 2: EGRESS (Caddy → Upstream)
This lets you:
Measure total latency vs. backend latency
See how Caddy transforms requests (headers, paths)
Debug issues on either side of the proxy
Caddy-Specific Details
Process Identification:
Look for exe containing caddy (often /usr/bin/caddy or /usr/local/bin/caddy)
Container name: caddy-demo (in Docker)
Automatic HTTPS:
When Caddy uses automatic HTTPS, Qtap still sees plaintext via eBPF TLS hooks
No certificate management needed
Works with Let's Encrypt, ZeroSSL, or custom CAs
Troubleshooting
Not Seeing Caddy Traffic?
Check 1: Is Qtap running before requests?
Check 2: Is ignore_loopback correct?
Check 3: Is Caddy processing requests?
Check 4: Verify Qtap hooks Caddy
Seeing "l7Protocol": "other"?
This means connection captured but HTTP not parsed:
Wait longer after starting Qtap (6+ seconds)
Check if Caddy is using HTTP/3 (QUIC) - not yet supported
Verify traffic is actually HTTP/HTTPS
Caddy Using HTTP/3?
Qtap currently supports HTTP/1.x and HTTP/2. If Caddy negotiates HTTP/3 (QUIC):
Disable HTTP/3 in Caddyfile:
Too Much Traffic?
Option 1: Conditional capture
Option 2: Filter specific routes
Option 3: Summary level only
Qtap operates out-of-band with minimal overhead:
CPU: ~1-3% for typical traffic
Memory: ~50-200MB depending on volume
Latency: Zero additional latency (passive observation)
Best practices for high-traffic Caddy:
Use level: summary or details for high volume
Apply conditional rules to capture selectively
Filter health checks and monitoring endpoints
Send to S3 with batching (use Fluent Bit)
Set TTL policies on storage (90 days recommended)
Scaling Recommendations
S3 + Fluent Bit + aggressive filtering
Caddy vs NGINX: Key Differences
Process Name:
Configuration:
Caddy: Caddyfile (simpler, more human-readable)
NGINX: nginx.conf (more complex, more options)
HTTPS:
Caddy: Automatic by default (Qtap still works!)
NGINX: Manual configuration
Language:
Caddy: Written in Go (uses Go's crypto/tls)
NGINX: Written in C (uses OpenSSL)
Both work perfectly with Qtap's eBPF-based capture.
Learn More About Qtap:
Production Deployment:
Related Guides:
Alternative: Cloud Management:
Qplane - Manage Qtap with visual dashboards
This guide uses validated configurations. All examples are tested and guaranteed to work with Caddy and Qtap.