Initial import of Hugo site to Forgejo

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

View file

@ -0,0 +1,874 @@
<!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 &#43; Nginx on a VPS &#43; 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>&nbsp;»&nbsp;<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 &#43; Nginx on a VPS &#43; 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>&nbsp;·&nbsp;6 min&nbsp;·&nbsp;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 didnt “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>Lets 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 Jellyfins own login)</li>
</ul>
<p>Im 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 &#43; Let&#39;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) Whats 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-&gt;80/tcp
jellyfin jellyfin/jellyfin 0.0.0.0:8096-&gt;8096/tcp</div>
<p>So on the Pi, the services listen on:</p>
<ul>
<li><code>http://&lt;pi&gt;:8080</code> for Nextcloud</li>
<li><code>http://&lt;pi&gt;: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 dont 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">&#34;Jellyfin</span> <span style="color:#e6db74">(private)&#34;</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">&#34;upgrade&#34;</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 cant 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">&#34;nextcloud.alipourimjourneys.ir&#34;</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">&#34;100.99.79.75&#34;</span>
</span></span></code></pre></div><p>(Use the <strong>VPSs 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">&#34;nextcloud.alipourimjourneys.ir&#34;</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">&#34;https&#34;</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">&#34;https://nextcloud.alipourimjourneys.ir&#34;</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">&#34;HTTP_X_FORWARDED_FOR&#34;</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 isnt directly exposed (only the VPS is public)</li>
<li>you keep things patched</li>
</ul>
<p>…then youre 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 thats “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 didnt 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 its flaky, Id 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/&#43;LsB&#43;V0BnPki5qaDc&#43;tRu6EXM5kPuH7G8x5ar4opsKgbfsRKEwT6/Q0Er
vfg48uYNp4fBnoyfJrgURx&#43;OwS7EVJRCmXaRsdilEEGHF7cT3qHhlczYaC&#43;58lGs
&#43;qXaHjYE8x0byAZ8iHNxNUhIyILVrcsdua3JZ3D3J/8r93dfVgrWg41Nrtj4tPUE
QQMW/KxTe/TOED4iivXxFlDCiT13KmgB/B9HeunGPqu8lPMTORf4ePoPzeYEeuuZ
iVH&#43;ssNZ9XB00Z4a/c3I0d2ALLYoA/dXmrJkl0qCCpCy&#43;7/id2yz3eXYWn2xKMvp
SAMTYaFAV6Fd7xdmxGYEeoCSDu8P57P7QfdPVEU1oUTANO21tEh1vGnjxcFdJUBx
kOvBdjQf4wDqUuNv7NKA&#43;OHOzhJ3Wf3VLb/ZNq6Y2okEibsCygm3XDn0008WeKPv
ncB0gceY6nFYzbyhuUIhPtQJJWDNi5KG/KMEvYcebEwDzn7TErg/v3Bp8ZCdWRx/
Gs8K9nADnHhAjWgwTq3D&#43;2qRWcF0tlLSTmKg&#43;95yaYi0XWWMFGTgCv2odPsgFlIS
3FiLJC3rV73prsk&#43;7eZftBTYCJN0Xk92YFj4a6bTYeuLcC20VbAA98Bpi0pCyO0v
TJ2&#43;amlKyT&#43;Nq9wGrAez&#43;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>&copy; 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>