Configure Nginx on Raspberry Pi 5
This article configures Nginx as a loopback-only origin on the prepared Pi at 192.168.60.10. Nginx listens on 127.0.0.1:8080, serves a small site from /srv/www/dmz-site, and is ready for the local cloudflared connector. It is not a WAN listener and does not require a pfSense port forward.
0.0.0.0:8080 would expose the origin to every reachable interface, including the DMZ. The loopback bind makes the local tunnel the intended path; still verify the socket after every configuration change.
Prerequisites and version check
Complete Prepare Raspberry Pi 5 as a DMZ Web Server. Keep a local console or a verified private administration session available before reloading a service. Confirm the OS, architecture, Nginx package source, and active network state:
cat /etc/os-release
uname -m
ip -br address
sudo apt update
apt-cache policy nginx
nginx -v 2>&1 || true
Bookworm repositories and Nginx module names can change. Use the current distribution package and Nginx documentation for the installed release; do not mix configuration snippets copied from a different major release without checking their directives.
Install Nginx and inspect the packaged layout
sudo apt install --no-install-recommends nginx
systemctl status nginx --no-pager
sudo nginx -T | sed -n '1,180p'
On Debian-family systems, the package commonly provides /etc/nginx/sites-available, sites-enabled, and a default site. The output of nginx -T is authoritative for this host. Do not put tunnel credentials or application secrets under the document root.
Create a minimal loopback site
Use a hostname that the tunnel will send in its Host header. Replace www.example.net with the actual hostname you control; do not leave a public catch-all that accidentally serves this origin for arbitrary hostnames.
sudo tee /etc/nginx/sites-available/dmz-site >/dev/null <<'EOF'
server {
listen 127.0.0.1:8080 default_server;
server_name _;
return 444;
}
server {
listen 127.0.0.1:8080;
server_name www.example.net;
root /srv/www/dmz-site;
index index.html;
access_log /var/log/nginx/dmz-site.access.log;
error_log /var/log/nginx/dmz-site.error.log warn;
server_tokens off;
client_max_body_size 2m;
location / {
try_files $uri $uri/ =404;
}
}
EOF
sudo ln -s /etc/nginx/sites-available/dmz-site /etc/nginx/sites-enabled/dmz-site
sudo rm -f /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl reload nginx
The first server is the IPv4 loopback default and rejects unknown Host values with Nginx’s nonstandard 444 close. The intended site is therefore not the default server. This baseline is IPv4-only: there is deliberately no [::1] listener. Never replace the loopback address with the Pi’s DMZ address merely to make an external test work.
Test the origin locally
sudo ss -lntp
sudo ss -lntp | grep -F '127.0.0.1:8080'
curl --fail --silent --show-error -H 'Host: www.example.net' http://127.0.0.1:8080/
curl --silent --show-error --output /dev/null --write-out 'unknown Host HTTP status: %{http_code}\n' -H 'Host: wrong.example.net' http://127.0.0.1:8080/ || true
curl --connect-timeout 2 --output /dev/null --write-out 'DMZ-address HTTP status: %{http_code}\n' http://192.168.60.10:8080/ || true
sudo nginx -T
The first request should return the placeholder page. The unknown-Host request should close without a normal HTTP response (curl commonly reports status 000 for Nginx’s 444), and must not return the intended page. The DMZ-address request should fail or show status 000 because no socket is bound there. Confirm ss shows only 127.0.0.1:8080, not 0.0.0.0:8080, [::1]:8080, or 192.168.60.10:8080. Test with the exact Host header that the tunnel configuration will send.
Set safe file permissions and log behavior
sudo find /srv/www/dmz-site -type d -exec chmod 0755 {} \;
sudo find /srv/www/dmz-site -type f -exec chmod 0644 {} \;
sudo chown -R www-data:www-data /srv/www/dmz-site
sudo install -d -o root -g adm -m 0750 /var/log/nginx
sudo systemctl enable nginx
sudo systemctl is-enabled nginx
sudo journalctl -u nginx -n 50 --no-pager
Keep logs protected and define rotation using the distribution’s existing logrotate configuration. Do not log authorization headers, tunnel tokens, session cookies, or unnecessary personal data. A static site still needs updates, monitoring, and a content rollback plan.
Use the host firewall as a second check
The preparation article’s nftables policy should not need an inbound DMZ rule for port 8080 because the origin is loopback-only. If you changed the policy while installing Nginx, inspect it and ensure no broad inbound rule was added:
sudo nft list ruleset
sudo ss -lntp
sudo nginx -t
There is no reason to open port 8080 on Netgate WAN, the DMZ interface, or a trusted LAN. If a future design requires a separate reverse proxy, document its source address and use a narrow rule rather than broadening this listener.
Application safety before publication
- Replace the placeholder with content that has no private hostnames, credentials, internal IPs, debug output, directory listings, or backup files.
- If the application needs PHP, a database, uploads, or dynamic execution, design and harden those components separately. Do not enable CGI, PHP execution, proxying, or unrestricted uploads just because Nginx supports them.
- Set application authentication and authorization independently of Cloudflare. A public hostname is not an authentication mechanism.
- Use a deliberate canonical hostname and review headers, redirects, cookies, cache behavior, and error pages from an external client.
Validation, rollback, and hand-off
Before creating a tunnel, validate that local requests work, wrong Host values do not disclose the intended site, Nginx starts after reboot, and no socket exists on the Pi’s DMZ address. Check access and error logs while making one local request. From another DMZ or trusted host, confirm 192.168.60.10:8080 is not reachable; from the Pi, confirm the firewall management address is not reachable.
To roll back, save the active configuration first, then disable the site and reload only after a syntax check:
sudo cp -a /etc/nginx /root/nginx-backup-$(date +%Y%m%d-%H%M%S)
sudo rm -f /etc/nginx/sites-enabled/dmz-site
sudo nginx -t
sudo systemctl reload nginx
If the reload fails, restore the last known-good site symlink and configuration, run nginx -t, and reload. If the package is no longer needed, stop and disable Nginx, remove the package only after preserving configuration and content, and remove any tunnel route that points to it.
When the local origin passes, continue with Install Cloudflared and Create the Tunnel.
dispelled