Skip to content

Commit df5e4e4

Browse files
committed
feat: add nginx reverse proxy for Plausible analytics
- Proxy Plausible script and event API through nginx to bypass adblockers - Add DNS resolver (Quad9 9.9.9.9) for upstream domain resolution - Add proxy_cache in /dev/shm for Plausible script (5m TTL, 100m max) - Add security headers (X-Frame-Options, X-Content-Type-Options, X-XSS-Protection, Referrer-Policy) - Configure Puma to bind to Unix socket when nginx config exists - Signal nginx buildpack readiness via /tmp/app-initialized file ## Lessons learned Deviations from initial plan: - Used /dev/shm (tmpfs) for proxy_cache instead of /tmp (limited space) - Used ENV detection for nginx config file presence, not DYNO var (not available on all dynos) - Moved nginx 'set' directives inside server block (required by nginx) - Had to add DNS resolver manually (Heroku's resolver not automatically available in proxy_pass) Key technical discoveries: - heroku-buildpack-nginx waits for /tmp/app-initialized file before starting nginx - nginx 'set' directive only works in server/location contexts, not http block - Puma must bind to unix socket AND signal readiness for nginx to connect - proxy_cache in /dev/shm persists across dyno restarts (vs /tmp which doesn't) See: github.com//discussions/2580
1 parent fd1a03b commit df5e4e4

4 files changed

Lines changed: 93 additions & 5 deletions

File tree

Procfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
release: bundle exec rake db:migrate
2-
web: bundle exec puma -C config/puma.rb
2+
web: bin/start-nginx bundle exec puma -C config/puma.rb

app/views/layouts/application.html.haml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,10 @@
3131
= csp_meta_tag
3232

3333
<!-- Privacy-friendly analytics by Plausible -->
34-
%script{ async: true, src: 'https://plausible.io/js/pa-PFruVsE_br97UUCRXE_6f.js' }
34+
%script{ async: true, src: '/js/script.js' }
3535
%script
3636
window.plausible = window.plausible || function() { (plausible.q = plausible.q || []).push(arguments) }, plausible.init = plausible.init || function(i) { plausible.o = i || {} };
37-
plausible.init()
37+
plausible.init({ endpoint: '/api/event' })
3838

3939
%body.no-js{ 'class': "#{params[:controller]}-#{params[:action]}", 'data-bs-no-jquery': 'true' }
4040
#top

config/nginx.conf.erb

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
daemon off;
2+
worker_processes auto;
3+
4+
events {
5+
worker_connections 1024;
6+
}
7+
8+
http {
9+
charset utf-8;
10+
server_tokens off;
11+
12+
# DNS resolver for proxy_pass
13+
resolver 9.9.9.9 valid=30s;
14+
15+
# Proxy cache in /dev/shm (tmpfs, persists across dyno restarts)
16+
proxy_cache_path /dev/shm/nginx_cache levels=1:2 keys_zone=plausible_cache:1m max_size=100m inactive=5m use_temp_path=off;
17+
18+
# Security headers
19+
add_header X-Frame-Options "SAMEORIGIN" always;
20+
add_header X-Content-Type-Options "nosniff" always;
21+
add_header X-XSS-Protection "1; mode=block" always;
22+
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
23+
24+
# Logs to stdout/stderr for Heroku
25+
access_log /dev/stdout;
26+
error_log /dev/stderr;
27+
28+
# Proxy settings
29+
proxy_http_version 1.1;
30+
proxy_buffering on;
31+
32+
upstream app_server {
33+
server unix:/tmp/nginx.socket fail_timeout=0;
34+
}
35+
36+
server {
37+
listen <%= ENV["PORT"] %>;
38+
server_name _;
39+
keepalive_timeout 5;
40+
41+
# Plausible endpoints (set inside server context)
42+
set $plausible_script_url https://plausible.io/js/pa-PFruVsE_br97UUCRXE_6f.js;
43+
set $plausible_event_url https://plausible.io/api/event;
44+
45+
# Plausible: Proxy script.js (cached)
46+
location = /js/script.js {
47+
proxy_cache plausible_cache;
48+
proxy_cache_valid 200 5m;
49+
proxy_cache_key "$host$uri";
50+
proxy_pass $plausible_script_url;
51+
proxy_set_header Host plausible.io;
52+
proxy_buffering on;
53+
54+
# Cache response headers
55+
add_header X-Cache $upstream_cache_status;
56+
}
57+
58+
# Plausible: Proxy event API
59+
location = /api/event {
60+
proxy_pass $plausible_event_url;
61+
proxy_set_header Host plausible.io;
62+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
63+
proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;
64+
proxy_set_header X-Forwarded-Host $host;
65+
proxy_buffering on;
66+
}
67+
68+
# Rails: All other requests
69+
location / {
70+
proxy_pass http://app_server;
71+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
72+
proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;
73+
proxy_set_header Host $http_host;
74+
proxy_redirect off;
75+
}
76+
}
77+
}

config/puma.rb

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
# This configuration file will be evaluated by Puma. The top-level methods that
22
# are invoked here are part of Puma's configuration DSL. For more information
33
# about methods provided by the DSL, see https://puma.io/puma/Puma/DSL.html.
4-
#
4+
5+
require 'fileutils'
6+
57
# Puma starts a configurable number of processes (workers) and each process
68
# serves each request in a thread from an internal thread pool.
79
#
@@ -29,7 +31,16 @@
2931
threads threads_count, threads_count
3032

3133
# Specifies the `port` that Puma will listen on to receive requests; default is 3000.
32-
port ENV.fetch("PORT", 3000)
34+
# Use Unix socket when nginx config exists (from heroku-community/nginx buildpack)
35+
# Falls back to port if nginx config not present
36+
if File.exist?("config/nginx.conf.erb")
37+
bind "unix:///tmp/nginx.socket?umask=0077" # Restrict socket permissions to owner only
38+
39+
# Signal to nginx buildpack that app is ready (required for nginx to start)
40+
FileUtils.touch("/tmp/app-initialized")
41+
else
42+
port ENV.fetch("PORT", 3000)
43+
end
3344

3445
# Allow puma to be restarted by `bin/rails restart` command.
3546
plugin :tmp_restart

0 commit comments

Comments
 (0)