# Nextcloud on a Raspberry Pi, Fronted by a Tiny VPS — Nginx Reverse Proxy, Trusted Proxies, and Basic Auth for Jellyfin

Cloudflare DNS + Nginx on a VPS + Tailscale to the Pi. Small, boring, and reliable.
February 2, 2026 · 6 min · Iman Alipour Comments

> 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:

    # 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:

    # 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:

    sudo nginx -t
    sudo systemctl reload nginx

### 3.2 Jellyfin vhost (with Basic Auth)#

Create:

`/etc/nginx/sites-available/jellyfin.alipourimjourneys.ir`

Example:

    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:

    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):

    sudo apt-get update
    sudo apt-get install -y apache2-utils

Create the password file:

    sudo htpasswd -c /etc/nginx/.htpasswd-jellyfin yourusername

(If adding more users later, omit `-c`.)

Lock it down:

    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:

    docker exec -u www-data nextcloud php /var/www/html/occ config:system:get trusted_domains

Add your public domain:

    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:

    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”:

    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)#

    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:

    docker restart nextcloud

## 6) Sanity checks (curl is your friend)#

From anywhere public:

    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.

Downloads: Markdown · Signature (.asc)

View OpenPGP signature

    -----BEGIN PGP SIGNATURE-----

    iQIzBAABCgAdFiEEVaKl3oR5K6zGyu4/tYgoUOBMjSoFAmpkbuYACgkQtYgoUOBM
    jSomZA/+LsB+V0BnPki5qaDc+tRu6EXM5kPuH7G8x5ar4opsKgbfsRKEwT6/Q0Er
    vfg48uYNp4fBnoyfJrgURx+OwS7EVJRCmXaRsdilEEGHF7cT3qHhlczYaC+58lGs
    +qXaHjYE8x0byAZ8iHNxNUhIyILVrcsdua3JZ3D3J/8r93dfVgrWg41Nrtj4tPUE
    QQMW/KxTe/TOED4iivXxFlDCiT13KmgB/B9HeunGPqu8lPMTORf4ePoPzeYEeuuZ
    iVH+ssNZ9XB00Z4a/c3I0d2ALLYoA/dXmrJkl0qCCpCy+7/id2yz3eXYWn2xKMvp
    SAMTYaFAV6Fd7xdmxGYEeoCSDu8P57P7QfdPVEU1oUTANO21tEh1vGnjxcFdJUBx
    kOvBdjQf4wDqUuNv7NKA+OHOzhJ3Wf3VLb/ZNq6Y2okEibsCygm3XDn0008WeKPv
    ncB0gceY6nFYzbyhuUIhPtQJJWDNi5KG/KMEvYcebEwDzn7TErg/v3Bp8ZCdWRx/
    Gs8K9nADnHhAjWgwTq3D+2qRWcF0tlLSTmKg+95yaYi0XWWMFGTgCv2odPsgFlIS
    3FiLJC3rV73prsk+7eZftBTYCJN0Xk92YFj4a6bTYeuLcC20VbAA98Bpi0pCyO0v
    TJ2+amlKyT+Nq9wGrAez+dTvR0FKuEvA5OO693Aibv/iwOX6UPU=
    =2UUg
    -----END PGP SIGNATURE-----

Verify locally:

    curl -fSLO https://blog.alipour.eu/sources/posts/pi_service_touchup.md
    curl -fSLO https://blog.alipour.eu/sources/posts/pi_service_touchup.md.asc
    gpg --verify pi_service_touchup.md.asc pi_service_touchup.md

Open on GitHub ↗
Comments powered by Isso
RSS · Visitors this week: —