Note: The following self-hosted proxy isn't provided by PostHog, so we can't take responsibility for it! If unsure, we recommend using our managed reverse proxy.
Note: If you are using the EU cloud then use
eu
instead ofus
in all domains (e.g.us.i.posthog.com
->eu.i.posthog.com
)
Caddy makes setting up a reverse proxy with TLS simple. For these examples:
- Sub out
YOUR_TRACKING_DOMAIN
for the domain you use for proxying to PostHog. We'd suggest something likee.yourdomain.com
. - Make sure your DNS records point to the server where Caddy is running.
- Make sure ports 80 and 443 are open and directed toward Caddy.
Basic setup
First, install Caddy.
Next, create a Caddyfile
that listens for incoming requests and proxies them to PostHog:
${YOUR_TRACKING_DOMAIN} {handle /static {reverse_proxy https://us-assets.i.posthog.com:443 {header_up Host us-assets.i.posthog.comheader_down -Access-Control-Allow-Origin}}handle {reverse_proxy https://us.i.posthog.com:443 {header_up Host us.i.posthog.comheader_down -Access-Control-Allow-Origin}}}
Run caddy start
from the same folder as your Caddyfile
. Once running, you can use your tracking domain as a reverse proxy to PostHog like this:
<script>!function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.crossOrigin="anonymous",p.async=!0,p.src=s.api_host+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="capture identify alias people.set people.set_once set_config register register_once unregister opt_out_capturing has_opted_out_capturing opt_in_capturing reset isFeatureEnabled onFeatureFlags getFeatureFlag getFeatureFlagPayload reloadFeatureFlags group updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures getActiveMatchingSurveys getSurveys getNextSurveyStep onSessionId".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);posthog.init('<ph_project_api_key>',{api_host:`${YOUR_TRACKING_DOMAIN}`,ui_host: '<ph_app_host>'})</script>
Using a subpath
If your reverse proxy is running on the same domain as another app, you can set up a handle_path
matcher and rewrite the path to remove it for the PostHog request. This is useful for testing the reverse proxy locally combined with the Caddy file_server
.
To showcase this, create a folder, and then a Caddyfile
in it like this:
:2015 {handle_path /phproxy/static* {rewrite * /static/{path}reverse_proxy https://us-assets.i.posthog.com:443 {header_up Host us-assets.i.posthog.comheader_down -Access-Control-Allow-Origin}}handle_path /phproxy* {rewrite * {path}reverse_proxy https://us.i.posthog.com:443 {header_up Host us.i.posthog.comheader_down -Access-Control-Allow-Origin}}file_server browse}
In the same folder, create a home.html
file with some content and the PostHog snippet.
<script>!function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.crossOrigin="anonymous",p.async=!0,p.src=s.api_host+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="capture identify alias people.set people.set_once set_config register register_once unregister opt_out_capturing has_opted_out_capturing opt_in_capturing reset isFeatureEnabled onFeatureFlags getFeatureFlag getFeatureFlagPayload reloadFeatureFlags group updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures getActiveMatchingSurveys getSurveys getNextSurveyStep onSessionId".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);posthog.init('<ph_project_api_key>',{api_host:'http://localhost:2015/phproxy',ui_host:'us.posthog.com'})</script><h1>Test home page</h1>
When you go to http://localhost:2015/home.html
, events are sent to PostHog via the reverse proxy.
Running Caddy with Docker
On the server where you run Docker, create a Caddyfile
in etc/caddy
like:
${YOUR_TRACKING_DOMAIN} {header {Access-Control-Allow-Origin https://${YOUR_TRACKING_DOMAIN}}handle /static {reverse_proxy https://us-assets.i.posthog.com:443 {header_up Host us-assets.i.posthog.comheader_down -Access-Control-Allow-Origin}}handle {reverse_proxy https://us.i.posthog.com:443 {header_up Host us.i.posthog.comheader_down -Access-Control-Allow-Origin}}}
If
YOUR_TRACKING_DOMAIN
points to the same domain as production then the above works for requests originating from that domain. If you want to test your proxy from other domains, such aslocalhost
, you'll need to tweak theAccess-Control-Allow-Origin
header and/orYOUR_TRACKING_DOMAIN
accordingly.
With Docker installed and set up, run the following command to start Caddy:
docker run -p 80:80 -p 443:443 \-v $PWD/Caddyfile:/etc/caddy/Caddyfile \-v caddy_data:/data \caddy
You can now use your tracking domain as a reverse proxy to PostHog. See the Docker Caddy overview for more details, especially important is the "⚠️ A note about persisted data" which explains why it is important to use a Docker volume for the /data
folder.