Deploy Django Behind Nginx Proxy Manager
Put a Docker-based Django app behind Nginx Proxy Manager: create a proxy host with Let’s Encrypt, connect containers on a shared network, forward the right headers, serve static files, allow large uploads, and keep the admin UI private.
Problem statement
You run several Docker apps on one server, often a home lab or small VPS, and use Nginx Proxy Manager (NPM) to route domains and get certificates through a web UI. Adding a Django app looks simple: create a proxy host pointing at the container. But Django then sees the wrong scheme and client IP, CSRF checks fail on HTTPS forms, uploads larger than 1 MB are rejected, and static files may not load. A few settings on both sides fix it.
- Put NPM and the Django container on the same Docker network, and use the container name as the forward host.
- Create a proxy host with scheme
http, port 8000, Block Common Exploits, and an SSL certificate with Force SSL. - In Django set
SECURE_PROXY_SSL_HEADER,ALLOWED_HOSTS, andCSRF_TRUSTED_ORIGINSfor the domain. - Add
client_max_body_size 50m;in the proxy host's advanced configuration for uploads. - Serve static files with WhiteNoise, and keep NPM's admin port (81) off the public internet.
Step-by-step solution
1. Share a network
# docker-compose.yml for the Django app
services:
web:
image: ghcr.io/acme/shop:latest
command: gunicorn config.wsgi --bind 0.0.0.0:8000 --workers 3
env_file: .env
networks: [proxy, default]
db:
image: postgres:17
volumes: [pgdata:/var/lib/postgresql/data]
networks:
proxy:
external: true
volumes:
pgdata: {}
Create the network once (docker network create proxy) and attach the NPM container to it as well. The web service publishes no ports; NPM reaches it as web (or the full container name) on port 8000.
2. Create the proxy host
In NPM's UI, add a proxy host:
- Domain:
shop.example.com - Scheme:
http, Forward hostname:shop-web-1(or the service alias), Port:8000 - Websockets support: on if you use Channels
- SSL tab: request a Let's Encrypt certificate, enable Force SSL and HTTP/2
3. Make Django proxy-aware
NPM sends X-Forwarded-Proto and X-Forwarded-For headers. Tell Django to trust the scheme header and list the domain:
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
ALLOWED_HOSTS = ["shop.example.com"]
CSRF_TRUSTED_ORIGINS = ["https://shop.example.com"]
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
Without SECURE_PROXY_SSL_HEADER, Django believes requests are HTTP, which causes redirect loops with SECURE_SSL_REDIRECT and CSRF origin failures. For client IPs, read X-Forwarded-For through a trusted middleware or Gunicorn's --forwarded-allow-ips, limited to NPM's network.
4. Allow uploads and long requests
In the proxy host's Advanced tab:
client_max_body_size 50m;
proxy_read_timeout 120s;
NPM's generated config otherwise uses Nginx defaults, including a 1 MB body limit, which returns 413 for larger uploads.
5. Static files and admin security
NPM proxies to the container; it does not see the container's files. Serve static files from Django with WhiteNoise, or mount the collected files into the NPM container and add a custom location /static/ block. WhiteNoise is simpler. Finally, NPM's admin UI on port 81 should only be reachable through a VPN, SSH tunnel, or localhost binding (127.0.0.1:81:81).
Explanation
Nginx Proxy Manager generates Nginx server blocks from the settings in its UI and manages Let's Encrypt certificates. It is regular Nginx underneath, so the same rules apply as with a hand-written reverse proxy: Django must be told which headers to trust for the original scheme and client address, and Nginx limits such as body size and timeouts must suit the app. The UI hides those details, which is why the defaults cause trouble with Django forms and uploads.
Edge cases / notes
- Cloudflare in front. With Cloudflare proxying to NPM, use Full (strict) SSL mode and a valid certificate on NPM.
- Real client IPs. Behind Cloudflare and NPM, the client address is in
CF-Connecting-IPor the first trusted hop ofX-Forwarded-For; configure accordingly. - Scaling. NPM suits a single host. For many services or advanced routing, Traefik or Caddy with configuration in Git may be easier to maintain.
Next steps
- Review the Compose stack with Deploy Django With Docker Compose in Production.
- Fix client addresses with Fix Django Wrong Client IP Behind a Proxy.
- Compare proxies in Nginx vs Caddy for Django.
FAQ
Should NPM forward to Django over https?
No. Use http to the container on the internal Docker network; TLS ends at NPM.
Why do my forms fail with CSRF errors behind NPM?
Django does not know the request was HTTPS. Set SECURE_PROXY_SSL_HEADER and CSRF_TRUSTED_ORIGINS.
How do I allow larger uploads?
Add client_max_body_size in the proxy host's Advanced configuration.
Can NPM serve Django static files?
Only if the files are mounted into the NPM container. Serving them with WhiteNoise from Django is simpler.