Initial import of Hugo site to Forgejo

This commit is contained in:
Iman Alipour 2026-07-28 12:47:20 +00:00
commit cb1ba0317f
1259 changed files with 236349 additions and 0 deletions

View file

@ -0,0 +1,418 @@
---
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 >}}