157 lines
No EOL
43 KiB
HTML
157 lines
No EOL
43 KiB
HTML
<!doctype html><html lang=en dir=auto><head><meta charset=utf-8><meta http-equiv=X-UA-Compatible content="IE=edge"><meta name=viewport content="width=device-width,initial-scale=1,shrink-to-fit=no"><meta name=robots content="noindex, nofollow"><title>Nextcloud on a Raspberry Pi, Fronted by a Tiny VPS — Nginx Reverse Proxy, Trusted Proxies, and Basic Auth for Jellyfin | AlipourIm journeys</title>
|
||
<meta name=keywords content="homelab,nginx,docker,nextcloud,jellyfin,cloudflare,tailscale,security,raspberrypi"><meta name=description content="Cloudflare DNS + Nginx on a VPS + Tailscale to the Pi. Small, boring, and reliable."><meta name=author content="Iman Alipour"><link rel=canonical href=https://blog.alipour.eu/posts/pi_service_touchup/><link crossorigin=anonymous href=/assets/css/stylesheet.31b150d909186c48d6dbbf653acc2489945fa9ac2c8f8cd3fe8448e89e40fccf.css integrity="sha256-MbFQ2QkYbEjW279lOswkiZRfqawsj4zT/oRI6J5A/M8=" rel="preload stylesheet" as=style><link rel=icon href=https://blog.alipour.eu/favicon.ico><link rel=icon type=image/png sizes=16x16 href=https://blog.alipour.eu/favicon-16x16.png><link rel=icon type=image/png sizes=32x32 href=https://blog.alipour.eu/favicon-32x32.png><link rel=apple-touch-icon href=https://blog.alipour.eu/apple-touch-icon.png><link rel=mask-icon href=https://blog.alipour.eu/safari-pinned-tab.svg><meta name=theme-color content="#2e2e33"><meta name=msapplication-TileColor content="#2e2e33"><link rel=alternate hreflang=en href=https://blog.alipour.eu/posts/pi_service_touchup/><noscript><style>#theme-toggle,.top-link{display:none}</style><style>@media(prefers-color-scheme:dark){:root{--theme:rgb(29, 30, 32);--entry:rgb(46, 46, 51);--primary:rgb(218, 218, 219);--secondary:rgb(155, 156, 157);--tertiary:rgb(65, 66, 68);--content:rgb(196, 196, 197);--code-block-bg:rgb(46, 46, 51);--code-bg:rgb(55, 56, 62);--border:rgb(51, 51, 51)}.list{background:var(--theme)}.list:not(.dark)::-webkit-scrollbar-track{background:0 0}.list:not(.dark)::-webkit-scrollbar-thumb{border-color:var(--theme)}}</style></noscript><script>(function(){window.goatcounter={endpoint:"/count"};const e=document.createElement("script");e.async=!0,e.src="/js/count.js",document.head.appendChild(e)})()</script><script>(function(){const s="",t=new Intl.NumberFormat,o="visit",i="visits";function a(e){document.readyState==="complete"||document.readyState==="interactive"?setTimeout(e,0):document.addEventListener("DOMContentLoaded",e)}function r(){try{return window.goatcounter&&window.goatcounter.get_data?window.goatcounter.get_data().p:location.pathname}catch{return location.pathname}}async function e(e,t){try{const o=new URLSearchParams({t:String(Date.now())});t&&o.set("start",t);const a=`${s}/counter/${encodeURIComponent(e)}.json?${o}`,n=await fetch(a,{credentials:"omit"});if(n.status===404)return 0;if(!n.ok)throw 0;const r=await n.json(),i=Number(String(r.count).replace(/,/g,""));return Number.isFinite(i)?i:0}catch{return null}}const n=e=>e===null?"—":`${t.format(e)} ${e===1?o:i}`;a(async function(){const s=document.querySelector(".post-single");if(s){const t=s.querySelector(".post-header .post-meta, .post-header .entry-meta, .post-meta"),o=await e(r());if(t){let e=t.querySelector(".post-views");e||(e=document.createElement("span"),e.className="post-views",t.appendChild(e)),e.textContent=n(o??0)}}const i=document.querySelectorAll("article.post-entry");await Promise.all(Array.from(i).map(async t=>{const s=t.querySelector(".entry-footer, .post-meta"),o=t.querySelector("a.entry-link, h2 a, .entry-title a");if(!s||!o)return;try{const i=new URL(o.getAttribute("href"),location.origin);if(i.origin!==location.origin)return;let t=s.querySelector(".post-views");t||(t=document.createElement("span"),t.className="post-views",s.appendChild(t)),t.textContent=n(await e(i.pathname)??0)}catch{}}));const o=document.getElementById("gc_site_visitors_week");if(o){const n=await e("TOTAL","week");o.textContent=n===null?"—":t.format(n)}})})()</script></head><body id=top><script>localStorage.getItem("pref-theme")==="dark"?document.body.classList.add("dark"):localStorage.getItem("pref-theme")==="light"?document.body.classList.remove("dark"):window.matchMedia("(prefers-color-scheme: dark)").matches&&document.body.classList.add("dark")</script><header class=header><nav class=nav><div class=logo><a href=https://blog.alipour.eu/ accesskey=h title="AlipourIm journeys (Alt + H)">AlipourIm journeys</a><div class=logo-switches><button id=theme-toggle accesskey=t title="(Alt + T)" aria-label="Toggle theme"><svg id="moon" width="24" height="18" viewBox="0 0 24 24" fill="none" stroke="currentcolor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z"/></svg><svg id="sun" width="24" height="18" viewBox="0 0 24 24" fill="none" stroke="currentcolor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg></button></div></div><ul id=menu><li><a href=https://blog.alipour.eu/posts/ title=Posts><span>Posts</span></a></li><li><a href=https://blog.alipour.eu/phd_journey/ title=PhD_journey><span>PhD_journey</span></a></li><li><a href=https://blog.alipour.eu/about/ title=About><span>About</span></a></li></ul></nav></header><main class=main><article class=post-single><header class=post-header><div class=breadcrumbs><a href=https://blog.alipour.eu/>Home</a> » <a href=https://blog.alipour.eu/posts/>Posts</a></div><h1 class="post-title entry-hint-parent">Nextcloud on a Raspberry Pi, Fronted by a Tiny VPS — Nginx Reverse Proxy, Trusted Proxies, and Basic Auth for Jellyfin</h1><div class=post-description>Cloudflare DNS + Nginx on a VPS + Tailscale to the Pi. Small, boring, and reliable.</div><div class=post-meta><span title='2026-02-02 00:00:00 +0000 UTC'>February 2, 2026</span> · 6 min · Iman Alipour
|
||
<span class=post-meta-item><a href=https://blog.alipour.eu/posts/pi_service_touchup/#isso-thread class=isso-comments-link>Comments</a></span></div></header><div class=post-content><blockquote><p>I didn’t “move Nextcloud to the cloud”.<br>I moved the <strong>front door</strong> to a VPS… and kept the house on my Raspberry Pi. 😄</p></blockquote><p>This post documents the setup I ended up with:</p><ul><li>A public <strong>VPS</strong> (host: <code>funbox</code>) running <strong>Nginx</strong> + <strong>Let’s Encrypt</strong></li><li>A private <strong>Raspberry Pi</strong> (host: <code>iot-hub</code>) running Docker services (<strong>Nextcloud</strong>, <strong>Jellyfin</strong>, …)</li><li>A private backhaul using <strong>Tailscale</strong> (the <code>100.x.y.z</code> network)</li><li>A correct Nextcloud reverse-proxy configuration (<strong>trusted_domains</strong>, <strong>trusted_proxies</strong>, and overwrite values)</li><li>A pragmatic security layer for media: <strong>Basic Auth at Nginx for Jellyfin</strong><br>(in addition to Jellyfin’s own login)</li></ul><p>I’m writing this as a “future me” note and a “copy-paste friendly” guide.</p><hr><h2 id=0-topology>0) Topology<a hidden class=anchor aria-hidden=true href=#0-topology>#</a></h2><p>The request path looks like:</p><div class=mermaid>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 / …</div><p>Why I like it:</p><ul><li>The Pi can sit behind home router / CGNAT and still be reachable.</li><li>TLS, redirects, headers, auth, rate limits… all centralized on the VPS.</li><li>Internal IPs can change without breaking public URLs.</li></ul><hr><h2 id=1-whats-running-on-the-pi>1) What’s running on the Pi?<a hidden class=anchor aria-hidden=true href=#1-whats-running-on-the-pi>#</a></h2><p>On <code>iot-hub</code> the “interesting” containers are:</p><ul><li><code>nextcloud</code> (Apache variant)</li><li><code>nextcloud-db</code> (Postgres)</li><li><code>nextcloud-redis</code></li><li><code>jellyfin</code></li></ul><p>Example <code>docker ps</code> style output (yours may vary):</p><div class=mermaid>nextcloud nextcloud:apache 0.0.0.0:8080->80/tcp
|
||
jellyfin jellyfin/jellyfin 0.0.0.0:8096->8096/tcp</div><p>So on the Pi, the services listen on:</p><ul><li><code>http://<pi>:8080</code> for Nextcloud</li><li><code>http://<pi>:8096</code> for Jellyfin</li></ul><p>But in my setup, <strong>the VPS does not reach those via LAN</strong> — it reaches them via Tailscale IPs.</p><hr><h2 id=2-tailscale-the-private-wire-between-vps-and-pi>2) Tailscale: the private wire between VPS and Pi<a hidden class=anchor aria-hidden=true href=#2-tailscale-the-private-wire-between-vps-and-pi>#</a></h2><p>Tailscale assigns each node an address like <code>100.xx.yy.zz</code>.</p><p>In my config, Nginx on <code>funbox</code> points to the Pi via Tailscale:</p><ul><li>Nextcloud upstream: <code>http://100.104.127.96:8080</code></li><li>Jellyfin upstream: <code>http://100.104.127.96:8096</code></li></ul><p>(Use the Tailscale IP of your Pi.)</p><p>Quick sanity checks from the VPS:</p><div class=highlight><pre tabindex=0 style=color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-bash data-lang=bash><span style=display:flex><span><span style=color:#75715e># from funbox, make sure you can reach the Pi service:</span>
|
||
</span></span><span style=display:flex><span>curl -I http://100.104.127.96:8080
|
||
</span></span><span style=display:flex><span>curl -I http://100.104.127.96:8096
|
||
</span></span></code></pre></div><p>If those don’t work: fix Tailscale connectivity first (ACLs, firewall, node online).</p><hr><h2 id=3-nginx-on-the-vps-reverse-proxy-blocks>3) Nginx on the VPS: reverse proxy blocks<a hidden class=anchor aria-hidden=true href=#3-nginx-on-the-vps-reverse-proxy-blocks>#</a></h2><h3 id=31-nextcloud-vhost-vps--pi-via-tailscale>3.1 Nextcloud vhost (VPS → Pi via Tailscale)<a hidden class=anchor aria-hidden=true href=#31-nextcloud-vhost-vps--pi-via-tailscale>#</a></h3><p>Create (or edit):</p><p><code>/etc/nginx/sites-available/nextcloud.alipourimjourneys.ir</code></p><p>and symlink into <code>sites-enabled</code>.</p><p>Here is a complete working example:</p><div class=highlight><pre tabindex=0 style=color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-nginx data-lang=nginx><span style=display:flex><span><span style=color:#75715e># Redirect HTTP → HTTPS
|
||
</span></span></span><span style=display:flex><span><span style=color:#75715e></span><span style=color:#66d9ef>server</span> {
|
||
</span></span><span style=display:flex><span> <span style=color:#f92672>listen</span> <span style=color:#ae81ff>80</span>;
|
||
</span></span><span style=display:flex><span> <span style=color:#f92672>listen</span> <span style=color:#e6db74>[::]:80</span>;
|
||
</span></span><span style=display:flex><span> <span style=color:#f92672>server_name</span> <span style=color:#e6db74>nextcloud.alipourimjourneys.ir</span>;
|
||
</span></span><span style=display:flex><span>
|
||
</span></span><span style=display:flex><span> <span style=color:#f92672>return</span> <span style=color:#ae81ff>301</span> <span style=color:#e6db74>https://</span>$host$request_uri;
|
||
</span></span><span style=display:flex><span>}
|
||
</span></span><span style=display:flex><span>
|
||
</span></span><span style=display:flex><span><span style=color:#66d9ef>server</span> {
|
||
</span></span><span style=display:flex><span> <span style=color:#f92672>listen</span> <span style=color:#ae81ff>443</span> <span style=color:#e6db74>ssl</span>;
|
||
</span></span><span style=display:flex><span> <span style=color:#f92672>listen</span> <span style=color:#e6db74>[::]:443</span> <span style=color:#e6db74>ssl</span>;
|
||
</span></span><span style=display:flex><span> <span style=color:#f92672>server_name</span> <span style=color:#e6db74>nextcloud.alipourimjourneys.ir</span>;
|
||
</span></span><span style=display:flex><span>
|
||
</span></span><span style=display:flex><span> <span style=color:#75715e># Certbot-managed certs
|
||
</span></span></span><span style=display:flex><span><span style=color:#75715e></span> <span style=color:#f92672>ssl_certificate</span> <span style=color:#e6db74>/etc/letsencrypt/live/nextcloud.alipourimjourneys.ir/fullchain.pem</span>;
|
||
</span></span><span style=display:flex><span> <span style=color:#f92672>ssl_certificate_key</span> <span style=color:#e6db74>/etc/letsencrypt/live/nextcloud.alipourimjourneys.ir/privkey.pem</span>;
|
||
</span></span><span style=display:flex><span> <span style=color:#f92672>include</span> <span style=color:#e6db74>/etc/letsencrypt/options-ssl-nginx.conf</span>;
|
||
</span></span><span style=display:flex><span> <span style=color:#f92672>ssl_dhparam</span> <span style=color:#e6db74>/etc/letsencrypt/ssl-dhparams.pem</span>;
|
||
</span></span><span style=display:flex><span>
|
||
</span></span><span style=display:flex><span> <span style=color:#75715e># Big uploads (tune to taste)
|
||
</span></span></span><span style=display:flex><span><span style=color:#75715e></span> <span style=color:#f92672>client_max_body_size</span> <span style=color:#e6db74>2G</span>;
|
||
</span></span><span style=display:flex><span>
|
||
</span></span><span style=display:flex><span> <span style=color:#75715e># CalDAV/CardDAV redirects
|
||
</span></span></span><span style=display:flex><span><span style=color:#75715e></span> <span style=color:#f92672>location</span> = <span style=color:#e6db74>/.well-known/carddav</span> { <span style=color:#f92672>return</span> <span style=color:#ae81ff>301</span> <span style=color:#e6db74>https://</span>$host/remote.php/dav/; }
|
||
</span></span><span style=display:flex><span> <span style=color:#f92672>location</span> = <span style=color:#e6db74>/.well-known/caldav</span> { <span style=color:#f92672>return</span> <span style=color:#ae81ff>301</span> <span style=color:#e6db74>https://</span>$host/remote.php/dav/; }
|
||
</span></span><span style=display:flex><span>
|
||
</span></span><span style=display:flex><span> <span style=color:#f92672>location</span> <span style=color:#e6db74>/</span> {
|
||
</span></span><span style=display:flex><span> <span style=color:#f92672>proxy_pass</span> <span style=color:#e6db74>http://100.104.127.96:8080</span>;
|
||
</span></span><span style=display:flex><span>
|
||
</span></span><span style=display:flex><span> <span style=color:#f92672>proxy_http_version</span> <span style=color:#ae81ff>1</span><span style=color:#e6db74>.1</span>;
|
||
</span></span><span style=display:flex><span>
|
||
</span></span><span style=display:flex><span> <span style=color:#f92672>proxy_set_header</span> <span style=color:#e6db74>Host</span> $host;
|
||
</span></span><span style=display:flex><span> <span style=color:#f92672>proxy_set_header</span> <span style=color:#e6db74>X-Real-IP</span> $remote_addr;
|
||
</span></span><span style=display:flex><span> <span style=color:#f92672>proxy_set_header</span> <span style=color:#e6db74>X-Forwarded-For</span> $proxy_add_x_forwarded_for;
|
||
</span></span><span style=display:flex><span> <span style=color:#f92672>proxy_set_header</span> <span style=color:#e6db74>X-Forwarded-Proto</span> $scheme;
|
||
</span></span><span style=display:flex><span>
|
||
</span></span><span style=display:flex><span> <span style=color:#75715e># Sometimes helps apps behind multiple proxies
|
||
</span></span></span><span style=display:flex><span><span style=color:#75715e></span> <span style=color:#f92672>proxy_set_header</span> <span style=color:#e6db74>X-Forwarded-Host</span> $host;
|
||
</span></span><span style=display:flex><span> <span style=color:#f92672>proxy_set_header</span> <span style=color:#e6db74>X-Forwarded-Port</span> $server_port;
|
||
</span></span><span style=display:flex><span>
|
||
</span></span><span style=display:flex><span> <span style=color:#75715e># Nextcloud + WebDAV can do long requests
|
||
</span></span></span><span style=display:flex><span><span style=color:#75715e></span> <span style=color:#f92672>proxy_read_timeout</span> <span style=color:#ae81ff>3600</span>;
|
||
</span></span><span style=display:flex><span> <span style=color:#f92672>proxy_send_timeout</span> <span style=color:#ae81ff>3600</span>;
|
||
</span></span><span style=display:flex><span>
|
||
</span></span><span style=display:flex><span> <span style=color:#75715e># Usually good for DAV/uploads
|
||
</span></span></span><span style=display:flex><span><span style=color:#75715e></span> <span style=color:#f92672>proxy_buffering</span> <span style=color:#66d9ef>off</span>;
|
||
</span></span><span style=display:flex><span> <span style=color:#f92672>proxy_request_buffering</span> <span style=color:#66d9ef>off</span>;
|
||
</span></span><span style=display:flex><span> }
|
||
</span></span><span style=display:flex><span>}
|
||
</span></span></code></pre></div><p>Then test + reload:</p><div class=highlight><pre tabindex=0 style=color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-bash data-lang=bash><span style=display:flex><span>sudo nginx -t
|
||
</span></span><span style=display:flex><span>sudo systemctl reload nginx
|
||
</span></span></code></pre></div><h3 id=32-jellyfin-vhost-with-basic-auth>3.2 Jellyfin vhost (with Basic Auth)<a hidden class=anchor aria-hidden=true href=#32-jellyfin-vhost-with-basic-auth>#</a></h3><p>Create:</p><p><code>/etc/nginx/sites-available/jellyfin.alipourimjourneys.ir</code></p><p>Example:</p><div class=highlight><pre tabindex=0 style=color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-nginx data-lang=nginx><span style=display:flex><span><span style=color:#66d9ef>server</span> {
|
||
</span></span><span style=display:flex><span> <span style=color:#f92672>listen</span> <span style=color:#ae81ff>80</span>;
|
||
</span></span><span style=display:flex><span> <span style=color:#f92672>listen</span> <span style=color:#e6db74>[::]:80</span>;
|
||
</span></span><span style=display:flex><span> <span style=color:#f92672>server_name</span> <span style=color:#e6db74>jellyfin.alipourimjourneys.ir</span>;
|
||
</span></span><span style=display:flex><span> <span style=color:#f92672>return</span> <span style=color:#ae81ff>301</span> <span style=color:#e6db74>https://</span>$host$request_uri;
|
||
</span></span><span style=display:flex><span>}
|
||
</span></span><span style=display:flex><span>
|
||
</span></span><span style=display:flex><span><span style=color:#66d9ef>server</span> {
|
||
</span></span><span style=display:flex><span> <span style=color:#f92672>listen</span> <span style=color:#ae81ff>443</span> <span style=color:#e6db74>ssl</span>;
|
||
</span></span><span style=display:flex><span> <span style=color:#f92672>listen</span> <span style=color:#e6db74>[::]:443</span> <span style=color:#e6db74>ssl</span>;
|
||
</span></span><span style=display:flex><span> <span style=color:#f92672>server_name</span> <span style=color:#e6db74>jellyfin.alipourimjourneys.ir</span>;
|
||
</span></span><span style=display:flex><span>
|
||
</span></span><span style=display:flex><span> <span style=color:#f92672>ssl_certificate</span> <span style=color:#e6db74>/etc/letsencrypt/live/jellyfin.alipourimjourneys.ir/fullchain.pem</span>;
|
||
</span></span><span style=display:flex><span> <span style=color:#f92672>ssl_certificate_key</span> <span style=color:#e6db74>/etc/letsencrypt/live/jellyfin.alipourimjourneys.ir/privkey.pem</span>;
|
||
</span></span><span style=display:flex><span> <span style=color:#f92672>include</span> <span style=color:#e6db74>/etc/letsencrypt/options-ssl-nginx.conf</span>;
|
||
</span></span><span style=display:flex><span> <span style=color:#f92672>ssl_dhparam</span> <span style=color:#e6db74>/etc/letsencrypt/ssl-dhparams.pem</span>;
|
||
</span></span><span style=display:flex><span>
|
||
</span></span><span style=display:flex><span> <span style=color:#f92672>client_max_body_size</span> <span style=color:#e6db74>512M</span>;
|
||
</span></span><span style=display:flex><span>
|
||
</span></span><span style=display:flex><span> <span style=color:#75715e># ✅ Basic Auth gate (extra layer before Jellyfin)
|
||
</span></span></span><span style=display:flex><span><span style=color:#75715e></span> <span style=color:#f92672>auth_basic</span> <span style=color:#e6db74>"Jellyfin</span> <span style=color:#e6db74>(private)"</span>;
|
||
</span></span><span style=display:flex><span> <span style=color:#f92672>auth_basic_user_file</span> <span style=color:#e6db74>/etc/nginx/.htpasswd-jellyfin</span>;
|
||
</span></span><span style=display:flex><span>
|
||
</span></span><span style=display:flex><span> <span style=color:#f92672>location</span> <span style=color:#e6db74>/</span> {
|
||
</span></span><span style=display:flex><span> <span style=color:#f92672>proxy_pass</span> <span style=color:#e6db74>http://100.104.127.96:8096</span>;
|
||
</span></span><span style=display:flex><span> <span style=color:#f92672>proxy_http_version</span> <span style=color:#ae81ff>1</span><span style=color:#e6db74>.1</span>;
|
||
</span></span><span style=display:flex><span>
|
||
</span></span><span style=display:flex><span> <span style=color:#f92672>proxy_set_header</span> <span style=color:#e6db74>Host</span> $host;
|
||
</span></span><span style=display:flex><span> <span style=color:#f92672>proxy_set_header</span> <span style=color:#e6db74>X-Real-IP</span> $remote_addr;
|
||
</span></span><span style=display:flex><span> <span style=color:#f92672>proxy_set_header</span> <span style=color:#e6db74>X-Forwarded-For</span> $proxy_add_x_forwarded_for;
|
||
</span></span><span style=display:flex><span> <span style=color:#f92672>proxy_set_header</span> <span style=color:#e6db74>X-Forwarded-Proto</span> $scheme;
|
||
</span></span><span style=display:flex><span>
|
||
</span></span><span style=display:flex><span> <span style=color:#75715e># Jellyfin uses websockets
|
||
</span></span></span><span style=display:flex><span><span style=color:#75715e></span> <span style=color:#f92672>proxy_set_header</span> <span style=color:#e6db74>Upgrade</span> $http_upgrade;
|
||
</span></span><span style=display:flex><span> <span style=color:#f92672>proxy_set_header</span> <span style=color:#e6db74>Connection</span> <span style=color:#e6db74>"upgrade"</span>;
|
||
</span></span><span style=display:flex><span>
|
||
</span></span><span style=display:flex><span> <span style=color:#f92672>proxy_read_timeout</span> <span style=color:#ae81ff>3600</span>;
|
||
</span></span><span style=display:flex><span> <span style=color:#f92672>proxy_send_timeout</span> <span style=color:#ae81ff>3600</span>;
|
||
</span></span><span style=display:flex><span>
|
||
</span></span><span style=display:flex><span> <span style=color:#f92672>proxy_buffering</span> <span style=color:#66d9ef>off</span>;
|
||
</span></span><span style=display:flex><span> }
|
||
</span></span><span style=display:flex><span>}
|
||
</span></span></code></pre></div><p>Enable:</p><div class=highlight><pre tabindex=0 style=color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-bash data-lang=bash><span style=display:flex><span>sudo ln -s /etc/nginx/sites-available/jellyfin.alipourimjourneys.ir /etc/nginx/sites-enabled/
|
||
</span></span><span style=display:flex><span>sudo nginx -t
|
||
</span></span><span style=display:flex><span>sudo systemctl reload nginx
|
||
</span></span></code></pre></div><hr><h2 id=4-creating-the-basic-auth-password-file>4) Creating the Basic Auth password file<a hidden class=anchor aria-hidden=true href=#4-creating-the-basic-auth-password-file>#</a></h2><p>Install tools (Debian/Ubuntu):</p><div class=highlight><pre tabindex=0 style=color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-bash data-lang=bash><span style=display:flex><span>sudo apt-get update
|
||
</span></span><span style=display:flex><span>sudo apt-get install -y apache2-utils
|
||
</span></span></code></pre></div><p>Create the password file:</p><div class=highlight><pre tabindex=0 style=color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-bash data-lang=bash><span style=display:flex><span>sudo htpasswd -c /etc/nginx/.htpasswd-jellyfin yourusername
|
||
</span></span></code></pre></div><p>(If adding more users later, omit <code>-c</code>.)</p><p>Lock it down:</p><div class=highlight><pre tabindex=0 style=color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-bash data-lang=bash><span style=display:flex><span>sudo chown root:root /etc/nginx/.htpasswd-jellyfin
|
||
</span></span><span style=display:flex><span>sudo chmod <span style=color:#ae81ff>640</span> /etc/nginx/.htpasswd-jellyfin
|
||
</span></span></code></pre></div><h3 id=notes-on-clients>Notes on clients<a hidden class=anchor aria-hidden=true href=#notes-on-clients>#</a></h3><ul><li>Most browsers + most Jellyfin apps handle HTTP Basic Auth fine.</li><li>Some TV apps can be quirky. If a client can’t handle it, you can:<ul><li>remove Basic Auth, or</li><li>keep it only on selected paths, or</li><li>use Cloudflare Access in front of Jellyfin instead (more work).</li></ul></li></ul><hr><h2 id=5-nextcloud-configure-it-to-behave-behind-the-reverse-proxy>5) Nextcloud: configure it to behave behind the reverse proxy<a hidden class=anchor aria-hidden=true href=#5-nextcloud-configure-it-to-behave-behind-the-reverse-proxy>#</a></h2><p>Nextcloud needs to know:</p><ul><li>what hostnames are valid,</li><li>which proxy is trusted,</li><li>and what the “outside” URL scheme is.</li></ul><p>You can do it via <code>occ</code> inside the container (Apache image uses <code>www-data</code>).</p><h3 id=51-trusted_domains>5.1 trusted_domains<a hidden class=anchor aria-hidden=true href=#51-trusted_domains>#</a></h3><p>Check current values:</p><div class=highlight><pre tabindex=0 style=color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-bash data-lang=bash><span style=display:flex><span>docker exec -u www-data nextcloud php /var/www/html/occ config:system:get trusted_domains
|
||
</span></span></code></pre></div><p>Add your public domain:</p><div class=highlight><pre tabindex=0 style=color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-bash data-lang=bash><span style=display:flex><span>docker exec -u www-data nextcloud php /var/www/html/occ config:system:set trusted_domains <span style=color:#ae81ff>1</span> --value<span style=color:#f92672>=</span><span style=color:#e6db74>"nextcloud.alipourimjourneys.ir"</span>
|
||
</span></span></code></pre></div><p>(Keep your internal name too if you still use it, e.g. <code>rpi:8080</code>.)</p><h3 id=52-trusted_proxies>5.2 trusted_proxies<a hidden class=anchor aria-hidden=true href=#52-trusted_proxies>#</a></h3><p>Because requests arrive from the VPS (over Tailscale), Nextcloud must trust that proxy IP.</p><p>Example:</p><div class=highlight><pre tabindex=0 style=color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-bash data-lang=bash><span style=display:flex><span>docker exec -u www-data nextcloud php /var/www/html/occ config:system:set trusted_proxies <span style=color:#ae81ff>0</span> --value<span style=color:#f92672>=</span><span style=color:#e6db74>"100.99.79.75"</span>
|
||
</span></span></code></pre></div><p>(Use the <strong>VPS’s Tailscale IP</strong> as seen from the Pi.)</p><h3 id=53-overwritehost--overwriteprotocol--overwritecliurl>5.3 overwritehost / overwriteprotocol / overwrite.cli.url<a hidden class=anchor aria-hidden=true href=#53-overwritehost--overwriteprotocol--overwritecliurl>#</a></h3><p>Tell Nextcloud “the world sees me as <a href=https://nextcloud.example>https://nextcloud.example</a>”:</p><div class=highlight><pre tabindex=0 style=color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-bash data-lang=bash><span style=display:flex><span>docker exec -u www-data nextcloud php /var/www/html/occ config:system:set overwritehost --value<span style=color:#f92672>=</span><span style=color:#e6db74>"nextcloud.alipourimjourneys.ir"</span>
|
||
</span></span><span style=display:flex><span>docker exec -u www-data nextcloud php /var/www/html/occ config:system:set overwriteprotocol --value<span style=color:#f92672>=</span><span style=color:#e6db74>"https"</span>
|
||
</span></span><span style=display:flex><span>docker exec -u www-data nextcloud php /var/www/html/occ config:system:set overwrite.cli.url --value<span style=color:#f92672>=</span><span style=color:#e6db74>"https://nextcloud.alipourimjourneys.ir"</span>
|
||
</span></span></code></pre></div><h3 id=54-forwarded_for_headers-optional-but-often-helpful>5.4 forwarded_for_headers (optional, but often helpful)<a hidden class=anchor aria-hidden=true href=#54-forwarded_for_headers-optional-but-often-helpful>#</a></h3><div class=highlight><pre tabindex=0 style=color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-bash data-lang=bash><span style=display:flex><span>docker exec -u www-data nextcloud php /var/www/html/occ config:system:set forwarded_for_headers <span style=color:#ae81ff>0</span> --value<span style=color:#f92672>=</span><span style=color:#e6db74>"HTTP_X_FORWARDED_FOR"</span>
|
||
</span></span></code></pre></div><p>Restart Nextcloud container after config:</p><div class=highlight><pre tabindex=0 style=color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-bash data-lang=bash><span style=display:flex><span>docker restart nextcloud
|
||
</span></span></code></pre></div><hr><h2 id=6-sanity-checks-curl-is-your-friend>6) Sanity checks (curl is your friend)<a hidden class=anchor aria-hidden=true href=#6-sanity-checks-curl-is-your-friend>#</a></h2><p>From anywhere public:</p><div class=highlight><pre tabindex=0 style=color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-bash data-lang=bash><span style=display:flex><span>curl -I http://nextcloud.alipourimjourneys.ir
|
||
</span></span><span style=display:flex><span>curl -I https://nextcloud.alipourimjourneys.ir
|
||
</span></span><span style=display:flex><span>curl -I https://nextcloud.alipourimjourneys.ir/.well-known/caldav
|
||
</span></span></code></pre></div><p>Expected “good signs”:</p><ul><li>HTTP returns <strong>301</strong> to HTTPS</li><li>HTTPS returns <strong>302</strong> to <code>/login</code> (or 200 if already authenticated)</li><li><code>/.well-known/caldav</code> returns <strong>301</strong> to <code>/remote.php/dav/</code></li></ul><p>If you see redirect loops or wrong hostnames:</p><ul><li>revisit <code>overwritehost</code>, <code>overwriteprotocol</code>, <code>trusted_proxies</code>.</li></ul><hr><h2 id=7-do-i-really-need-cloudflare-access--warp>7) “Do I really need Cloudflare Access / WARP?”<a hidden class=anchor aria-hidden=true href=#7-do-i-really-need-cloudflare-access--warp>#</a></h2><h3 id=the-honest-answer>The honest answer<a hidden class=anchor aria-hidden=true href=#the-honest-answer>#</a></h3><p>If your setup is:</p><ul><li>HTTPS only</li><li>strong passwords + MFA in Nextcloud/Jellyfin</li><li>your origin isn’t directly exposed (only the VPS is public)</li><li>you keep things patched</li></ul><p>…then you’re already in a <strong>reasonable</strong> place.</p><h3 id=can-i-skip-cloudflare-access>“Can I skip Cloudflare Access?”<a hidden class=anchor aria-hidden=true href=#can-i-skip-cloudflare-access>#</a></h3><p>Yes. In this topology, Cloudflare Access is optional. The main security boundary is:</p><ul><li><strong>Public:</strong> VPS + Nginx</li><li><strong>Private:</strong> Pi over Tailscale</li></ul><p>For Jellyfin, Basic Auth adds a cheap extra gate that’s “family friendly”.</p><hr><h2 id=8-cloudflare-access-one-time-pin-not-arriving--passkeys>8) Cloudflare Access: One-time PIN not arriving + passkeys<a hidden class=anchor aria-hidden=true href=#8-cloudflare-access-one-time-pin-not-arriving--passkeys>#</a></h2><p>Two common gotchas:</p><h3 id=81-one-time-pin-email-didnt-arrive>8.1 One-time PIN email didn’t arrive<a hidden class=anchor aria-hidden=true href=#81-one-time-pin-email-didnt-arrive>#</a></h3><p>That flow relies on email delivery. Check:</p><ul><li>spam/junk folder</li><li>if your provider blocked it</li><li>the exact email allowlist in your policy</li></ul><p>If it’s flaky, I’d avoid One-time PIN and use a real identity provider.</p><h3 id=82-can-i-use-passkeys--apple--google>8.2 Can I use passkeys / Apple / Google?<a hidden class=anchor aria-hidden=true href=#82-can-i-use-passkeys--apple--google>#</a></h3><p>Yes — but passkeys typically come via an identity provider (IdP) that supports WebAuthn/passkeys.
|
||
Practical approach:</p><ul><li>pick what your family already uses (Google or Apple),</li><li>configure that as the login method,</li><li>avoid WARP enrollment unless you specifically want device-based access.</li></ul><hr><h2 id=9-hardening-checklist-tiny-but-effective>9) Hardening checklist (tiny but effective)<a hidden class=anchor aria-hidden=true href=#9-hardening-checklist-tiny-but-effective>#</a></h2><p>On the VPS:</p><ul><li>Keep Ubuntu security updates on</li><li>firewall: allow only what you need (22/80/443)</li><li>optional: <code>fail2ban</code> for SSH</li></ul><p>On the Pi:</p><ul><li>keep Docker images updated</li><li>Postgres/Redis not exposed publicly (Docker internal network)</li><li>backups: Nextcloud data + DB</li></ul><hr><h2 id=10-tldr>10) TL;DR<a hidden class=anchor aria-hidden=true href=#10-tldr>#</a></h2><ul><li>VPS Nginx terminates TLS, proxies to Pi over Tailscale</li><li>Nextcloud must be told about:<ul><li><code>trusted_domains</code></li><li><code>trusted_proxies</code></li><li>overwrite values (<code>overwritehost</code>, <code>overwriteprotocol</code>, <code>overwrite.cli.url</code>)</li></ul></li><li><code>curl -I</code> should show sane redirects + <code>/remote.php/dav/</code></li><li>Jellyfin gets an extra gate with <strong>Nginx Basic Auth</strong></li></ul><p>Boring is good. Boring runs for months.</p><hr><hr><div class=signature-block style=margin-top:1rem><p><strong>Downloads:</strong>
|
||
<a href=/sources/posts/pi_service_touchup.md>Markdown</a> ·
|
||
<a href=/sources/posts/pi_service_touchup.md.asc>Signature (.asc)</a></p><details class=signature><summary>View OpenPGP signature</summary><pre style=font-size:.85em;overflow-x:auto;padding:.75rem>-----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-----
|
||
</pre></details><p><em>Verify locally:</em></p><pre style=font-size:.85em;overflow-x:auto;padding:.75rem>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
|
||
</pre></div></div><footer class=post-footer><ul class=post-tags><li><a href=https://blog.alipour.eu/tags/homelab/>Homelab</a></li><li><a href=https://blog.alipour.eu/tags/nginx/>Nginx</a></li><li><a href=https://blog.alipour.eu/tags/docker/>Docker</a></li><li><a href=https://blog.alipour.eu/tags/nextcloud/>Nextcloud</a></li><li><a href=https://blog.alipour.eu/tags/jellyfin/>Jellyfin</a></li><li><a href=https://blog.alipour.eu/tags/cloudflare/>Cloudflare</a></li><li><a href=https://blog.alipour.eu/tags/tailscale/>Tailscale</a></li><li><a href=https://blog.alipour.eu/tags/security/>Security</a></li><li><a href=https://blog.alipour.eu/tags/raspberrypi/>Raspberrypi</a></li></ul><nav class=paginav><a class=prev href=https://blog.alipour.eu/posts/h3ll0_fr1end/><span class=title>« Prev</span><br><span>H3ll0 Fr1end</span>
|
||
</a><a class=next href=https://blog.alipour.eu/posts/blackout/><span class=title>Next »</span><br><span>Blackout</span></a></nav></footer><div class=comments-switch style="display:flex;gap:.5rem;margin:.5rem 0 1rem"><button id=btn-isso type=button aria-pressed=true>Anonymous (Isso)</button>
|
||
<button id=btn-giscus type=button aria-pressed=false>GitHub (Giscus)</button>
|
||
<a href=https://github.com/AlipourIm/blog-comments/discussions/categories/comments target=_blank rel=noopener>Open on GitHub ↗</a></div><div id=panel-giscus style=display:none><section id=giscus-thread></section></div><div id=panel-isso><section id=isso-thread data-isso-id=/posts/pi_service_touchup/ data-title="Nextcloud on a Raspberry Pi, Fronted by a Tiny VPS — Nginx Reverse Proxy, Trusted Proxies, and Basic Auth for Jellyfin"><noscript>Javascript needs to be activated to view comments.</noscript></section><script src=/isso/js/embed.min.js data-isso=/isso/ data-isso-css=false data-isso-lang=en data-isso-max-comments-nested=5 data-isso-sorting=newest async></script><small class=isso-powered style="display:block;margin:.5rem 0;color:var(--secondary,#888)">Comments powered by <a href=https://isso-comments.de target=_blank rel=noopener>Isso</a></small></div><script>(function(){var i=document.getElementById("panel-isso"),a=document.getElementById("panel-giscus"),e=document.getElementById("btn-isso"),t=document.getElementById("btn-giscus"),s=!1;function r(){if(n=localStorage.getItem("pref-theme"),n==="dark")return!0;if(n==="light")return!1;var n,e=document.documentElement,t=document.body;return!!(e.classList&&e.classList.contains("dark")||t.classList&&t.classList.contains("dark")||e.getAttribute("data-theme")==="dark"||t.getAttribute("data-theme")==="dark"||e.classList&&e.classList.contains("theme-dark")||t.classList&&t.classList.contains("theme-dark"))||window.matchMedia&&window.matchMedia("(prefers-color-scheme: dark)").matches}function o(){return r()?"dark_dimmed":"noborder_light"}function c(){if(s)return;var t,n,e=document.createElement("script");e.src="https://giscus.app/client.js",e.async=!0,e.crossOrigin="anonymous",e.setAttribute("data-repo","AlipourIm/blog-comments"),e.setAttribute("data-repo-id","R_kgDOQGARyA"),e.setAttribute("data-category","Comments"),e.setAttribute("data-category-id","DIC_kwDOQGARyM4Cw3x-"),e.setAttribute("data-mapping","pathname"),e.setAttribute("data-strict","0"),e.setAttribute("data-reactions-enabled","1"),e.setAttribute("data-emit-metadata","0"),e.setAttribute("data-input-position","bottom"),e.setAttribute("data-lang","en"),e.setAttribute("data-theme",o()),document.getElementById("giscus-thread").appendChild(e);function i(){var e=document.querySelector("iframe.giscus-frame");e&&e.contentWindow&&e.contentWindow.postMessage({giscus:{setConfig:{theme:o()}}},"https://giscus.app")}t=document.getElementById("theme-toggle"),t&&t.addEventListener("click",function(){setTimeout(i,0)}),n=new MutationObserver(function(){i()}),n.observe(document.documentElement,{attributes:!0,attributeFilter:["class","data-theme"]}),n.observe(document.body,{attributes:!0,attributeFilter:["class","data-theme"]}),s=!0}function n(n){var s=n==="isso";i.style.display=s?"block":"none",a.style.display=s?"none":"block",e&&e.setAttribute("aria-pressed",s?"true":"false"),t&&t.setAttribute("aria-pressed",s?"false":"true"),s||c()}e&&e.addEventListener("click",function(e){e.preventDefault(),n("isso")}),t&&t.addEventListener("click",function(e){e.preventDefault(),n("giscus")}),n("isso")})()</script></article></main><footer class=footer><span>© 2026 <a href=https://blog.alipour.eu/>AlipourIm journeys</a></span> ·
|
||
<span>Powered by
|
||
<a href=https://gohugo.io/ rel="noopener noreferrer" target=_blank>Hugo</a> &
|
||
<a href=https://github.com/adityatelange/hugo-PaperMod/ rel=noopener target=_blank>PaperMod</a></span></footer><a href=#top aria-label="go to top" title="Go to Top (Alt + G)" class=top-link id=top-link accesskey=g><svg viewBox="0 0 12 6" fill="currentcolor"><path d="M12 6H0l6-6z"/></svg></a><div class=site-meta-row style=display:flex;align-items:center;gap:.75rem;flex-wrap:wrap><a href=/index.xml rel=alternate type=application/rss+xml title=RSS class=rss-link style=display:inline-flex;align-items:center;gap:.35rem><svg viewBox="0 0 24 24" fill="none" stroke="currentcolor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 11a9 9 0 019 9"/><path d="M4 4a16 16 0 0116 16"/><circle cx="5" cy="19" r="1"/></svg>
|
||
<span>RSS</span>
|
||
</a><span aria-hidden=true>·</span>
|
||
<span>Visitors this week: <span id=gc_site_visitors_week>—</span></span></div><script>let menu=document.getElementById("menu");menu&&(menu.scrollLeft=localStorage.getItem("menu-scroll-position"),menu.onscroll=function(){localStorage.setItem("menu-scroll-position",menu.scrollLeft)}),document.querySelectorAll('a[href^="#"]').forEach(e=>{e.addEventListener("click",function(e){e.preventDefault();var t=this.getAttribute("href").substr(1);window.matchMedia("(prefers-reduced-motion: reduce)").matches?document.querySelector(`[id='${decodeURIComponent(t)}']`).scrollIntoView():document.querySelector(`[id='${decodeURIComponent(t)}']`).scrollIntoView({behavior:"smooth"}),t==="top"?history.replaceState(null,null," "):history.pushState(null,null,`#${t}`)})})</script><script>var mybutton=document.getElementById("top-link");window.onscroll=function(){document.body.scrollTop>800||document.documentElement.scrollTop>800?(mybutton.style.visibility="visible",mybutton.style.opacity="1"):(mybutton.style.visibility="hidden",mybutton.style.opacity="0")}</script><script>document.getElementById("theme-toggle").addEventListener("click",()=>{document.body.className.includes("dark")?(document.body.classList.remove("dark"),localStorage.setItem("pref-theme","light")):(document.body.classList.add("dark"),localStorage.setItem("pref-theme","dark"))})</script><script>document.querySelectorAll("pre > code").forEach(e=>{const n=e.parentNode.parentNode,t=document.createElement("button");t.classList.add("copy-code"),t.innerHTML="copy";function s(){t.innerHTML="copied!",setTimeout(()=>{t.innerHTML="copy"},2e3)}t.addEventListener("click",t=>{if("clipboard"in navigator){navigator.clipboard.writeText(e.textContent),s();return}const n=document.createRange();n.selectNodeContents(e);const o=window.getSelection();o.removeAllRanges(),o.addRange(n);try{document.execCommand("copy"),s()}catch{}o.removeRange(n)}),n.classList.contains("highlight")?n.appendChild(t):n.parentNode.firstChild==n||(e.parentNode.parentNode.parentNode.parentNode.parentNode.nodeName=="TABLE"?e.parentNode.parentNode.parentNode.parentNode.parentNode.appendChild(t):e.parentNode.appendChild(t))})</script></body></html> |