Nginx Proxy Setup
Configure Nginx reverse proxy to serve Rybbit tracking and bypass ad blockers
Nginx is a powerful web server and reverse proxy that makes it easy to proxy Rybbit tracking through your own domain. This guide shows how to configure Nginx to forward tracking requests to Rybbit servers while caching static scripts for better performance.
Two settings make or break a first-party proxy:
- Enable First-Party Proxy in your Rybbit Site Settings → Privacy & Security, so the visitor IP your proxy forwards in
X-Forwarded-Foris trusted directly. - Forward all of the browser's request headers, not a hand-picked few. Rybbit's bot detection expects the headers every real browser sends (
Accept,Accept-Language,Accept-Encoding,Sec-Fetch-*) — proxies that forward onlyUser-Agentget their events accepted with200 OKbut classified as bot traffic. UseX-Forwarded-Forfor the visitor IP;X-Real-IPdoes not survive Rybbit Cloud's edge.
Overview
Nginx's proxy_pass directive lets you forward requests from your domain to Rybbit's servers. Combined with caching, this provides excellent performance and reliability for proxying analytics.
What you'll achieve:
- Proxy all Rybbit endpoints through your domain
- Cache static scripts for better performance
- Forward necessary headers for accurate tracking
- Support all Rybbit features (session replay, Web Vitals, custom events)
Prerequisites
- Nginx installed (version 1.18 or later recommended)
- SSL/TLS certificate configured (Let's Encrypt recommended)
- Your Rybbit instance URL:
- Cloud hosted:
https://app.rybbit.io - Self-hosted: Your instance URL
- Cloud hosted:
- Your Rybbit site ID (found in your dashboard)
Implementation
Configure Nginx Proxy
Add a new location block (or update existing configuration) in your Nginx site configuration:
# /etc/nginx/sites-available/yourdomain.com
server {
listen 443 ssl http2;
server_name yourdomain.com;
# Your SSL configuration
ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
# Your main site configuration
location / {
# Your regular site config...
}
# Rybbit Analytics Proxy
# nginx passes the browser's other request headers through unchanged.
# Main tracking script
location = /analytics/script.js {
proxy_pass https://app.rybbit.io/api/script.js;
proxy_set_header Host app.rybbit.io;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_ssl_server_name on;
}
# Session replay script (loaded only when session replay is on)
location = /analytics/replay.js {
proxy_pass https://app.rybbit.io/api/replay.js;
proxy_set_header Host app.rybbit.io;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_ssl_server_name on;
}
# Event tracking endpoint (not cached)
location = /analytics/track {
proxy_pass https://app.rybbit.io/api/track;
proxy_set_header Host app.rybbit.io;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_ssl_server_name on;
}
# User identification endpoint (not cached)
location = /analytics/identify {
proxy_pass https://app.rybbit.io/api/identify;
proxy_set_header Host app.rybbit.io;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_ssl_server_name on;
}
# Site configuration endpoint
location /analytics/site/tracking-config/ {
proxy_pass https://app.rybbit.io/api/site/tracking-config/;
proxy_set_header Host app.rybbit.io;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_ssl_server_name on;
}
# Feature flag evaluation (not cached)
location ~ ^/analytics/site/[^/]+/feature-flags/evaluate$ {
rewrite ^/analytics/(.*)$ /api/$1 break;
proxy_pass https://app.rybbit.io;
proxy_set_header Host app.rybbit.io;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_ssl_server_name on;
}
# Session replay recording endpoint (not cached)
location /analytics/session-replay/record/ {
proxy_pass https://app.rybbit.io/api/session-replay/record/;
proxy_set_header Host app.rybbit.io;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_ssl_server_name on;
# Allow larger uploads for session replay
client_max_body_size 10M;
}
}This covers every endpoint the tracking script calls. Web vitals are bundled into script.js; replay.js loads only when session replay is on. If the feature-flag route isn't proxied, rybbit.flag() silently returns your fallback.
Replace app.rybbit.io with your self-hosted Rybbit instance URL if you're not using the cloud version.
Keep variables out of proxy_pass. With a variable such as $1, nginx resolves the upstream hostname per request and returns 502 unless you configure a resolver. And in a regex location (location ~), a proxy_pass with a fixed path such as /api/track fails nginx -t; only a path built from variables is allowed there, and that brings back the resolver requirement. That's why the paths with a site ID use prefix locations, or a rewrite followed by a proxy_pass without a path.
Minimal configuration with just the script and the event endpoint:
# /etc/nginx/sites-available/yourdomain.com
server {
listen 443 ssl http2;
server_name yourdomain.com;
# SSL configuration
ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
# Main tracking script
location = /analytics/script.js {
proxy_pass https://app.rybbit.io/api/script.js;
proxy_set_header Host app.rybbit.io;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_ssl_server_name on;
}
# Event tracking
location = /analytics/track {
proxy_pass https://app.rybbit.io/api/track;
proxy_set_header Host app.rybbit.io;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_ssl_server_name on;
}
}A minimal setup proxies only script.js and /track. The script then can't fetch your site's tracking config, so it ignores every setting on the site's Tracking tab and runs on built-in defaults: pageviews, SPA navigation, query strings and outbound links are tracked; web vitals, error tracking, session replay, button clicks, copy and form tracking stay off; feature flags return their fallback; and identify() and setTraits() can't reach /identify, so traits aren't stored and earlier anonymous activity isn't linked to the user. Use the full proxy unless that's what you want.
The other tabs don't cache anything: proxy_cache_valid has no effect without a proxy_cache zone. To cache the scripts at your proxy, define a cache zone:
# /etc/nginx/nginx.conf (http block)
http {
# Define cache zone for Rybbit
proxy_cache_path /var/cache/nginx/rybbit
levels=1:2
keys_zone=rybbit_cache:10m
max_size=100m
inactive=60m
use_temp_path=off;
# ... rest of http config
}Then add the cache directives to the script locations in your site config:
# /etc/nginx/sites-available/yourdomain.com
server {
# ... server config
location = /analytics/script.js {
proxy_pass https://app.rybbit.io/api/script.js;
proxy_set_header Host app.rybbit.io;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_ssl_server_name on;
# Use the cache zone
proxy_cache rybbit_cache;
proxy_cache_valid 200 1h;
proxy_cache_key "$scheme$request_method$host$request_uri";
add_header X-Cache-Status $upstream_cache_status;
}
# ... other locations
}Do the same for replay.js. If you also cache /analytics/site/tracking-config/, keep it short (proxy_cache_valid 200 5m;) so changes you make in the dashboard reach visitors. Never cache the POST endpoints.
Important: The proxy_set_header X-Real-IP and X-Forwarded-For headers are critical for accurate geolocation and user identification. Without these, all traffic will appear to come from your Nginx server's IP.
Create Cache Directory (Optional)
If using a cache zone, create the cache directory:
sudo mkdir -p /var/cache/nginx/rybbit
sudo chown www-data:www-data /var/cache/nginx/rybbitTest Nginx Configuration
Before reloading Nginx, test the configuration for syntax errors:
sudo nginx -tYou should see:
nginx: configuration file /etc/nginx/nginx.conf test is successfulReload Nginx
Apply the changes by reloading Nginx:
sudo systemctl reload nginxOr if you prefer:
sudo nginx -s reloadUpdate Your Tracking Script
Update your HTML to load the script from your proxied domain:
<!DOCTYPE html>
<html>
<head>
<!-- ... other head elements -->
<script src="/analytics/script.js?siteId=YOUR_SITE_ID" defer></script>
</head>
<body>
<!-- Your content -->
</body>
</html>Replace YOUR_SITE_ID with your actual site ID from the Rybbit dashboard. Older snippets that pass the ID as data-site-id still work.
Keep the proxied file named script.js. The script cuts its own URL at /script.js to find where to send events, so a renamed file (for example /analytics/a.js) sends every request to the wrong place.
Verify the Setup
Test your configuration:
-
Check Nginx is running:
sudo systemctl status nginx -
Test script loading:
curl -I https://yourdomain.com/analytics/script.jsYou should see:
HTTP/2 200 content-type: application/javascript x-cache-status: MISS # First request, only if you set up the cache zone -
If you set up the cache zone, test again to verify caching:
curl -I https://yourdomain.com/analytics/script.jsYou should see:
HTTP/2 200 x-cache-status: HIT # Cached! -
Open your website in a browser with Developer Tools and verify:
- Script loads from
/analytics/script.js(your domain) - Tracking requests go to
/analytics/track(your domain) - Data appears in your Rybbit dashboard after 1-2 minutes
- Script loads from
How It Works
Nginx's proxy_pass directive transparently forwards requests to Rybbit's servers:
- Browser requests
https://yourdomain.com/analytics/script.js - Nginx matches the location block and forwards to
https://app.rybbit.io/api/script.js - Nginx sets necessary headers (
X-Real-IP,X-Forwarded-For) for accurate tracking - Response is cached (if configured) and sent back to browser
- All subsequent tracking requests follow the same pattern
The Rybbit script auto-detects it's being served from your domain and sends all tracking data to your domain's /analytics/* endpoints.
Performance Optimization
Caching Strategy
Configure different cache durations based on content type (requires the cache zone from the With Cache Zone tab). These blocks replace the matching ones from the Full Proxy tab; don't add them alongside. nginx picks an exact-match location = over a regex one, and a repeated prefix location fails nginx -t.
# Scripts - nginx follows Rybbit's Cache-Control: 1 hour for script.js, 1 day for replay.js
location ~ ^/analytics/(script|replay)\.js$ {
rewrite ^/analytics/(.*)$ /api/$1 break;
proxy_pass https://app.rybbit.io;
# ... proxy headers
proxy_cache rybbit_cache;
proxy_cache_valid 200 1h;
proxy_cache_valid 404 1m; # Cache 404s briefly
}
# Config - cache for 5 minutes (changes when you update dashboard settings)
location /analytics/site/tracking-config/ {
proxy_pass https://app.rybbit.io/api/site/tracking-config/;
# ... proxy headers
proxy_cache rybbit_cache;
proxy_cache_valid 200 5m;
}
# Tracking - never cache (each request is unique)
location ~ ^/analytics/(track|identify)$ {
rewrite ^/analytics/(.*)$ /api/$1 break;
proxy_pass https://app.rybbit.io;
# ... proxy headers
# No caching directives
}Gzip Compression
Enable compression for JavaScript files:
# In http or server block
gzip on;
gzip_vary on;
gzip_types application/javascript text/javascript;
gzip_min_length 1000;Connection Pooling
Keep connections to Rybbit backend alive for better performance:
upstream rybbit_backend {
server app.rybbit.io:443;
keepalive 32;
}
location /analytics/ {
proxy_pass https://rybbit_backend/api/;
proxy_http_version 1.1;
proxy_set_header Connection "";
# With an upstream block, Host and SNI default to "rybbit_backend"
proxy_set_header Host app.rybbit.io;
proxy_ssl_server_name on;
proxy_ssl_name app.rybbit.io;
# ... other proxy headers
}Proxy Buffering
For better performance with larger session replay uploads:
location /analytics/session-replay/record/ {
proxy_pass https://app.rybbit.io/api/session-replay/record/;
# ... proxy headers
proxy_buffering on;
proxy_buffer_size 4k;
proxy_buffers 8 4k;
proxy_busy_buffers_size 8k;
client_max_body_size 10M;
}Security Considerations
Rate Limiting
Protect your proxy from abuse with rate limiting:
# In http block
http {
limit_req_zone $binary_remote_addr zone=analytics_limit:10m rate=10r/s;
# ... rest of config
}
# In server block
location = /analytics/track {
limit_req zone=analytics_limit burst=20 nodelay;
proxy_pass https://app.rybbit.io/api/track;
# ... proxy headers
}This limits to 10 requests per second per IP, with bursts up to 20.
Request Size Limits
Set appropriate limits for different endpoints:
# Default for most endpoints
client_max_body_size 1M;
# Larger limit for session replay
location /analytics/session-replay/record/ {
client_max_body_size 10M;
# ... proxy config
}Security Headers
Add security headers to your responses:
add_header X-Content-Type-Options nosniff;
add_header X-Frame-Options DENY;
add_header Referrer-Policy strict-origin-when-cross-origin;Troubleshooting
502 Bad Gateway
Problem: Nginx returns 502 error when accessing analytics endpoints.
Solution:
- Check Rybbit backend is accessible:
curl -I https://app.rybbit.io/api/script.js - Verify
proxy_ssl_server_name onis set - If the error log says
no resolver defined to resolve app.rybbit.io, aproxy_passcontains a variable (such as$1). Remove the variable (use a prefix location, orrewrite ... breakwith aproxy_passthat has no path), or add aresolverdirective - Check Nginx error logs:
sudo tail -f /var/log/nginx/error.log
nginx -t fails on a regex location
Problem: nginx -t reports "proxy_pass" cannot have URI part in location given by regular expression.
Solution: A location ~ block can't use a proxy_pass with a fixed path such as /api/... (a path built from variables is allowed, but then nginx needs a resolver). Use a prefix location (location /analytics/session-replay/record/), or keep the regex and rewrite the URI first:
location ~ ^/analytics/site/[^/]+/feature-flags/evaluate$ {
rewrite ^/analytics/(.*)$ /api/$1 break;
proxy_pass https://app.rybbit.io;
# ... proxy headers
}Cache not working
Problem: X-Cache-Status always shows MISS.
Solution:
- Verify cache directory exists and is writable
- Check
proxy_cache_pathis defined in http block - Ensure
proxy_cachedirective uses correct zone name - Check cache key is consistent:
proxy_cache_key "$scheme$request_method$host$request_uri";
Incorrect geolocation in Rybbit
Problem: All visitors show same location (your server's location).
Solution: Ensure you're forwarding client IP headers, and that First-Party Proxy is on in your site settings:
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;SSL errors in Nginx logs
Problem: Logs show SSL verification errors when connecting to Rybbit.
Solution:
Add proxy_ssl_server_name on to enable SNI:
location /analytics/ {
proxy_pass https://app.rybbit.io/api/;
proxy_ssl_server_name on; # Enable SNI
# ... other config
}Advanced Configuration
Subdomain Proxy
Use a dedicated subdomain for analytics:
# /etc/nginx/sites-available/analytics.yourdomain.com
server {
listen 443 ssl http2;
server_name analytics.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/analytics.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/analytics.yourdomain.com/privkey.pem;
# Proxy all requests to Rybbit
location / {
proxy_pass https://app.rybbit.io/api/;
proxy_set_header Host app.rybbit.io;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_ssl_server_name on;
}
}Then use:
<script src="https://analytics.yourdomain.com/script.js?siteId=123" defer></script>Multiple Rybbit Instances
If you need to proxy multiple Rybbit instances:
location /analytics-prod/ {
proxy_pass https://prod.rybbit.io/api/;
# ... proxy config
}
location /analytics-staging/ {
proxy_pass https://staging.rybbit.io/api/;
# ... proxy config
}Related Resources
- Nginx Custom Configuration Guide - Self-hosting Rybbit with Nginx
- Tracking Script Documentation - Script configuration options
- Generic Proxy Guide - Framework-agnostic concepts
- Nginx Proxy Module - Official Nginx documentation