penpot/docker/images/files/nginx.conf.template
Andrey Antukh adb0fe01d2 🔧 Drop loopback bind on the stub_status endpoint
A compose port mapping delivers traffic to the container address,
never to loopback, so `listen 127.0.0.1:8082` made
`ports: <host>:8082` fail from the host. Both configs (image
template and devenv) now use `listen 8082`, which binds every
interface and matches the implicit bind of the public
`listen 8080 default_server`.

Rewrite both block comments to state the new bind and who decides
access from outside the host. The scrape URI stays on
127.0.0.1:8082: it still reaches the socket.

AI-assisted-by: mimo-v2.6-flash-free
2026-09-23 12:44:37 +00:00

233 lines
7.8 KiB
Plaintext

worker_processes auto;
pid /tmp/nginx.pid;
include /etc/nginx/overrides/main.d/*.conf;
events {
worker_connections 65535;
multi_accept on;
}
http {
client_body_temp_path /tmp/client_temp;
proxy_temp_path /tmp/proxy_temp_path;
fastcgi_temp_path /tmp/fastcgi_temp;
uwsgi_temp_path /tmp/uwsgi_temp;
scgi_temp_path /tmp/scgi_temp;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_requests 30;
keepalive_timeout 65;
types_hash_max_size 2048;
server_tokens off;
reset_timedout_connection on;
client_body_timeout 30s;
client_header_timeout 30s;
include /etc/nginx/mime.types;
default_type application/octet-stream;
error_log /dev/stderr;
# Access log enriched with upstream diagnostics: what the backend
# answered (us), how long it took to connect to it (uct), how long it
# took to answer (urt) and which internal address served the request
# (ua). With proxy_pass on variables there is no upstream keepalive,
# so uct measures one real TCP connection per request.
log_format penpot_upstream '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" "$http_user_agent" '
'us=$upstream_status uct=$upstream_connect_time '
'urt=$upstream_response_time ua=$upstream_addr';
access_log /dev/stdout penpot_upstream;
proxy_connect_timeout 300s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
send_timeout 300s;
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_static on;
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_types text/plain text/css text/javascript application/javascript application/json application/transit+json image/svg+xml application/wasm;
proxy_buffer_size 16k;
proxy_busy_buffers_size 24k; # essentially, proxy_buffer_size + 2 small buffers of 4k
proxy_buffers 32 4k;
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
# Link preview crawlers; their requests for the application root
# are served with dynamic Open Graph metadata from the backend.
map $http_user_agent $penpot_link_preview_agent {
default 0;
~*(slackbot|discordbot|twitterbot|facebookexternalhit|facebookcatalog|whatsapp|telegrambot|linkedinbot|skypeuripreview|pinterestbot|redditbot|embedly|iframely|mastodon|bluesky|applebot|googlebot|bingbot|bingpreview|duckduckbot) 1;
}
proxy_cache_path /tmp/cache/ levels=2:2 keys_zone=penpot:20m;
proxy_cache_methods GET HEAD;
proxy_cache_valid any 48h;
proxy_cache_key "$host$request_uri";
proxy_http_version 1.1;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Scheme $scheme;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
include /etc/nginx/overrides/http.d/*.conf;
server {
listen 8080 default_server reuseport backlog=16384;
${PENPOT_IPV6_LISTEN_DIRECTIVE}
server_name _;
client_max_body_size $PENPOT_HTTP_SERVER_MAX_BODY_SIZE;
charset utf-8;
etag off;
proxy_hide_header X-Powered-By;
include /etc/nginx/nginx-security-headers.conf;
root /var/www/app/;
location @handle_redirect {
set $redirect_uri "$upstream_http_location";
set $redirect_host "$upstream_http_x_host";
set $redirect_cache_control "$upstream_http_cache_control";
set $real_mtype "$upstream_http_x_mtype";
proxy_buffering off;
proxy_set_header Host "$redirect_host";
proxy_set_header Authorization "";
proxy_hide_header etag;
proxy_hide_header x-amz-id-2;
proxy_hide_header x-amz-request-id;
proxy_hide_header x-amz-meta-server-side-encryption;
proxy_hide_header x-amz-server-side-encryption;
proxy_ssl_server_name on;
proxy_pass $redirect_uri;
include /etc/nginx/nginx-security-headers.conf;
add_header x-internal-redirect "$redirect_uri";
add_header x-cache-control "$redirect_cache_control";
add_header cache-control "$redirect_cache_control";
add_header content-type "$real_mtype";
}
location /assets {
proxy_set_header Host $proxy_host;
proxy_pass $PENPOT_BACKEND_URI/assets;
recursive_error_pages on;
proxy_intercept_errors on;
error_page 301 302 307 = @handle_redirect;
include /etc/nginx/overrides/assets.d/*.conf;
}
location /internal/assets {
internal;
alias /opt/data/assets;
include /etc/nginx/nginx-security-headers.conf;
add_header x-internal-redirect "$upstream_http_x_accel_redirect";
}
location = /link-preview {
proxy_pass $PENPOT_BACKEND_URI/link-preview$is_args$args;
}
location /api/export {
proxy_set_header Host $proxy_host;
proxy_pass $PENPOT_EXPORTER_URI;
}
location /api {
proxy_set_header Host $proxy_host;
proxy_pass $PENPOT_BACKEND_URI/api;
proxy_buffering off;
}
location /plugins {
alias /var/www/app/plugins;
proxy_http_version 1.1;
}
location /readyz {
access_log off;
proxy_set_header Host $proxy_host;
proxy_pass $PENPOT_BACKEND_URI$request_uri;
}
location /ws/notifications {
proxy_set_header Host $proxy_host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_pass $PENPOT_BACKEND_URI/ws/notifications;
}
include /etc/nginx/overrides/server.d/*.conf;
location / {
include /etc/nginx/overrides/location.d/*.conf;
# Regenerated from the environment on every container start
# (see nginx-entrypoint.sh) while its URL is only versioned by
# the build, so a flags only restart leaves the URL untouched.
# Caching it like a build asset would keep returning users on
# the previous PENPOT_FLAGS for up to a week.
location = /js/config.js {
include /etc/nginx/nginx-security-headers.conf;
add_header Cache-Control "no-store, no-cache, max-age=0" always;
}
location ~* \.(js|css|jpg|png|svg|gif|ttf|woff|woff2|wasm|map)$ {
include /etc/nginx/nginx-security-headers.conf;
add_header Cache-Control "public, max-age=604800" always; # 7 days
}
location ~ ^/[^/]+/(.*)$ {
return 301 " /404";
}
if ($penpot_link_preview_agent) {
rewrite ^/$ /link-preview last;
}
include /etc/nginx/nginx-security-headers.conf;
add_header Cache-Control "no-store, no-cache, max-age=0" always;
try_files $uri /index.html$is_args$args /index.html =404;
}
}
# Dedicated health endpoint consumed by the nginx-prometheus-exporter
# sidecar (deployed by ops, scraping http://127.0.0.1:8082/stub_status).
# `listen 8082` binds every interface and sits out of the public server:
# a compose port mapping delivers traffic to the container address, never
# to loopback, so only the published port decides who gets in from
# outside the host. Counts client-side connections only; it says nothing
# about the upstream pools.
server {
listen 8082;
server_name _;
access_log off;
location = /stub_status {
stub_status;
}
}
}