<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Docker on AlipourIm journeys</title><link>https://blog.alipour.eu/tags/docker/</link><description>Recent content in Docker on AlipourIm journeys</description><generator>Hugo -- 0.146.0</generator><language>en</language><lastBuildDate>Mon, 02 Feb 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://blog.alipour.eu/tags/docker/index.xml" rel="self" type="application/rss+xml"/><item><title>Nextcloud on a Raspberry Pi, Fronted by a Tiny VPS — Nginx Reverse Proxy, Trusted Proxies, and Basic Auth for Jellyfin</title><link>https://blog.alipour.eu/posts/pi_service_touchup/</link><pubDate>Mon, 02 Feb 2026 00:00:00 +0000</pubDate><guid>https://blog.alipour.eu/posts/pi_service_touchup/</guid><description>Cloudflare DNS + Nginx on a VPS + Tailscale to the Pi. Small, boring, and reliable.</description><content:encoded><![CDATA[<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</h2>
<p>The request path looks like:</p>
<pre tabindex="0"><code>Browser
  ↓ HTTPS (public)
Cloudflare DNS (optional proxy on/off)
  ↓
VPS (funbox) — Nginx reverse proxy + Let&#39;s Encrypt
  ↓ HTTP over Tailscale (private 100.x network)
Raspberry Pi (iot-hub) — Docker: Nextcloud / Jellyfin / …
</code></pre><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?</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>
<pre tabindex="0"><code>nextcloud         nextcloud:apache     0.0.0.0:8080-&gt;80/tcp
jellyfin          jellyfin/jellyfin    0.0.0.0:8096-&gt;8096/tcp
</code></pre><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</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</h2>
<h3 id="31-nextcloud-vhost-vps--pi-via-tailscale">3.1 Nextcloud vhost (VPS → Pi via Tailscale)</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)</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</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</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</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</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</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>VPS’s Tailscale IP</strong> as seen from the Pi.)</p>
<h3 id="53-overwritehost--overwriteprotocol--overwritecliurl">5.3 overwritehost / overwriteprotocol / overwrite.cli.url</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)</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)</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?”</h2>
<h3 id="the-honest-answer">The honest answer</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?”</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</h2>
<p>Two common gotchas:</p>
<h3 id="81-one-time-pin-email-didnt-arrive">8.1 One-time PIN email didn’t arrive</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?</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)</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</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;">curl -fSLO https://blog.alipour.eu/sources/posts/pi_service_touchup.md
curl -fSLO https://blog.alipour.eu/sources/posts/pi_service_touchup.md.asc
gpg --verify pi_service_touchup.md.asc pi_service_touchup.md
  </pre>
</div>


]]></content:encoded></item><item><title>Movie Night Over the Internet: Jellyfin + Tailscale on a Raspberry Pi 4</title><link>https://blog.alipour.eu/posts/boredom/</link><pubDate>Sun, 21 Dec 2025 09:30:00 +0100</pubDate><guid>https://blog.alipour.eu/posts/boredom/</guid><description>A cozy, slightly chaotic, very fun setup story: turning a Raspberry Pi 4 into a private movie-night server with Jellyfin, Docker, and Tailscale—starting on microSD and graduating to SSD later.</description><content:encoded><![CDATA[<p>Ok. Let me tell you the why first. So&hellip;. I was turning from one side to the other on my bed, going through YouTube. The thing that I used to silence my pain, to let the time pass, and to forget. But it didn&rsquo;t hit the same anymore. I was bored. Eternal boredom. Then the loneliness came back. It&rsquo;s funny how you can be in the middle of dozens of people, i.e., not alone, but still feel lonely. Anyway. I though to myself &ldquo;let&rsquo;t message one of my two best friends&rdquo;, We eventually went into a google meet, talked and talked, and decided to watch a movie together at the same time. I told him I still have my Raspberry Pi lying around, let me put it to good use. Few hours passed, and I was ready. Here is what happened in those few hours.</p>
<p>I wanted the kind of movie night where everyone presses play at the same moment, laughs at the same joke, and groans at the same plot twist—even when we’re scattered across different apartments and time zones. I also wanted it to be a little nerdy, a little elegant, and the kind of project that makes you feel like you own your gadgets again. Enter: a Raspberry Pi 4 (8GB), Jellyfin, and Tailscale.</p>
<p>This post is the “how it went” and the “how to do it” version. It’s written for the future-me who will forget everything the second it works, and for anyone who wants a private streaming setup without punching holes in their router for the entire internet to poke at.</p>
<h2 id="the-idea">The idea</h2>
<p>Jellyfin is the media server that turns your own movie files into a nice Netflix-like library. Tailscale is the secret tunnel that lets your friends reach your server as if they were sitting on your Wi‑Fi—without you exposing ports to the public internet. Docker is the little shipping container that keeps everything tidy and portable, which matters because I started on a microSD card and planned to move to an SSD later.</p>
<p>The trick to making a Raspberry Pi feel “fast enough” is to avoid video transcoding whenever possible. When Jellyfin can “direct play” a file, the Pi mostly just serves bytes. When it has to transcode, it suddenly feels like you asked a hamster to tow a car. So the theme of this build is compatibility: store media in formats that phones, browsers, and TVs can play directly.</p>
<h2 id="what-you-need">What you need</h2>
<p>My setup uses Ubuntu (64-bit is the long-term-friendly choice), a Raspberry Pi 4, a microSD card to get started, and eventually a USB 3 SSD for real “server vibes.” You’ll also want a folder of movies you’re allowed to stream to your friends. Streaming DRM content from Netflix or rental platforms through Jellyfin isn’t supported, and I’m intentionally keeping this firmly in the “your own files” lane.</p>
<h2 id="step-one-choose-a-home-for-your-stuff">Step one: choose a home for your stuff</h2>
<p>Before installing anything, I picked paths that would survive my future SSD upgrade. The idea is simple: keep all persistent app data under one directory, keep all media under another directory, and let Docker mount those paths into containers.</p>
<p>On the Pi, I created a little directory neighborhood under <code>/srv</code>:</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 mkdir -p /srv/jellyfin/<span style="color:#f92672">{</span>config,cache<span style="color:#f92672">}</span> /srv/media /srv/compose/jellyfin
</span></span><span style="display:flex;"><span>sudo chown -R $USER:$USER /srv
</span></span></code></pre></div><p>If you’re not ready to move movies into <code>/srv/media</code> yet, that’s okay. The important part is that you pick a place you can later mount your SSD onto without rewriting your whole setup.</p>
<h2 id="step-two-install-docker-and-start-thinking-in-containers">Step two: install Docker and start thinking in containers</h2>
<p>I installed Docker and verified that <code>docker compose</code> worked. After that, everything became a file called <code>docker-compose.yml</code> and the comforting feeling that I can rebuild my server from a single folder if life gets weird.</p>
<p>A typical install flow on Raspberry Pi OS looks like this:</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 update
</span></span><span style="display:flex;"><span>sudo apt install -y ca-certificates curl
</span></span><span style="display:flex;"><span>curl -fsSL https://get.docker.com | sh
</span></span><span style="display:flex;"><span>sudo usermod -aG docker $USER
</span></span><span style="display:flex;"><span>newgrp docker
</span></span><span style="display:flex;"><span>docker --version
</span></span><span style="display:flex;"><span>docker compose version
</span></span></code></pre></div><p>Now for the part that feels like summoning a friendly daemon.</p>
<h2 id="step-three-jellyfin-running-happily-in-docker">Step three: Jellyfin, running happily in Docker</h2>
<p>In <code>/srv/compose/jellyfin/docker-compose.yml</code>, I defined a Jellyfin service and mounted my config, cache, and media directories into it. The key mental shift is that Jellyfin can only see what you mount into the container. If you try to browse to <code>/home/hub/Documents/movies</code> from inside the Jellyfin UI and it looks like the folder doesn’t exist, it’s not being dramatic—it genuinely can’t see it. Containers are like that.</p>
<p>Here’s a simple <code>docker-compose.yml</code> that works well on a Pi:</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-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">services</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">jellyfin</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">image</span>: <span style="color:#ae81ff">jellyfin/jellyfin:latest</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">container_name</span>: <span style="color:#ae81ff">jellyfin</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">user</span>: <span style="color:#e6db74">&#34;1000:1000&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ports</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#e6db74">&#34;8096:8096/tcp&#34;</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#e6db74">&#34;7359:7359/udp&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">volumes</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">/srv/jellyfin/config:/config</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">/srv/jellyfin/cache:/cache</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">/srv/media:/media</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">restart</span>: <span style="color:#ae81ff">unless-stopped</span>
</span></span></code></pre></div><p>If your user isn’t UID 1000, check it with <code>id -u</code> and <code>id -g</code>, then update the <code>user:</code> line accordingly.</p>
<p>I started Jellyfin like this:</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>cd /srv/compose/jellyfin
</span></span><span style="display:flex;"><span>docker compose up -d
</span></span></code></pre></div><p>After that, Jellyfin lived at <code>http://&lt;pi-lan-ip&gt;:8096</code> on my home network, which is the most satisfying “it’s alive” moment of the whole build.</p>
<h2 id="step-four-tailscale-the-magic-door-for-friends">Step four: Tailscale, the magic door for friends</h2>
<p>Now comes the part that makes everything feel effortless. Instead of forwarding ports, configuring certificates, and hoping I didn’t accidentally publish my server to the open ocean, I installed Tailscale on the Pi host.</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 -fsSL https://tailscale.com/install.sh | sh
</span></span><span style="display:flex;"><span>sudo tailscale up
</span></span><span style="display:flex;"><span>tailscale ip -4
</span></span></code></pre></div><p>That last command gives you a <code>100.x.y.z</code> address. It’s your Pi’s Tailscale IP, and it’s the address your friends will use to reach Jellyfin. From anywhere, the server becomes:</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-text" data-lang="text"><span style="display:flex;"><span>http://100.x.y.z:8096
</span></span></code></pre></div><p>Or if you enabled magicDNS:</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-text" data-lang="text"><span style="display:flex;"><span>http://rpi:8096
</span></span></code></pre></div><p>The moment you can open that URL from your phone on mobile data and see your own server is the moment you start plotting your next homelab project.</p>
<p>If you want friends to access only this server and not everything else in your tailnet, Tailscale has a device sharing flow that’s perfect for “here’s the Pi, please don’t look in the rest of my digital house.”</p>
<h2 id="step-five-create-a-jellyfin-user-for-your-friend">Step five: create a Jellyfin user for your friend</h2>
<p>Jellyfin accounts are local to your server. Your friend won’t “sign up” on some global Jellyfin site; instead, you create a user for them inside your Jellyfin admin dashboard. The nice part is that you can give them access only to the libraries you want, and you can keep admin permissions to yourself.</p>
<p>If typing passwords on a TV app makes you feel old instantly, Jellyfin’s Quick Connect feature can make logins much smoother. Your friend gets a short code on their device, you approve it from your logged-in browser, and nobody has to poke letters into a remote control one by one.</p>
<h2 id="step-six-the-secret-to-a-smooth-pi-movie-night-is-direct-play">Step six: the secret to a smooth Pi movie night is Direct Play</h2>
<p>A Raspberry Pi 4 is perfectly happy serving media files. It becomes less happy when it has to convert them on the fly. The goal is to store your library in a way that most clients can play without conversion.</p>
<p>For “plays on basically everything,” I like MP4 with H.264 video and AAC audio. When I inspected one of my files, I found H.264 video already, but the audio was AC3. AC3 is fine on some devices, but it’s not universal in browsers, and “my friend has no audio” is a movie-night mood killer.</p>
<p>The fix was to keep the video as-is and convert only the audio to AAC, which is much lighter than re-encoding the whole video. This command copies the H.264 video stream, converts audio to AAC stereo, and produces an MP4 that’s friendlier to phones and browsers:</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>ffmpeg -i <span style="color:#e6db74">&#34;500.Days.of.Summer.2009.1080p.BluRay.RARBG.FilmKio.mkv&#34;</span>   -map 0:v:0 -map 0:a:0   -c:v copy   -c:a aac -b:a 384k -ac <span style="color:#ae81ff">2</span>   -movflags +faststart   <span style="color:#e6db74">&#34;500.Days.of.Summer.2009.1080p.mp4&#34;</span>
</span></span></code></pre></div><p>Subtitles deserve their own tiny cautionary tale. Some subtitle formats trigger transcoding because the client can’t render them natively and asks the server to burn them into the video. Text-based SRT subtitles are usually the easiest path. If your MKV contains an SRT track, you can extract it and keep it next to your MP4:</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>ffmpeg -i <span style="color:#e6db74">&#34;500.Days.of.Summer.2009.1080p.BluRay.RARBG.FilmKio.mkv&#34;</span>   -map 0:s:0   <span style="color:#e6db74">&#34;500.Days.of.Summer.2009.1080p.srt&#34;</span>
</span></span></code></pre></div><p>Once your friend is watching, you can confirm how Jellyfin is handling playback by checking the active session in the admin dashboard. Seeing “Direct Play” feels like winning.</p>
<h2 id="step-seven-syncplay-aka-we-pressed-play-together">Step seven: SyncPlay, aka “we pressed play together”</h2>
<p>Jellyfin has SyncPlay for “group watch” nights, and it’s wonderfully simple when everyone is using compatible clients. In practice, the web client is an excellent baseline because it’s consistent and easy for friends. When we do movie night, we start a SyncPlay group, join it from each device, and keep a voice call open for commentary and laughter. It’s not complicated; it’s just… cozy.</p>
<h2 id="starting-on-microsd-graduating-to-ssd-later">Starting on microSD, graduating to SSD later</h2>
<p>Yes, you can run all of this on microSD to begin with. It works. It also feels like using a napkin as a hard drive if you add heavy-write services later. My plan is to move the system to an SSD when convenient, because SSDs are faster and they handle constant writes far better than microSD cards.</p>
<p>The reason the <code>/srv</code> layout is so helpful is that migration becomes a simple copy-and-mount story. When the SSD arrives, you can copy <code>/srv</code> to the SSD, mount the SSD at <code>/srv</code>, reboot, and your containers will come back exactly as before. Docker will happily reuse the same volume paths; Jellyfin will happily reuse the same library; and you’ll feel like a wizard who planned ahead.</p>
<h2 id="where-this-goes-next">Where this goes next</h2>
<p>Once Jellyfin and Tailscale are stable, it’s easy to imagine the Pi doing more. Pi-hole is light and useful. A smart home hub is fun. Nextcloud is possible, especially with SSD storage and some attention to databases and caching. The practical advice I’m giving future-me is to add services one at a time and keep <code>/srv</code> as the center of gravity, because nothing ruins movie night like “I tweaked DNS and now nothing works.”</p>
<p>For now, though, this little Pi does the job: it hosts a library, it welcomes friends through a private tunnel, and it turns “let’s watch something” into a real shared moment—even when we’re not in the same room.</p>
<p>Happy streaming.</p>
<p>The movie night went great, I even posted a BeReal on it. The choice of movie was not random either. I&rsquo;ll write a review for it later on. It was just amazing.</p>
<hr>
<div class="signature-block" style="margin-top:1rem">
  <p><strong>Downloads:</strong>
    <a href="/sources/posts/boredom.md">Markdown</a> ·
    <a href="/sources/posts/boredom.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/tYgoUOBMjSoFAmpkbugACgkQtYgoUOBM
jSrqzw/8Crod3Gm5xGMMrOtsoVm9NFwTAD7xdc8H5LcFC8P5OZmyEYBxF/SEHk6m
TDhSyj6bNdShWIrzkKL0XHm6ntjhkBX4&#43;dFzPbKjpHZ84on/xfNwIOh8br&#43;WJEBF
V3zCialBjjxxE37PVLkqsNVVtMgT2Wv5GnR7SWLKkovJWpIn/FP0gSsQFUIvRvdC
Xhy3nvODqXZqfg77kKllmbkPI5ePkxl95CK9fd4yzhI13G41vveVO4dt2JhWVR6H
dfRv6XG53WGP0nVWyEdHJjJhiT1JTd7//AD3DsRCTmBNyaxweRlPsvqqICquGx1U
LGQ62y0oZL5WDqjiD7T2ZJAJ6sqr6NngFGjh7RaKOWDT1&#43;8fTdXfQg/t91lTHVfh
efVqOiH&#43;B6SlzqPM4LgzRjf&#43;36XdSu9R1w75sGykQpGRBfq2IymoM6RPQLEwgm3J
haMjYRqx5jZSLj9FpjOZFYEuqYCa0ut0lUgY9BDHf0u8kKrW6OQZFVdhN31g&#43;Lvf
5qjzC&#43;ZzR4BLMpA4E33NKmYT4Rt1i5mSPiGY85yqhJdjFJRtPfXXf05&#43;m7VGjRPp
sWQmqO1o7cChFqVnF5IalDFCNIsGavBf8F0/7tu3y4bLIqoBMBfgIgg9KMORVHIn
kqUsV4LpNgVWdTzp0QURkHUOL5uP72Jqa3ppVYEXlJQbhuLpCDY=
=/K6E
-----END PGP SIGNATURE-----
</pre>
  </details><p><em>Verify locally:</em></p>
  <pre style="font-size:0.85em; overflow-x:auto; padding:0.75rem;">curl -fSLO https://blog.alipour.eu/sources/posts/boredom.md
curl -fSLO https://blog.alipour.eu/sources/posts/boredom.md.asc
gpg --verify boredom.md.asc boredom.md
  </pre>
</div>


]]></content:encoded></item><item><title>Dockerizing my Minecraft Server + Geyser: from 'no space left' to stable releases</title><link>https://blog.alipour.eu/posts/minecraft_server/</link><pubDate>Mon, 29 Sep 2025 09:06:29 +0000</pubDate><guid>https://blog.alipour.eu/posts/minecraft_server/</guid><description>How I containerized a Java Minecraft server with Geyser, hit a /var space wall, and locked the server to the latest stable release.</description><content:encoded><![CDATA[<h2 id="why">Why</h2>
<p>I’ve been running a Java Minecraft world (with Bedrock players via <strong>Geyser</strong>) in <code>tmux</code>. I moved it to Docker for easier updates, backups, and restarts. Along the way I hit:</p>
<ul>
<li><code>failed to register layer: ... no space left on device</code></li>
<li>Client saying: <strong>“Outdated client, please use 1.21.9 Pre-Release 2”</strong></li>
<li>Wanting config in a neat <code>.env</code> and <strong>no whitelist</strong></li>
</ul>
<p>Here’s exactly what I did.</p>
<hr>
<h2 id="folder-layout">Folder layout</h2>
<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>~/minecraft/
</span></span><span style="display:flex;"><span>├─ docker-compose.yml
</span></span><span style="display:flex;"><span>├─ .env
</span></span><span style="display:flex;"><span>└─ plugins/
</span></span></code></pre></div><hr>
<h2 id="env-secrets--knobs"><code>.env</code> (secrets &amp; knobs)</h2>
<p>Use the <strong>latest stable</strong> (not snapshots/pre-releases) by setting <code>VERSION=LATEST</code>.</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-env" data-lang="env"><span style="display:flex;"><span><span style="color:#75715e"># Minecraft server configuration</span>
</span></span><span style="display:flex;"><span>EULA<span style="color:#f92672">=</span>TRUE
</span></span><span style="display:flex;"><span>TYPE<span style="color:#f92672">=</span>PAPER
</span></span><span style="display:flex;"><span>VERSION<span style="color:#f92672">=</span>LATEST
</span></span><span style="display:flex;"><span>MEMORY<span style="color:#f92672">=</span>4G
</span></span><span style="display:flex;"><span>USE_AIKAR_FLAGS<span style="color:#f92672">=</span>true
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># RCON (remote console)</span>
</span></span><span style="display:flex;"><span>ENABLE_RCON<span style="color:#f92672">=</span>true
</span></span><span style="display:flex;"><span>RCON_PASSWORD<span style="color:#f92672">=</span>superSecretPassword123
</span></span></code></pre></div><blockquote>
<p>I don’t use a whitelist, so no <code>WHITELIST</code>/<code>ENFORCE_WHITELIST</code> variables.</p></blockquote>
<hr>
<h2 id="docker-composeyml"><code>docker-compose.yml</code></h2>
<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-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">services</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">mc</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">image</span>: <span style="color:#ae81ff">itzg/minecraft-server:latest</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">container_name</span>: <span style="color:#ae81ff">mc</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">restart</span>: <span style="color:#ae81ff">unless-stopped</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ports</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#e6db74">&#34;25565:25565&#34;</span>        <span style="color:#75715e"># Java</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#e6db74">&#34;19132:19132/udp&#34;</span>    <span style="color:#75715e"># Bedrock via Geyser plugin</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">env_file</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">.env</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">volumes</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">mc-data:/data</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">./plugins:/plugins:ro</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">volumes</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">mc-data</span>: {}
</span></span></code></pre></div><blockquote>
<p>The <code>version:</code> key in Compose is obsolete now, so I dropped it.</p></blockquote>
<hr>
<h2 id="add-geyser-and-optional-floodgate-as-plugins">Add Geyser (and optional Floodgate) as plugins</h2>
<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>mkdir -p ~/minecraft/plugins
</span></span><span style="display:flex;"><span>cd ~/minecraft/plugins
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Geyser (Spigot/Paper)</span>
</span></span><span style="display:flex;"><span>wget https://download.geysermc.org/v2/projects/geyser/versions/latest/builds/latest/downloads/spigot   -O Geyser-Spigot.jar
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Floodgate (optional: Bedrock accounts without Java linking)</span>
</span></span><span style="display:flex;"><span>wget https://download.geysermc.org/v2/projects/floodgate/versions/latest/builds/latest/downloads/spigot   -O Floodgate-Spigot.jar
</span></span></code></pre></div><p>Start it up:</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 compose up -d
</span></span></code></pre></div><p>On first run, Geyser writes its config to <code>/data/plugins/Geyser-Spigot/</code>. Open UDP <strong>19132</strong> on your firewall.</p>
<hr>
<h2 id="hit-a-wall-var-ran-out-of-space">Hit a wall: <code>/var</code> ran out of space</h2>
<p>Docker stores layers at <code>/var/lib/docker</code> by default. My <code>/var</code> LV was tiny:</p>
<pre tabindex="0"><code>/dev/mapper/ubuntu--vg-var   3.9G  3.4G  333M  92% /var
</code></pre><h3 id="quick-cleanup">Quick cleanup</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 system df
</span></span><span style="display:flex;"><span>docker system prune -f
</span></span><span style="display:flex;"><span>docker builder prune -af
</span></span><span style="display:flex;"><span>sudo apt-get clean
</span></span><span style="display:flex;"><span>sudo journalctl --vacuum-size<span style="color:#f92672">=</span>100M
</span></span></code></pre></div><p>If that’s not enough, you have two good options:</p>
<h3 id="option-a--move-dockers-data-off-var">Option A — Move Docker’s data off <code>/var</code></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>sudo systemctl stop docker
</span></span><span style="display:flex;"><span>sudo mkdir -p /home/docker
</span></span><span style="display:flex;"><span>sudo rsync -aHAX --info<span style="color:#f92672">=</span>progress2 /var/lib/docker/ /home/docker/
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#39;{ &#34;data-root&#34;: &#34;/home/docker&#34; }&#39;</span> | sudo tee /etc/docker/daemon.json
</span></span><span style="display:flex;"><span>sudo systemctl start docker
</span></span><span style="display:flex;"><span>docker info | grep <span style="color:#e6db74">&#34;Docker Root Dir&#34;</span>
</span></span></code></pre></div><p>If all looks good, free the old space:</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 rm -rf /var/lib/docker/*
</span></span></code></pre></div><h3 id="option-b--grow-var-lvm">Option B — Grow <code>/var</code> (LVM)</h3>
<p>Check free space in the VG:</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 vgdisplay   <span style="color:#75715e"># look for &#34;Free PE / Size&#34;</span>
</span></span></code></pre></div><p>If available, extend <code>/var</code> by +5G:</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 lvextend -L +5G /dev/mapper/ubuntu--vg-var
</span></span><span style="display:flex;"><span>sudo resize2fs /dev/mapper/ubuntu--vg-var
</span></span></code></pre></div><hr>
<h2 id="the-version-mismatch-pre-release-vs-stable">The version mismatch: pre-release vs stable</h2>
<p>My logs showed:</p>
<pre tabindex="0"><code>Starting minecraft server version 1.21.9 Pre-Release 2
</code></pre><p>That happens if the server pulls a pre-release build (or if <code>VERSION=LATEST</code>). Fix:</p>
<ol>
<li>Ensure <code>.env</code> has:
<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-env" data-lang="env"><span style="display:flex;"><span>VERSION<span style="color:#f92672">=</span>LATEST_RELEASE
</span></span></code></pre></div></li>
<li>Recreate:
<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 compose down
</span></span><span style="display:flex;"><span>docker compose pull
</span></span><span style="display:flex;"><span>docker compose up -d
</span></span></code></pre></div></li>
<li>Verify:
<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 logs mc | grep <span style="color:#e6db74">&#34;Starting minecraft server version&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># -&gt; Starting minecraft server version 1.21.1   (example)</span>
</span></span></code></pre></div></li>
</ol>
<blockquote>
<p>Tip: If you <strong>want to pin</strong> and avoid auto-updates entirely, set <code>VERSION=1.21.1</code> (or whatever stable you’ve validated).</p></blockquote>
<hr>
<h2 id="no-whitelist">No whitelist</h2>
<p>Because I don’t set <code>WHITELIST</code>/<code>ENFORCE_WHITELIST</code>, anyone can join (subject to online-mode, bans, and Geyser/Floodgate auth settings). Manage ops/permissions via:</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 -it mc rcon-cli <span style="color:#e6db74">&#34;op YourJavaIGN&#34;</span>
</span></span></code></pre></div><p>(or edit <code>/data/ops.json</code>).</p>
<hr>
<h2 id="backup--updates">Backup &amp; updates</h2>
<ul>
<li>World/data live under the <code>mc-data</code> volume → back up <code>/data</code> regularly.</li>
<li>Update cleanly:
<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 compose pull <span style="color:#f92672">&amp;&amp;</span> docker compose up -d
</span></span></code></pre></div></li>
<li>Watch space:
<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>watch -n5 <span style="color:#e6db74">&#39;df -h /var; docker system df&#39;</span>
</span></span></code></pre></div></li>
</ul>
<hr>
<h2 id="tldr">TL;DR</h2>
<ul>
<li>Put <strong>Geyser/Floodgate</strong> jars in <code>./plugins</code> and expose <strong>19132/udp</strong>.</li>
<li>Keep configs in <code>.env</code>.</li>
<li>Fix <code>/var</code> space by pruning, <strong>moving Docker’s data-root</strong>, or <strong>extending <code>/var</code></strong> via LVM.</li>
<li>Verify version in logs after each change.</li>
</ul>
<p>Happy block-breaking! 🧱🚀</p>
<hr>
<div class="signature-block" style="margin-top:1rem">
  <p><strong>Downloads:</strong>
    <a href="/sources/posts/minecraft_server.md">Markdown</a> ·
    <a href="/sources/posts/minecraft_server.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/tYgoUOBMjSoFAmpkbuAACgkQtYgoUOBM
jSrrmBAAuVoSvB3tRIzD&#43;N0F5OwfYvG3V3z6ok58df7p4js91/a9lcki2ywxw/Dy
Q3aeieiGITkd/zMNjTydy4XjkScoRFFlL5GVZ2WNjO8CvjpEv3nK7EX6jRt5leMV
0D0DESMnOQzgo4a1CuNHrYPHKPaMVpJ/1aKOUZwyPC9j8/70irAJpoA2jdBgSsxw
7p/hYijX0vGJ8&#43;WSFj6707dh6hNRk5ZaNc0cElpkE4kXA31H0h/fRwPPteT4wjGA
JWQAyEUu3Vx/U0BnN6R9Lne&#43;5cqxO0Kh8VrcBpyH2VpqH3fDk1fIHk1RdhDxwKD7
OzvAT5XXRS5Q6Udc7Nsa9T/OYG&#43;elcgMAMFXosItqTRJNG72OyWloLVc/OrJ5Qsc
s81FbfcHp1dgjJ2gtO2Eyb/wb1WkyvrQegNnjuJzMt3awBN1BWex5Nn4Bz&#43;UbLDz
g6dGQxcpfDJ6F9Tjz/ru7RZ9Yic/bo8vVvKaa6FhSAwF4SluKWS3cf3meE4GQD5r
NrClzg0Vujk/3zP/6yhCL7I0SwQ&#43;NeW2HoUHeoawApBmFt/q5du4ftIx2iMMeWoH
F91H35CchRtKArxjpwFNt/J1QAPvKx9UvzA2uAl&#43;LNOWXf/gomXH6Tqm9vnWr/aX
sczdH6N2E0&#43;7KDO7NwjBJWa/QwdXKUlOq5fFgAop22v3vWOml1M=
=NmxL
-----END PGP SIGNATURE-----
</pre>
  </details><p><em>Verify locally:</em></p>
  <pre style="font-size:0.85em; overflow-x:auto; padding:0.75rem;">curl -fSLO https://blog.alipour.eu/sources/posts/minecraft_server.md
curl -fSLO https://blog.alipour.eu/sources/posts/minecraft_server.md.asc
gpg --verify minecraft_server.md.asc minecraft_server.md
  </pre>
</div>


]]></content:encoded></item><item><title>Self-Hosting HedgeDoc with Docker + Nginx + Let's Encrypt</title><link>https://blog.alipour.eu/posts/hedge_doc/</link><pubDate>Fri, 29 Aug 2025 00:00:00 +0000</pubDate><guid>https://blog.alipour.eu/posts/hedge_doc/</guid><description>&lt;p>HedgeDoc is an open-source collaborative Markdown editor. Think &lt;em>Google Docs for Markdown&lt;/em>: multiple people can edit the same note in real-time, with support for diagrams, math, polls, and slide decks. In this post we’ll walk through setting up your own instance on a server, secured with HTTPS.&lt;/p>
&lt;hr>
&lt;h2 id="prerequisites">Prerequisites&lt;/h2>
&lt;ul>
&lt;li>A Linux server with &lt;strong>Docker&lt;/strong> and &lt;strong>Docker Compose&lt;/strong>&lt;/li>
&lt;li>A domain name pointing to your server (e.g. &lt;code>notes.alipourimjourneys.ir&lt;/code>)&lt;/li>
&lt;li>&lt;strong>Nginx&lt;/strong> installed for reverse proxying&lt;/li>
&lt;li>&lt;strong>Certbot&lt;/strong> for Let’s Encrypt certificates&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="1-create-the-project-directory">1. Create the project directory&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>mkdir ~/hedgedoc &lt;span style="color:#f92672">&amp;amp;&amp;amp;&lt;/span> cd ~/hedgedoc&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;hr>
&lt;h2 id="2-create-env">2. Create &lt;code>.env&lt;/code>&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-env" data-lang="env">&lt;span style="display:flex;">&lt;span>POSTGRES_PASSWORD&lt;span style="color:#f92672">=&lt;/span>ChangeThisStrongPassword
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>HD_DOMAIN&lt;span style="color:#f92672">=&lt;/span>notes.alipourimjourneys.ir&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;p>Generate a strong password with:&lt;/p></description><content:encoded><![CDATA[<p>HedgeDoc is an open-source collaborative Markdown editor. Think <em>Google Docs for Markdown</em>: multiple people can edit the same note in real-time, with support for diagrams, math, polls, and slide decks. In this post we’ll walk through setting up your own instance on a server, secured with HTTPS.</p>
<hr>
<h2 id="prerequisites">Prerequisites</h2>
<ul>
<li>A Linux server with <strong>Docker</strong> and <strong>Docker Compose</strong></li>
<li>A domain name pointing to your server (e.g. <code>notes.alipourimjourneys.ir</code>)</li>
<li><strong>Nginx</strong> installed for reverse proxying</li>
<li><strong>Certbot</strong> for Let’s Encrypt certificates</li>
</ul>
<hr>
<h2 id="1-create-the-project-directory">1. Create the project directory</h2>
<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>mkdir ~/hedgedoc <span style="color:#f92672">&amp;&amp;</span> cd ~/hedgedoc</span></span></code></pre></div>
<hr>
<h2 id="2-create-env">2. Create <code>.env</code></h2>
<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-env" data-lang="env"><span style="display:flex;"><span>POSTGRES_PASSWORD<span style="color:#f92672">=</span>ChangeThisStrongPassword
</span></span><span style="display:flex;"><span>HD_DOMAIN<span style="color:#f92672">=</span>notes.alipourimjourneys.ir</span></span></code></pre></div>
<p>Generate a strong password with:</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>openssl rand -base64 <span style="color:#ae81ff">32</span></span></span></code></pre></div>
<hr>
<h2 id="3-create-docker-composeyml">3. Create <code>docker-compose.yml</code></h2>
<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-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">version</span>: <span style="color:#e6db74">&#34;3.9&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">services</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">db</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">image</span>: <span style="color:#ae81ff">postgres:16</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">environment</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">POSTGRES_USER</span>: <span style="color:#ae81ff">hedgedoc</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">POSTGRES_PASSWORD</span>: <span style="color:#ae81ff">${POSTGRES_PASSWORD}</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">POSTGRES_DB</span>: <span style="color:#ae81ff">hedgedoc</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">volumes</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">db:/var/lib/postgresql/data</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">restart</span>: <span style="color:#ae81ff">unless-stopped</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">hedgedoc</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">image</span>: <span style="color:#ae81ff">quay.io/hedgedoc/hedgedoc:1.10.2</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">depends_on</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">db</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">environment</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">CMD_DB_URL</span>: <span style="color:#ae81ff">postgres://hedgedoc:${POSTGRES_PASSWORD}@db:5432/hedgedoc</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">CMD_DOMAIN</span>: <span style="color:#ae81ff">${HD_DOMAIN}</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">CMD_PROTOCOL_USESSL</span>: <span style="color:#e6db74">&#34;true&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">CMD_URL_ADDPORT</span>: <span style="color:#e6db74">&#34;false&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">CMD_PORT</span>: <span style="color:#e6db74">&#34;3000&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">CMD_EMAIL</span>: <span style="color:#e6db74">&#34;true&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">CMD_ALLOW_EMAIL_REGISTER</span>: <span style="color:#e6db74">&#34;false&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">volumes</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">uploads:/hedgedoc/public/uploads</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ports</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#e6db74">&#34;127.0.0.1:3000:3000&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">restart</span>: <span style="color:#ae81ff">unless-stopped</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">volumes</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">db</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#ae81ff">uploads:</span></span></span></code></pre></div>
<p>Bring it up:</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 compose up -d</span></span></code></pre></div>
<hr>
<h2 id="4-get-a-lets-encrypt-certificate">4. Get a Let’s Encrypt certificate</h2>
<p>Request a cert with a <strong>DNS challenge</strong>:</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 certbot certonly   --manual   --preferred-challenges dns   -d notes.alipourimjourneys.ir   -m you@example.com   --agree-tos --no-eff-email</span></span></code></pre></div>
<p>Add the TXT record certbot asks for, wait for DNS to propagate, then continue.<br>
Certificates will be in:</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>/etc/letsencrypt/live/notes.alipourimjourneys.ir/</span></span></code></pre></div>
<hr>
<h2 id="5-configure-nginx">5. Configure Nginx</h2>
<p>Create <code>/etc/nginx/sites-available/notes.alipourimjourneys.ir</code>:</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">server_name</span> <span style="color:#e6db74">notes.alipourimjourneys.ir</span>;
</span></span><span style="display:flex;"><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">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">server_name</span> <span style="color:#e6db74">notes.alipourimjourneys.ir</span>;
</span></span><span style="display:flex;"><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 style="color:#e6db74">http2</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 style="color:#e6db74">http2</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/notes.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/notes.alipourimjourneys.ir/privkey.pem</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://127.0.0.1:3000</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> <span style="color:#e6db74">https</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 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></span></code></pre></div>
<p>Enable the config and reload Nginx:</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/notes.alipourimjourneys.ir /etc/nginx/sites-enabled/
</span></span><span style="display:flex;"><span>sudo nginx -t <span style="color:#f92672">&amp;&amp;</span> sudo systemctl reload nginx</span></span></code></pre></div>
<hr>
<h2 id="6-create-users">6. Create users</h2>
<p>Because we disabled self-registration, create accounts manually:</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 compose exec hedgedoc ./bin/manage_users --add alice@example.com</span></span></code></pre></div>
<p>You’ll be prompted for a password.<br>
To reset a password later:</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 compose exec hedgedoc ./bin/manage_users --reset alice@example.com</span></span></code></pre></div>
<hr>
<h2 id="7-done">7. Done!</h2>
<p>Visit <a href="https://notes.alipourimjourneys.ir">https://notes.alipourimjourneys.ir</a> and log in with the user you created. You now have your own collaborative Markdown editor 🎉</p>
<hr>
<p><strong>Extras:</strong></p>
<ul>
<li>Backups: dump the PostgreSQL database and save the <code>uploads</code> volume.</li>
<li>Upgrades: <code>docker pull quay.io/hedgedoc/hedgedoc:latest &amp;&amp; docker compose up -d</code>.</li>
<li>Integrations: HedgeDoc supports S3/MinIO image storage, GitHub/GitLab/Google login, and more.</li>
</ul>
<hr>
<div class="signature-block" style="margin-top:1rem">
  <p><strong>Downloads:</strong>
    <a href="/sources/posts/hedge_doc.md">Markdown</a> ·
    <a href="/sources/posts/hedge_doc.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/tYgoUOBMjSoFAmpkbucACgkQtYgoUOBM
jSrPHA//WlMEZx1k/aGx3zau&#43;wRrAOq4XlrvC7keBvqMs0PJGhJmT8jnOMh0&#43;26w
AGav2jBuChLYsDx&#43;O8l18uxcsu9fdrz8r4N4sDOnsndSsg15rTT28P3MNvvUL8ns
iOc9uEUkxF59rT3OXRI56hH3VX6vzxakdAnW3Afhki2Bl54jur2&#43;6RVtuthGUEV&#43;
QZ/36QVXLrOAas1bqq3f38m4M2no3ZqVvpj&#43;Alur2Ji/gcGZlSArBnIoJQoK5L&#43;r
UZLJsQ&#43;LrqCJp4i&#43;GkkX05si2srSTjawBu&#43;ssHf&#43;uwPg0Q6AMTbubrGiHrMMtMbM
4PCFtS8vD/ZBVkVpSBybyXdMxQU&#43;y9AI1LZ//X4cQWdqgRpinOVENuZ2FeVI6qa/
sBGHLThq9nZEXmMT2oQa1wi&#43;CaY17z9fYj20F5moOp2Q6pxBGPyHZRV3WAr5xURU
5B9SwVtNqIzm7eoAbwckU3h&#43;TfIJ4vGulSqPqHvZ/XAdbzCVXaSXBN951oA0No1y
MyvsIskzKVPvSqndYjxLGiopvOpITgxN5q6a7ubBmxYCrS&#43;SF74jPkDjtc4EFuKB
QrMTBm/&#43;Xl/VEESYv5Mx7hwYv1dnmJUFrx62FUYmAMaFP2UcMIlJJ5zQCwsDdREk
aG3wFuWH4d9UFohK&#43;MJIHqYk1&#43;TbZJm3bnds8pOqniIZ7M5PcEg=
=uf5y
-----END PGP SIGNATURE-----
</pre>
  </details><p><em>Verify locally:</em></p>
  <pre style="font-size:0.85em; overflow-x:auto; padding:0.75rem;">curl -fSLO https://blog.alipour.eu/sources/posts/hedge_doc.md
curl -fSLO https://blog.alipour.eu/sources/posts/hedge_doc.md.asc
gpg --verify hedge_doc.md.asc hedge_doc.md
  </pre>
</div>


]]></content:encoded></item></channel></rss>