---
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 didn’t “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** + **Let’s 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 Jellyfin’s own login)

I’m 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) What’s 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 don’t 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 can’t 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 **VPS’s 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 isn’t directly exposed (only the VPS is public)
- you keep things patched

…then you’re 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 that’s “family friendly”.

---

## 8) Cloudflare Access: One-time PIN not arriving + passkeys

Two common gotchas:

### 8.1 One-time PIN email didn’t arrive
That flow relies on email delivery. Check:
- spam/junk folder
- if your provider blocked it
- the exact email allowlist in your policy

If it’s flaky, I’d 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 >}}
