Caddy reverse proxy
Contents
- If you use a self-hosted proxy, PostHog can't help troubleshoot. Use our managed reverse proxy if you want support.
- Use domains matching your PostHog region:
us.i.posthog.comfor US,eu.i.posthog.comfor EU. - Don't use obvious path names like
/analytics,/tracking,/telemetry, or/posthog. Blockers will catch them. Use something unique to your app instead.
This guide shows you how to configure Caddy as a reverse proxy for PostHog.
Caddy automatically handles TLS certificates via Let's Encrypt, making setup simple compared to other web servers. You don't need to manually configure SSL - Caddy provisions and renews certificates for you.
Prerequisites
- Caddy installed on your server or Docker
- A domain with DNS records pointing to your server
- Ports 80 and 443 open if using a public subdomain
Choose your setup option
All three options accomplish the same goal of routing PostHog through your domain to bypass ad blockers. Choose the one that fits your infrastructure:
- Option 1: Basic setup: Routes all PostHog traffic through a dedicated subdomain like
e.yourdomain.com. Use this for production deployments where you want a clean, simple proxy. - Option 2: Subpath routing: Routes PostHog through a path on an existing domain like
yourdomain.com/ph-proxy. Use this when you're running multiple services on the same domain or for local development and testing. - Option 3: Docker deployment: Same as Option 1, but runs Caddy in a Docker container. Use this if your infrastructure is containerized or you prefer container management.
Option 1: Basic setup
This setup proxies all PostHog requests through a dedicated subdomain.
- 1
Create a Caddyfile
In your working directory, create a file named
Caddyfile:Replace
e.yourdomain.comwith your chosen subdomain.The
header_up Hostdirective tells PostHog which domain the request is for. Without this, PostHog won't know how to route your request and you'll get 401 errors.The
header_down -Access-Control-Allow-Origindirective removes CORS headers from PostHog's response. This prevents conflicts with your own CORS configuration. - 2
Start Caddy
Run this command from the same directory as your
Caddyfile:TerminalCaddy will automatically request a TLS certificate from Let's Encrypt for your subdomain. The first start may take a minute while it provisions the certificate.
See Caddy's quick start guide for more details.
- 3
Update your PostHog SDK
In your application code, update your PostHog initialization to use your tracking subdomain:
Replace
e.yourdomain.comwith your actual subdomain.The
ui_hostmust point to PostHog's actual domain so features like the toolbar link correctly. Verify your setup
CheckpointConfirm events are flowing through your proxy:
- Open your browser's developer tools and go to the Network tab
- Trigger an event in your app, like a page view
- Look for a request to your subdomain (e.g.,
e.yourdomain.com) - Verify the response is
200 OK - Check the PostHog app to confirm events appear
If you see errors, check troubleshooting below.
Option 2: Subpath routing
If you're running other services on the same domain or testing locally, you can proxy PostHog through a subpath like /ph-proxy.
- 1
Create a Caddyfile with subpath routing
In your working directory, create a file named
Caddyfile:This configuration:
- Listens on port 2015 for local testing (no TLS certificate needed)
- Routes requests from
/ph-proxyto PostHog - Uses
handle_pathto strip the/ph-proxyprefix before forwarding - Uses
rewriteto reconstruct the correct path for PostHog - Serves local files from the same directory for testing
The
rewritedirective is necessary because PostHog expects requests like/static/array.js, not/ph-proxy/static/array.js. Thehandle_pathdirective strips the prefix, thenrewritereconstructs the path. - 2
Create a test HTML file
OptionalIf you want to test your proxy locally, create a file named
home.htmlin the same directory:HTMLThis file includes the PostHog snippet configured to use your local proxy. You'll use it to verify the proxy works before deploying to production.
- 3
Start Caddy and test locally
Start Caddy from your working directory:
TerminalOpen
http://localhost:2015/home.htmlin your browser. Check the Network tab to verify requests go tolocalhost:2015/ph-proxy. You should see200 OKresponses. - 4
Update your PostHog SDK
For production deployment, update your application code to use your proxy path:
Replace
yourdomain.comwith your actual domain and adjust the Caddyfile to listen on ports 80/443 instead of 2015. Verify your setup
CheckpointConfirm events are flowing through your proxy:
- Open your browser's developer tools and go to the Network tab
- Trigger an event in your app, like a page view
- Look for a request to your domain with
/ph-proxypath - Verify the response is
200 OK - Check the PostHog app to confirm events appear
If you see errors, check troubleshooting below.
Option 3: Docker deployment
If you prefer containerized deployments, you can run Caddy in Docker with the same configuration as option 1.
- 1
Create your Caddyfile
On your server, create a file named
Caddyfilein a directory like/etc/caddy:Replace
e.yourdomain.comwith your subdomain andyourdomain.comwith your application domain.The
Access-Control-Allow-Originheader allows your application domain to make requests to the proxy. Adjust this if you need to allow requests from multiple domains orlocalhostfor testing. - 2
Start the Caddy container
Run this command to start Caddy with Docker:
TerminalThis command:
- Mounts your Caddyfile into the container
- Persists Caddy's data (including TLS certificates) in a Docker volume named
caddy_data
The volume persistence is critical. Without it, Caddy will request new certificates every time the container restarts, which can hit Let's Encrypt's rate limits and cause your proxy to fail.
See the Caddy Docker documentation for more details.
- 3
Update your PostHog SDK
In your application code, update your PostHog initialization to use your tracking subdomain:
Replace
e.yourdomain.comwith your actual subdomain. Verify your setup
CheckpointConfirm events are flowing through your proxy:
- Open your browser's developer tools and go to the Network tab
- Trigger an event in your app, like a page view
- Look for a request to your subdomain (e.g.,
e.yourdomain.com) - Verify the response is
200 OK - Check the PostHog app to confirm events appear
If you see errors, check troubleshooting below.
Troubleshooting
TLS certificate provisioning fails
If Caddy can't obtain a TLS certificate:
- Verify your DNS records point to your server's IP address
- Check that ports 80 and 443 are open and accessible from the internet
- Ensure no other service is using ports 80 or 443
- Check Caddy's logs with
caddy logsfor specific errors
Caddy needs port 80 temporarily during certificate provisioning, even if you only serve traffic on 443. Let's Encrypt uses HTTP-01 challenges to verify domain ownership.
If you hit Let's Encrypt's rate limits of 5 failed attempts per hour, wait an hour and try again. Using a Docker volume prevents this by persisting certificates.
401 Unauthorized errors
If PostHog returns 401 Unauthorized when capturing events:
- Verify you set
header_up Hostto the correct PostHog domain - Check that your PostHog region matches
- Confirm your project API key is correct in the SDK initialization
The header_up Host directive is essential. PostHog uses the Host header to identify which project the request belongs to. Without it, PostHog can't authenticate your events.
CORS errors in browser console
If you see No 'Access-Control-Allow-Origin' header errors:
Add explicit CORS headers to your Caddyfile:
Replace https://yourdomain.com with your application's origin. For multiple origins or testing, use * instead, but be aware this is less secure.
Static assets return 404
If the PostHog SDK fails to load or you see 404 errors for /static/* requests:
- Verify your Caddyfile has a
handle /static/*block - Check that it points to the correct assets domain,
us-assets.i.posthog.comoreu-assets.i.posthog.com - Confirm the assets domain matches your PostHog region
The /static/* route must be defined before the catch-all handle block. Caddy evaluates handlers in order, so place specific paths before generic ones.
Port conflicts
If Caddy fails to start with "address already in use":
- Check what's using ports 80 and 443:
sudo lsof -i :80 -i :443 - Stop the conflicting service or configure Caddy to use different ports
- If using different ports, you'll need to include the port in your
api_host(e.g.,https://e.yourdomain.com:8443)
Common conflicts include Apache, Nginx, or other Caddy instances.