874 lines
51 KiB
HTML
874 lines
51 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="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/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="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/favicon.ico">
|
||
<link rel="icon" type="image/png" sizes="16x16" href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/favicon-16x16.png">
|
||
<link rel="icon" type="image/png" sizes="32x32" href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/favicon-32x32.png">
|
||
<link rel="apple-touch-icon" href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/apple-touch-icon.png">
|
||
<link rel="mask-icon" href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/safari-pinned-tab.svg">
|
||
<meta name="theme-color" content="#2e2e33">
|
||
<meta name="msapplication-TileColor" content="#2e2e33">
|
||
<link rel="alternate" hreflang="en" href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/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 s = document.createElement("script");
|
||
s.async = true;
|
||
s.src = "/js/count.js";
|
||
document.head.appendChild(s);
|
||
})();
|
||
</script>
|
||
|
||
<script>
|
||
(function () {
|
||
|
||
const GC = "";
|
||
const nf = new Intl.NumberFormat();
|
||
const SING = "visit";
|
||
const PLUR = "visits";
|
||
|
||
function ready(fn) {
|
||
if (document.readyState === "complete" || document.readyState === "interactive") setTimeout(fn, 0);
|
||
else document.addEventListener("DOMContentLoaded", fn);
|
||
}
|
||
|
||
function gcPath() {
|
||
try {
|
||
return window.goatcounter && window.goatcounter.get_data
|
||
? window.goatcounter.get_data().p
|
||
: location.pathname;
|
||
} catch (_) {
|
||
return location.pathname;
|
||
}
|
||
}
|
||
|
||
async function fetchCount(path, period) {
|
||
try {
|
||
const q = new URLSearchParams({ t: String(Date.now()) });
|
||
if (period) q.set("start", period);
|
||
const url = `${GC}/counter/${encodeURIComponent(path)}.json?${q}`;
|
||
const r = await fetch(url, { credentials: "omit" });
|
||
if (r.status === 404) return 0;
|
||
if (!r.ok) throw 0;
|
||
const j = await r.json();
|
||
|
||
const n = Number(String(j.count).replace(/,/g, ""));
|
||
return Number.isFinite(n) ? n : 0;
|
||
} catch {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
const labelVisits = (n) =>
|
||
n === null ? "—" : `${nf.format(n)} ${n === 1 ? SING : PLUR}`;
|
||
|
||
ready(async function () {
|
||
const post = document.querySelector(".post-single");
|
||
if (post) {
|
||
const meta = post.querySelector(
|
||
".post-header .post-meta, .post-header .entry-meta, .post-meta"
|
||
);
|
||
const visits = await fetchCount(gcPath());
|
||
if (meta) {
|
||
let span = meta.querySelector(".post-views");
|
||
if (!span) {
|
||
span = document.createElement("span");
|
||
span.className = "post-views";
|
||
meta.appendChild(span);
|
||
}
|
||
span.textContent = labelVisits(visits ?? 0);
|
||
}
|
||
}
|
||
|
||
const cards = document.querySelectorAll("article.post-entry");
|
||
await Promise.all(
|
||
Array.from(cards).map(async (card) => {
|
||
const meta = card.querySelector(".entry-footer, .post-meta");
|
||
const link = card.querySelector("a.entry-link, h2 a, .entry-title a");
|
||
if (!meta || !link) return;
|
||
try {
|
||
const u = new URL(link.getAttribute("href"), location.origin);
|
||
if (u.origin !== location.origin) return;
|
||
let span = meta.querySelector(".post-views");
|
||
if (!span) {
|
||
span = document.createElement("span");
|
||
span.className = "post-views";
|
||
meta.appendChild(span);
|
||
}
|
||
span.textContent = labelVisits((await fetchCount(u.pathname)) ?? 0);
|
||
} catch {}
|
||
})
|
||
);
|
||
|
||
const uvEl = document.getElementById("gc_site_visitors_week");
|
||
if (uvEl) {
|
||
const n = await fetchCount("TOTAL", "week");
|
||
uvEl.textContent = n === null ? "—" : nf.format(n);
|
||
}
|
||
});
|
||
})();
|
||
</script>
|
||
|
||
</head>
|
||
|
||
<body class="" id="top">
|
||
<script>
|
||
if (localStorage.getItem("pref-theme") === "dark") {
|
||
document.body.classList.add('dark');
|
||
} else if (localStorage.getItem("pref-theme") === "light") {
|
||
document.body.classList.remove('dark')
|
||
} else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||
document.body.classList.add('dark');
|
||
}
|
||
|
||
</script>
|
||
|
||
<header class="header">
|
||
<nav class="nav">
|
||
<div class="logo">
|
||
<a href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/" 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" xmlns="http://www.w3.org/2000/svg" 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 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
|
||
</svg>
|
||
<svg id="sun" xmlns="http://www.w3.org/2000/svg" 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"></circle>
|
||
<line x1="12" y1="1" x2="12" y2="3"></line>
|
||
<line x1="12" y1="21" x2="12" y2="23"></line>
|
||
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
|
||
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
|
||
<line x1="1" y1="12" x2="3" y2="12"></line>
|
||
<line x1="21" y1="12" x2="23" y2="12"></line>
|
||
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
|
||
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<ul id="menu">
|
||
<li>
|
||
<a href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/posts/" title="Posts">
|
||
<span>Posts</span>
|
||
</a>
|
||
</li>
|
||
<li>
|
||
<a href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/phd_journey/" title="PhD_journey">
|
||
<span>PhD_journey</span>
|
||
</a>
|
||
</li>
|
||
<li>
|
||
<a href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/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="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/">Home</a> » <a href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/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="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/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:0.85em; overflow-x:auto; padding:0.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:0.85em; overflow-x:auto; padding:0.75rem;">torsocks curl -fSLO http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/sources/posts/pi_service_touchup.md
|
||
torsocks curl -fSLO http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/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="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/tags/homelab/">Homelab</a></li>
|
||
<li><a href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/tags/nginx/">Nginx</a></li>
|
||
<li><a href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/tags/docker/">Docker</a></li>
|
||
<li><a href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/tags/nextcloud/">Nextcloud</a></li>
|
||
<li><a href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/tags/jellyfin/">Jellyfin</a></li>
|
||
<li><a href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/tags/cloudflare/">Cloudflare</a></li>
|
||
<li><a href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/tags/tailscale/">Tailscale</a></li>
|
||
<li><a href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/tags/security/">Security</a></li>
|
||
<li><a href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/tags/raspberrypi/">Raspberrypi</a></li>
|
||
</ul>
|
||
<nav class="paginav">
|
||
<a class="prev" href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/posts/h3ll0_fr1end/">
|
||
<span class="title">« Prev</span>
|
||
<br>
|
||
<span>H3ll0 Fr1end</span>
|
||
</a>
|
||
<a class="next" href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/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 panelIsso = document.getElementById('panel-isso');
|
||
var panelGisc = document.getElementById('panel-giscus');
|
||
var issoBtn = document.getElementById('btn-isso');
|
||
var giscBtn = document.getElementById('btn-giscus');
|
||
var gLoaded = false;
|
||
|
||
function isDark() {
|
||
var pref = localStorage.getItem('pref-theme');
|
||
if (pref === 'dark') return true;
|
||
if (pref === 'light') return false;
|
||
|
||
var html = document.documentElement, body = document.body;
|
||
if (
|
||
(html.classList && html.classList.contains('dark')) ||
|
||
(body.classList && body.classList.contains('dark')) ||
|
||
html.getAttribute('data-theme') === 'dark' ||
|
||
body.getAttribute('data-theme') === 'dark' ||
|
||
(html.classList && html.classList.contains('theme-dark')) ||
|
||
(body.classList && body.classList.contains('theme-dark'))
|
||
) return true;
|
||
|
||
return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||
}
|
||
function gTheme(){ return isDark() ? 'dark_dimmed' : 'noborder_light'; }
|
||
|
||
function loadGiscus() {
|
||
if (gLoaded) return;
|
||
var s = document.createElement('script');
|
||
s.src = 'https://giscus.app/client.js';
|
||
s.async = true;
|
||
s.crossOrigin = 'anonymous';
|
||
s.setAttribute('data-repo','AlipourIm/blog-comments');
|
||
s.setAttribute('data-repo-id','R_kgDOQGARyA');
|
||
s.setAttribute('data-category','Comments');
|
||
s.setAttribute('data-category-id','DIC_kwDOQGARyM4Cw3x-');
|
||
s.setAttribute('data-mapping','pathname');
|
||
s.setAttribute('data-strict','0');
|
||
s.setAttribute('data-reactions-enabled','1');
|
||
s.setAttribute('data-emit-metadata','0');
|
||
s.setAttribute('data-input-position','bottom');
|
||
s.setAttribute('data-lang','en');
|
||
s.setAttribute('data-theme', gTheme());
|
||
document.getElementById('giscus-thread').appendChild(s);
|
||
|
||
function reTheme(){
|
||
var iframe = document.querySelector('iframe.giscus-frame');
|
||
if (iframe && iframe.contentWindow) {
|
||
iframe.contentWindow.postMessage(
|
||
{ giscus: { setConfig: { theme: gTheme() } } },
|
||
'https://giscus.app'
|
||
);
|
||
}
|
||
}
|
||
|
||
var tbtn = document.getElementById('theme-toggle');
|
||
if (tbtn) tbtn.addEventListener('click', function(){ setTimeout(reTheme, 0); });
|
||
|
||
var mo = new MutationObserver(function(){ reTheme(); });
|
||
mo.observe(document.documentElement, { attributes:true, attributeFilter:['class','data-theme'] });
|
||
mo.observe(document.body, { attributes:true, attributeFilter:['class','data-theme'] });
|
||
|
||
gLoaded = true;
|
||
}
|
||
|
||
function show(which) {
|
||
var showIsso = which === 'isso';
|
||
panelIsso.style.display = showIsso ? 'block' : 'none';
|
||
panelGisc.style.display = showIsso ? 'none' : 'block';
|
||
if (issoBtn) issoBtn.setAttribute('aria-pressed', showIsso ? 'true' : 'false');
|
||
if (giscBtn) giscBtn.setAttribute('aria-pressed', showIsso ? 'false' : 'true');
|
||
if (!showIsso) loadGiscus();
|
||
}
|
||
|
||
if (issoBtn) issoBtn.addEventListener('click', function(e){ e.preventDefault(); show('isso'); });
|
||
if (giscBtn) giscBtn.addEventListener('click', function(e){ e.preventDefault(); show('giscus'); });
|
||
|
||
|
||
show('isso');
|
||
})();
|
||
</script>
|
||
|
||
|
||
</article>
|
||
</main>
|
||
|
||
<footer class="footer">
|
||
<span>© 2026 <a href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/">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 xmlns="http://www.w3.org/2000/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 xmlns="http://www.w3.org/2000/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 0 1 9 9" />
|
||
<path d="M4 4a16 16 0 0 1 16 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')
|
||
if (menu) {
|
||
menu.scrollLeft = localStorage.getItem("menu-scroll-position");
|
||
menu.onscroll = function () {
|
||
localStorage.setItem("menu-scroll-position", menu.scrollLeft);
|
||
}
|
||
}
|
||
|
||
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
||
anchor.addEventListener("click", function (e) {
|
||
e.preventDefault();
|
||
var id = this.getAttribute("href").substr(1);
|
||
if (!window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
||
document.querySelector(`[id='${decodeURIComponent(id)}']`).scrollIntoView({
|
||
behavior: "smooth"
|
||
});
|
||
} else {
|
||
document.querySelector(`[id='${decodeURIComponent(id)}']`).scrollIntoView();
|
||
}
|
||
if (id === "top") {
|
||
history.replaceState(null, null, " ");
|
||
} else {
|
||
history.pushState(null, null, `#${id}`);
|
||
}
|
||
});
|
||
});
|
||
|
||
</script>
|
||
<script>
|
||
var mybutton = document.getElementById("top-link");
|
||
window.onscroll = function () {
|
||
if (document.body.scrollTop > 800 || document.documentElement.scrollTop > 800) {
|
||
mybutton.style.visibility = "visible";
|
||
mybutton.style.opacity = "1";
|
||
} else {
|
||
mybutton.style.visibility = "hidden";
|
||
mybutton.style.opacity = "0";
|
||
}
|
||
};
|
||
|
||
</script>
|
||
<script>
|
||
document.getElementById("theme-toggle").addEventListener("click", () => {
|
||
if (document.body.className.includes("dark")) {
|
||
document.body.classList.remove('dark');
|
||
localStorage.setItem("pref-theme", 'light');
|
||
} else {
|
||
document.body.classList.add('dark');
|
||
localStorage.setItem("pref-theme", 'dark');
|
||
}
|
||
})
|
||
|
||
</script>
|
||
<script>
|
||
document.querySelectorAll('pre > code').forEach((codeblock) => {
|
||
const container = codeblock.parentNode.parentNode;
|
||
|
||
const copybutton = document.createElement('button');
|
||
copybutton.classList.add('copy-code');
|
||
copybutton.innerHTML = 'copy';
|
||
|
||
function copyingDone() {
|
||
copybutton.innerHTML = 'copied!';
|
||
setTimeout(() => {
|
||
copybutton.innerHTML = 'copy';
|
||
}, 2000);
|
||
}
|
||
|
||
copybutton.addEventListener('click', (cb) => {
|
||
if ('clipboard' in navigator) {
|
||
navigator.clipboard.writeText(codeblock.textContent);
|
||
copyingDone();
|
||
return;
|
||
}
|
||
|
||
const range = document.createRange();
|
||
range.selectNodeContents(codeblock);
|
||
const selection = window.getSelection();
|
||
selection.removeAllRanges();
|
||
selection.addRange(range);
|
||
try {
|
||
document.execCommand('copy');
|
||
copyingDone();
|
||
} catch (e) { };
|
||
selection.removeRange(range);
|
||
});
|
||
|
||
if (container.classList.contains("highlight")) {
|
||
container.appendChild(copybutton);
|
||
} else if (container.parentNode.firstChild == container) {
|
||
|
||
} else if (codeblock.parentNode.parentNode.parentNode.parentNode.parentNode.nodeName == "TABLE") {
|
||
|
||
codeblock.parentNode.parentNode.parentNode.parentNode.parentNode.appendChild(copybutton);
|
||
} else {
|
||
|
||
codeblock.parentNode.appendChild(copybutton);
|
||
}
|
||
});
|
||
</script>
|
||
</body>
|
||
|
||
</html>
|