Serving a Python Flask App Behind Nginx

Why this pattern works for any backend

Nginx doesn't care what's behind proxy_pass. PHP-FPM talking over FastCGI, a Python app on port 5000, a Go binary on port 8080, a Node service on port 3000 — they all look the same from Nginx's perspective. The pattern is identical:

  1. Write the app so it listens on 127.0.0.1 on a port nothing else uses
  2. Wrap it in systemd so it starts on boot and restarts if it crashes
  3. Point Nginx at it with proxy_pass
  4. Let Nginx handle SSL, headers, access control, and rate limiting

The Jellyfin reverse proxy guide applies the same pattern to a media server. The home server WordPress proxy applies it to an entire PHP stack across a Tailscale tunnel. The health endpoint in this guide is a natural companion to both — deploy it on the backend server and you've got visibility into disk, memory, and load before proxying requests. Once you've set up one proxy_pass backend, the second one takes five minutes.


Security considerations

The app runs as an unprivileged user (username, not root, not www-data). It can only read log files that user has permission to read — grant access selectively via group membership rather than opening permissions broadly. The app binds to localhost, so even if the firewall is wide open, nothing outside the server can reach port 5000 directly.

A log reader is not a public endpoint. The log endpoints expose error log contents — IP addresses, request paths, timestamps — to anyone who can reach them. Left open, a bot could map your server's entire attack surface from your own logs. The Nginx config in Step 3 restricts access to trusted IPs with allow/deny. If you need remote access from changing IPs, add authentication at the app level or route through a VPN.

The /health endpoint is safer — it exposes system metrics but no request data — but you should still restrict it if it's running on a production server. Hostname, load average, and disk usage are useful to you and reconnaissance for an attacker.

For a public-facing Python app — an API, a dashboard, a webhook — you don't need IP restrictions, but you need rate limiting. A Python backend uses a fixed number of workers. Without a limit_req zone on the location / block, one aggressive client can exhaust the worker pool. The rate limiting guide covers the setup — the same limit_req_zone you'd use for PHP works identically behind proxy_pass.

If the app needed write access — say, to maintain its own state file — it would write to its own directory under /home/username/apps/logwatch/, not anywhere in /var. Principle of least privilege applies to Python apps the same way it applies to PHP-FPM pools.

For a deeper dive into locking down the server side, the Securing Nginx and PHP guide covers open_basedir, per-user PHP-FPM pools, and filesystem ACLs — the same principles apply to any backend language running on the same server.

Technical Audit Summary

This guide documents a minimal Flask application served behind Nginx via proxy_pass, with systemd service management and a Python virtual environment. The architecture is backend-agnostic — Go, Node, Ruby, or any language that listens on a TCP port works identically behind the same Nginx pattern.

Last Audit: June 2026
Environment: Debian Trixie (13)
Nginx: 1.31.1
Python: 3.12

Compatibility: The proxy_pass pattern works on any Nginx 1.18+ build. Flask 3.x and Gunicorn 22.x are current stable releases. The systemd service template applies to any systemd-based distribution. The virtual environment pattern is Python-version-agnostic.

2026-06-09: Initial publication — Flask app behind Nginx reverse proxy with systemd service management, virtual environment isolation, and IP-restricted access. Includes a health endpoint (/health) for backend server monitoring alongside log-reader endpoints, chunked file reading for memory efficiency, and the logrotate group-fix for adm/www-data permission issues. Cross-linked to Jellyfin proxy guide and home server reverse proxy guide as examples of the same proxy_pass pattern applied to different backends.