Reverse Proxy
For production deployments, place PicPeak behind a reverse proxy for SSL termination and routing.
The routing below applies to the multi-container Compose stack, where the frontend and backend are separate services. The all-in-one image serves everything from one port, so a proxy in front of it needs a single rule pointing at port 3000, and the container needs TRUST_PROXY=1 (see below) — see Example: NAS at Home for a worked setup with Nginx Proxy Manager.
Routing schema
| Path | Service | Port | Description |
|---|---|---|---|
/api/* | Backend | 3001 | All API endpoints |
/photos/* | Backend | 3001 | Protected photo files |
/thumbnails/* | Backend | 3001 | Protected thumbnail files |
/uploads/* | Backend | 3001 | Upload files |
/* (everything else) | Frontend | 3000 | React SPA (including /admin/*, /gallery/*) |
The /admin/* routes are served by the frontend (React SPA), not the backend. The backend only handles /api/admin/* requests.
The production compose file publishes the backend port 3001 on host loopback (127.0.0.1) only. A proxy on the same host reaches it at localhost:3001 as below; a proxy on another machine cannot, unless you set PICPEAK_BACKEND_BIND_ADDRESS. The simpler alternative is a single rule that sends everything to the frontend port 3000, whose nginx forwards /api and the media paths to the backend itself — then set TRUST_PROXY=2 (see below).
Nginx
Create /etc/nginx/sites-available/picpeak:
server {
listen 80;
server_name your-domain.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name your-domain.com;
ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem;
# Backend: API endpoints
location /api/ {
proxy_pass http://localhost:3001;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Backend: Protected media files
location ~ ^/(photos|thumbnails|uploads)/ {
proxy_pass http://localhost:3001;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Frontend: Everything else (React SPA)
location / {
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}Enable the site:
sudo ln -s /etc/nginx/sites-available/picpeak /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginxFor video uploads, increase the body size limit:
client_max_body_size 10G;
proxy_read_timeout 3600;
proxy_send_timeout 3600;Client IP, rate limiting and TRUST_PROXY
PicPeak keys its API rate limiter and its login lockout on the client IP. Behind a proxy the backend only sees the real client address when two things hold: the proxy forwards X-Forwarded-For (all configurations above do), and PicPeak trusts exactly the proxies in front of it.
TRUST_PROXY is the number of proxy hops PicPeak trusts. The production compose file defaults it to 1, which is the bundled frontend nginx. Count the proxies between the client and the backend on the path your /api traffic takes:
| Layout | Hops | TRUST_PROXY |
|---|---|---|
| No proxy of your own; clients open the frontend port | frontend nginx | 1 (default) |
Your proxy forwards everything to the frontend port 3000 (the installer’s reverse-proxy layout, Cloudflare Tunnel, Nginx Proxy Manager) | your proxy, frontend nginx | 2 |
Your proxy routes /api and the media paths straight to the backend, as in the configurations above | your proxy | 1 (default) |
| All-in-one image behind your proxy | your proxy | 1 (the image defaults to 0) |
# .env — your proxy in front of the frontend port
TRUST_PROXY=2Set it before you update an install that sits behind a proxy and has no TRUST_PROXY in its .env (beta from v3.174.2-beta.0, stable from v3.134.7). With too low a value every visitor presents as the proxy’s address, shares one rate-limit budget of 300 requests per 15 minutes, and five failed logins from anyone lock out logins for everyone for 15 minutes. When TRUST_PROXY is 1 and a request shows another private-range proxy in front of nginx, the backend logs one warning suggesting 2.
A hop count is only safe when clients cannot take a shorter path to the app, which is why the backend port is published on host loopback only (PICPEAK_BACKEND_BIND_ADDRESS). With a proxy on the same host, also set PICPEAK_BIND_ADDRESS=127.0.0.1 so the plain-HTTP frontend port is not reachable around it. Do not set TRUST_PROXY=true unless every hop in front of PicPeak is yours: a trusted-all setting lets any client spoof its address through X-Forwarded-For.
See Environment Variables → TRUST_PROXY.
Running behind Cloudflare? Bots and Scanners has the ready-made TRUST_PROXY line.
Mixed HTTPS + LAN HTTP access
Some self-hosted setups want two working entry points to the same PicPeak instance:
- Public HTTPS via the reverse proxy on a domain (
https://photos.example.com) - Direct HTTP on the LAN for admin access (
http://192.168.1.50:3000, the frontend port)
This works with the default COOKIE_SECURE=auto in production. The backend reads req.secure from Express: the Secure cookie flag is set when the request arrived over HTTPS (via a trusted proxy forwarding X-Forwarded-Proto: https) and left off otherwise. Both public HTTPS login and LAN HTTP login work with correctly-flagged cookies.
# .env — the default; only needed if you set something else before
COOKIE_SECURE=autoTwo conditions:
- The proxy must be inside the
TRUST_PROXYboundary described above. If it is not, every request counts as plain HTTP and the cookies lose theirSecureflag. - The LAN entry point is the frontend port. The backend port
3001is published on host loopback only; setPICPEAK_BACKEND_BIND_ADDRESSto reach it from another machine.
Keep PICPEAK_BIND_ADDRESS at its default (0.0.0.0) in this setup, since the LAN needs the frontend port. If the site is only ever reached over HTTPS, set COOKIE_SECURE=true instead.
The Nginx, Traefik, and Caddy configurations shown above already forward X-Forwarded-Proto correctly.
See Environment Variables → Authentication cookies for the full reference.