mysite/static/sources/posts/pi_service_touchup.md

418 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
title: "Nextcloud on a Raspberry Pi, Fronted by a Tiny VPS — Nginx Reverse Proxy, Trusted Proxies, and Basic Auth for Jellyfin"
date: 2026-02-02
tags: ["homelab","nginx","docker","nextcloud","jellyfin","cloudflare","tailscale","security","raspberrypi"]
description: "Cloudflare DNS + Nginx on a VPS + Tailscale to the Pi. Small, boring, and reliable."
draft: false
---
> I didnt “move Nextcloud to the cloud”.
> I moved the **front door** to a VPS… and kept the house on my Raspberry Pi. 😄
This post documents the setup I ended up with:
- A public **VPS** (host: `funbox`) running **Nginx** + **Lets Encrypt**
- A private **Raspberry Pi** (host: `iot-hub`) running Docker services (**Nextcloud**, **Jellyfin**, …)
- A private backhaul using **Tailscale** (the `100.x.y.z` network)
- A correct Nextcloud reverse-proxy configuration (**trusted_domains**, **trusted_proxies**, and overwrite values)
- A pragmatic security layer for media: **Basic Auth at Nginx for Jellyfin**
(in addition to Jellyfins own login)
Im writing this as a “future me” note and a “copy-paste friendly” guide.
---
## 0) Topology
The request path looks like:
```
Browser
↓ HTTPS (public)
Cloudflare DNS (optional proxy on/off)
VPS (funbox) — Nginx reverse proxy + Let's Encrypt
↓ HTTP over Tailscale (private 100.x network)
Raspberry Pi (iot-hub) — Docker: Nextcloud / Jellyfin / …
```
Why I like it:
- The Pi can sit behind home router / CGNAT and still be reachable.
- TLS, redirects, headers, auth, rate limits… all centralized on the VPS.
- Internal IPs can change without breaking public URLs.
---
## 1) Whats running on the Pi?
On `iot-hub` the “interesting” containers are:
- `nextcloud` (Apache variant)
- `nextcloud-db` (Postgres)
- `nextcloud-redis`
- `jellyfin`
Example `docker ps` style output (yours may vary):
```
nextcloud nextcloud:apache 0.0.0.0:8080->80/tcp
jellyfin jellyfin/jellyfin 0.0.0.0:8096->8096/tcp
```
So on the Pi, the services listen on:
- `http://<pi>:8080` for Nextcloud
- `http://<pi>:8096` for Jellyfin
But in my setup, **the VPS does not reach those via LAN** — it reaches them via Tailscale IPs.
---
## 2) Tailscale: the private wire between VPS and Pi
Tailscale assigns each node an address like `100.xx.yy.zz`.
In my config, Nginx on `funbox` points to the Pi via Tailscale:
- Nextcloud upstream: `http://100.104.127.96:8080`
- Jellyfin upstream: `http://100.104.127.96:8096`
(Use the Tailscale IP of your Pi.)
Quick sanity checks from the VPS:
```bash
# from funbox, make sure you can reach the Pi service:
curl -I http://100.104.127.96:8080
curl -I http://100.104.127.96:8096
```
If those dont work: fix Tailscale connectivity first (ACLs, firewall, node online).
---
## 3) Nginx on the VPS: reverse proxy blocks
### 3.1 Nextcloud vhost (VPS → Pi via Tailscale)
Create (or edit):
`/etc/nginx/sites-available/nextcloud.alipourimjourneys.ir`
and symlink into `sites-enabled`.
Here is a complete working example:
```nginx
# Redirect HTTP → HTTPS
server {
listen 80;
listen [::]:80;
server_name nextcloud.alipourimjourneys.ir;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name nextcloud.alipourimjourneys.ir;
# Certbot-managed certs
ssl_certificate /etc/letsencrypt/live/nextcloud.alipourimjourneys.ir/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/nextcloud.alipourimjourneys.ir/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
# Big uploads (tune to taste)
client_max_body_size 2G;
# CalDAV/CardDAV redirects
location = /.well-known/carddav { return 301 https://$host/remote.php/dav/; }
location = /.well-known/caldav { return 301 https://$host/remote.php/dav/; }
location / {
proxy_pass http://100.104.127.96:8080;
proxy_http_version 1.1;
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;
# Sometimes helps apps behind multiple proxies
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
# Nextcloud + WebDAV can do long requests
proxy_read_timeout 3600;
proxy_send_timeout 3600;
# Usually good for DAV/uploads
proxy_buffering off;
proxy_request_buffering off;
}
}
```
Then test + reload:
```bash
sudo nginx -t
sudo systemctl reload nginx
```
### 3.2 Jellyfin vhost (with Basic Auth)
Create:
`/etc/nginx/sites-available/jellyfin.alipourimjourneys.ir`
Example:
```nginx
server {
listen 80;
listen [::]:80;
server_name jellyfin.alipourimjourneys.ir;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name jellyfin.alipourimjourneys.ir;
ssl_certificate /etc/letsencrypt/live/jellyfin.alipourimjourneys.ir/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/jellyfin.alipourimjourneys.ir/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
client_max_body_size 512M;
# ✅ Basic Auth gate (extra layer before Jellyfin)
auth_basic "Jellyfin (private)";
auth_basic_user_file /etc/nginx/.htpasswd-jellyfin;
location / {
proxy_pass http://100.104.127.96:8096;
proxy_http_version 1.1;
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;
# Jellyfin uses websockets
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 3600;
proxy_send_timeout 3600;
proxy_buffering off;
}
}
```
Enable:
```bash
sudo ln -s /etc/nginx/sites-available/jellyfin.alipourimjourneys.ir /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
```
---
## 4) Creating the Basic Auth password file
Install tools (Debian/Ubuntu):
```bash
sudo apt-get update
sudo apt-get install -y apache2-utils
```
Create the password file:
```bash
sudo htpasswd -c /etc/nginx/.htpasswd-jellyfin yourusername
```
(If adding more users later, omit `-c`.)
Lock it down:
```bash
sudo chown root:root /etc/nginx/.htpasswd-jellyfin
sudo chmod 640 /etc/nginx/.htpasswd-jellyfin
```
### Notes on clients
- Most browsers + most Jellyfin apps handle HTTP Basic Auth fine.
- Some TV apps can be quirky. If a client cant handle it, you can:
- remove Basic Auth, or
- keep it only on selected paths, or
- use Cloudflare Access in front of Jellyfin instead (more work).
---
## 5) Nextcloud: configure it to behave behind the reverse proxy
Nextcloud needs to know:
- what hostnames are valid,
- which proxy is trusted,
- and what the “outside” URL scheme is.
You can do it via `occ` inside the container (Apache image uses `www-data`).
### 5.1 trusted_domains
Check current values:
```bash
docker exec -u www-data nextcloud php /var/www/html/occ config:system:get trusted_domains
```
Add your public domain:
```bash
docker exec -u www-data nextcloud php /var/www/html/occ config:system:set trusted_domains 1 --value="nextcloud.alipourimjourneys.ir"
```
(Keep your internal name too if you still use it, e.g. `rpi:8080`.)
### 5.2 trusted_proxies
Because requests arrive from the VPS (over Tailscale), Nextcloud must trust that proxy IP.
Example:
```bash
docker exec -u www-data nextcloud php /var/www/html/occ config:system:set trusted_proxies 0 --value="100.99.79.75"
```
(Use the **VPSs Tailscale IP** as seen from the Pi.)
### 5.3 overwritehost / overwriteprotocol / overwrite.cli.url
Tell Nextcloud “the world sees me as https://nextcloud.example”:
```bash
docker exec -u www-data nextcloud php /var/www/html/occ config:system:set overwritehost --value="nextcloud.alipourimjourneys.ir"
docker exec -u www-data nextcloud php /var/www/html/occ config:system:set overwriteprotocol --value="https"
docker exec -u www-data nextcloud php /var/www/html/occ config:system:set overwrite.cli.url --value="https://nextcloud.alipourimjourneys.ir"
```
### 5.4 forwarded_for_headers (optional, but often helpful)
```bash
docker exec -u www-data nextcloud php /var/www/html/occ config:system:set forwarded_for_headers 0 --value="HTTP_X_FORWARDED_FOR"
```
Restart Nextcloud container after config:
```bash
docker restart nextcloud
```
---
## 6) Sanity checks (curl is your friend)
From anywhere public:
```bash
curl -I http://nextcloud.alipourimjourneys.ir
curl -I https://nextcloud.alipourimjourneys.ir
curl -I https://nextcloud.alipourimjourneys.ir/.well-known/caldav
```
Expected “good signs”:
- HTTP returns **301** to HTTPS
- HTTPS returns **302** to `/login` (or 200 if already authenticated)
- `/.well-known/caldav` returns **301** to `/remote.php/dav/`
If you see redirect loops or wrong hostnames:
- revisit `overwritehost`, `overwriteprotocol`, `trusted_proxies`.
---
## 7) “Do I really need Cloudflare Access / WARP?”
### The honest answer
If your setup is:
- HTTPS only
- strong passwords + MFA in Nextcloud/Jellyfin
- your origin isnt directly exposed (only the VPS is public)
- you keep things patched
…then youre already in a **reasonable** place.
### “Can I skip Cloudflare Access?”
Yes. In this topology, Cloudflare Access is optional. The main security boundary is:
- **Public:** VPS + Nginx
- **Private:** Pi over Tailscale
For Jellyfin, Basic Auth adds a cheap extra gate thats “family friendly”.
---
## 8) Cloudflare Access: One-time PIN not arriving + passkeys
Two common gotchas:
### 8.1 One-time PIN email didnt arrive
That flow relies on email delivery. Check:
- spam/junk folder
- if your provider blocked it
- the exact email allowlist in your policy
If its flaky, Id avoid One-time PIN and use a real identity provider.
### 8.2 Can I use passkeys / Apple / Google?
Yes — but passkeys typically come via an identity provider (IdP) that supports WebAuthn/passkeys.
Practical approach:
- pick what your family already uses (Google or Apple),
- configure that as the login method,
- avoid WARP enrollment unless you specifically want device-based access.
---
## 9) Hardening checklist (tiny but effective)
On the VPS:
- Keep Ubuntu security updates on
- firewall: allow only what you need (22/80/443)
- optional: `fail2ban` for SSH
On the Pi:
- keep Docker images updated
- Postgres/Redis not exposed publicly (Docker internal network)
- backups: Nextcloud data + DB
---
## 10) TL;DR
- VPS Nginx terminates TLS, proxies to Pi over Tailscale
- Nextcloud must be told about:
- `trusted_domains`
- `trusted_proxies`
- overwrite values (`overwritehost`, `overwriteprotocol`, `overwrite.cli.url`)
- `curl -I` should show sane redirects + `/remote.php/dav/`
- Jellyfin gets an extra gate with **Nginx Basic Auth**
Boring is good. Boring runs for months.
---
---
{{< sigdl >}}