mysite/public/posts/index.xml

2589 lines
No EOL
258 KiB
XML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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>Posts on AlipourIm journeys</title><link>http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/posts/</link><description>Recent content in Posts on AlipourIm journeys</description><generator>Hugo -- 0.146.0</generator><language>en</language><lastBuildDate>Mon, 20 Oct 2025 18:47:04 +0000</lastBuildDate><atom:link href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/posts/index.xml" rel="self" type="application/rss+xml"/><item><title>Skin routine</title><link>http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/posts/skin_routine/</link><pubDate>Mon, 20 Oct 2025 18:47:04 +0000</pubDate><guid>http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/posts/skin_routine/</guid><description>My skin routine!</description><content:encoded><![CDATA[<p>I don&rsquo;t know how to answer the &ldquo;why?&rdquo; or &ldquo;whyyyyy?&rdquo; or even &ldquo;whe the f***?&rdquo; I have a skin routine.
Last year, after I came to Germany, I asked a female friend about how to do skin care.
She touched my face and said &ldquo;Knock on wood, you have good skin!&rdquo;.
So&hellip; idk why I decided to take extra care of my skin, but I did!</p>
<p>Generaly speaking, things like this make me feel good about myself.
Like I&rsquo;m doing something positive while not being tortured!
It&rsquo;s always fun to rub a creme on your face, or gently message it.
Even cleaning the face skin feels refreshing.
Everything also smells nice!</p>
<p>Oh&hellip; and yeah, idk why I&rsquo;m not good at excercising, but I really like to do things like this!
Weird.
I should definitly start going to gym and working out.
It is needed for me.</p>
<p>So&hellip; I decided to watch a few Youtube videos, and a guide about skin care for men.
My routine is super simple!
I have a face cleanser that I use first and wash my face with it.
It always feels refreshing and nice to use it!
I initially bought the Cerave cleaner, but switched to &ldquo;Jack Black Pure Clean Daily Facial Cleanser&rdquo; after that one ended.
The cleanser deeply cleans your skin, and almost all of the bad things that might be on your skin.
You don&rsquo;t need to use the cleanser in the morning, but you should definitely use it at night.
It&rsquo;s ok to wash your face with water in the morning.</p>
<p>Next step for me is applying the toner.
I use &ldquo;NIVEA Derma Skin Clear Toner&rdquo;.
It smells really nice, and is quite refreshing to apply to skin!
The toner adjusts the PH of your skin, and deep cleans things that the cleanser could not.
I gently rub it and let it dry, no rinsing is required here.</p>
<p>After this I apply an exfoliant to exfoliate my skin.
I&rsquo;ve been using &ldquo;Paula&rsquo;s Choice Skin Perfecting 2% BHA Liquid Exfoliant&rdquo; so far, but this time I got &ldquo;BULLDOG Original Exfoliating Face Scrub for Purer Skin&rdquo;.
Haven&rsquo;t used it yet, but the Paula&rsquo;s choice one is definitly good.
The thing with it is that you don&rsquo;t have to message it, it&rsquo;s not physical, it&rsquo;s an acid.
I prefer the scrubby oness, but I think these are better for your skin.
I&rsquo;ll see how I like the new one, and if I prefer it or not.
No rinsing is required here either.</p>
<p>I then apply my eye cream which is also from CeraVe.
Haven&rsquo;t really seen much of a difference under my eyes, but it is supposed to help.
I don&rsquo;t know! It feels good to apply the eye cream regardless.</p>
<p>The one to the last step for me is the best one!
Moisturiser.
Yayy!!!
It actually feels weird to use a moisturiser since I&rsquo;ve watched Mortuary Assisant&rsquo;s gameplay, and the last step there for embalming the body is applying moisturiser.
Feels weird.
The one I use is water based, hence perfect for men.
I use &ldquo;Neutrogena Hydro Boost Aqua Gel Moisturiser&rdquo;.
As a man if you use oil-based, you&rsquo;ll get acne.
So don&rsquo;t.</p>
<p>I learned to use pads for applying some of these stuff, it feels cooler when using, specifically when you gently tap your face with a cotton pad! ^^</p>
<p>And&hellip; last but not least is applying sun screen.
Nothing special here.
I just make sure to use SPF 50+ sun screen for better protection.</p>
<p>Even if it does nothing, it still makes me feel good about myself.</p>
<p>The whole routine does not take more than 10 min, but gives me an energy boost, and a lot of good vibes both at night and in the morning!</p>
]]></content:encoded></item><item><title>INET Logo That Breathes — From CAD to LEDs to a Calm Little Server</title><link>http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/posts/inet_logo/</link><pubDate>Fri, 17 Oct 2025 00:00:00 +0000</pubDate><guid>http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/posts/inet_logo/</guid><description>&lt;blockquote>
&lt;p>I didnt add a warning sign to the room. I taught the &lt;strong>logo&lt;/strong> to breathe. ;-D&lt;/p>&lt;/blockquote>
&lt;p>The Rotunde is a good room for bold ideas and yummy coffee. The door sighs shut and the outside world gives up on us. The only thing it&amp;rsquo;s not good at is &lt;strong>ventilating&lt;/strong> itself. Forty minutes into a group meeting, or a sressful defence, and we all feel like sleeping and running back to our offices!&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>I didnt add a warning sign to the room. I taught the <strong>logo</strong> to breathe. ;-D</p></blockquote>
<p>The Rotunde is a good room for bold ideas and yummy coffee. The door sighs shut and the outside world gives up on us. The only thing it&rsquo;s not good at is <strong>ventilating</strong> itself. Forty minutes into a group meeting, or a sressful defence, and we all feel like sleeping and running back to our offices!</p>
<p>So I crafted the INET logo and gave it a 9-5 job: be a <strong>polite barometer</strong>. If the air is fresh, it glows cool and calm. If its getting stale, it warms up. No blinking warnings, no sound effects — just mood.</p>
<p>This post is the story of how I designed the channel and diffuser, printed the parts, cut, layed out, and soldered the snippets of LED strips, wrote the software, and wrapped it all in a systemd service so it wakes up with the Pi. Feel free to grab a cup of coffee and enjoy the post! I&rsquo;ll try to include as much detail as I can.</p>
<hr>
<h2 id="1-sketch--cad--print">1) Sketch → CAD → Print</h2>
<p><img alt="Fusion design + logotype" loading="lazy" src="/images/inet/inet_logo_fusion_merged.png"></p>
<p>I started the way all sensible hardware projects start: with <strong>overconfidence</strong> and a sketch. The INET logotype looks simple from the front but its a small maze inside. I modeled pockets for each letter in Fusion: a tall <strong>I</strong>, the curving <strong>N</strong>, three stacked bars for <strong>E</strong>, and the long arm of <strong>T</strong>. Each pocket got:</p>
<ul>
<li><strong>Mounting bosses</strong> for the front diffusers (M2 screws),</li>
<li><strong>Cable holes</strong> between chambers (rounded to avoid cutting silicone wire), and</li>
<li>A small <strong>wiring bay</strong> for the controller and power.</li>
</ul>
<p>I printed the channel parts in matte black PLA for heat resilience and the diffusers in white PLA at <strong>1-2 mm</strong> thick to hide hotspots without wasting brightness. That thickness ended up perfect; you can still see motion, but the individual LED package disappears.
The print process took around 24h, including the misprints, and the in-between prints for adjusting the sizes and calibrating everything. Each letter was either printed in multiple parts, or individually so that the logo becomes as large as possible. I used around 1Kg of fillament to print the whole thing.</p>
<p>The design process took around 18 iterations, with initial iteration being simple letters in italic form, and the later ones being more similar to the original INET logo of our group.
I had some experience with CAD, and had designed some simple things to address my everyday problems, but this was a more serious one. Overall it took me a few days to design the logo and print it part by part. The design is not the most artistic, but as authentic as I could get it.</p>
<hr>
<h2 id="2-the-look-i-wanted">2) The Look I Wanted</h2>
<p><img alt="Animations collage" loading="lazy" src="/images/inet/inet_animation_collage.jpg"></p>
<p>I wasn&rsquo;t building a stage light. The goal was <strong>calm</strong>: slow waves, breathing brightness, gentle comets. Everything reads through those letter chambers like a tiny light sculpture. The cyan frame in the collage is the <strong>CO₂ monitor</strong> in a “fresh air” state (more on the color language below). There are many many other animations that I stole from different git repositories, but the most used one is the Co2 monitor with breathing animation. I did want to include more animations other than breathing, but that will be a future me problem with creativity left to spare; right now my creativity well is as dry as the paint on my wall! For now, I&rsquo;m happy with it and want to let it be there doing it&rsquo;s job.</p>
<hr>
<h2 id="3-the-tidy-mess-behind-the-panel">3) The Tidy Mess Behind the Panel</h2>
<p><img alt="Hardware collage" loading="lazy" src="/images/inet/inet_hardware_collage.jpg"></p>
<p>Inside the box theres a Raspberry Pi zero 2 W, a short run of WS2812B LEDs (78 pixels total), a 5 V 34 A supply, jumpers that mind their manners, and two little hardware choices that pay rent every day:</p>
<ul>
<li>A <strong>330470 Ω</strong> series resistor on the <em>data</em> line right at the first pixel. It damps ringing and prevents weird flickers.</li>
<li>A <strong>1000 µF</strong> electrolytic across <strong>5V/GND</strong> at the strip start. It handles inrush so the first LED doesnt faint at boot.</li>
</ul>
<p>I route the strip <strong>DIN → DOUT</strong> through the chambers and use short silicone jumpers at the bends. Every joint gets heatshrink and a tiny dab of hot glue as strain relief. I continuitycheck <strong>before</strong> power and bring brightness up slowly on a bench supply.</p>
<p>The soldering was a big mess. I had never done hardware debugging before. My soldering skills were definitly challenged for this project, as the connections kept breaking when I moved the logo. I did learn how to perform better soldering, but it was already too late. I had to tin the wires after adding flux, and let the soldering wire go up the wire to get stronger connections and leave no naked coppers. A note to remember for next soldering journeys. It took me around two whole days to solder everything and debug it. At some point there was a faulty LED in the middle that caused shorting, and I found and cut it out. This was the most annoying part of the debugging as I had to go through the LEDs, measuring voltage difference to see what is causing the problem. I honestly gave up somewhere in here and thought I won&rsquo;t be able to push through the marathon, but here I am, done with the logo, happy as a four young old licking their ice cream! xD</p>
<hr>
<h2 id="4-a-web-panel-that-stays-out-of-the-way">4) A Web Panel that Stays Out of the Way</h2>
<p><img alt="UI collage" loading="lazy" src="/images/inet/inet_ui_collage.jpg"></p>
<p>The web UI is quiet on purpose: a single navbar, a <code>/control</code> page with big samesize buttons, a <code>/schedule</code> table, a draganddrop <code>/calendar</code>, a <code>/status</code> page that updates once a second, and <code>/history</code> charts for CO₂/temperature/humidity with white backgrounds so theyre legible on a projector. Temprature values are not about room, but the box, as I could not mount the sensor outside of the box easily. I just let it sit inside the box. The panel was created with the help of trusty vibe-coding (!). I used chat GPT to create a template, and expanded it for the purpose. Flask made things very easy.</p>
<p>Theres also a <strong>Safe Mode</strong> switch that hides flashier patterns. Meeting rooms are for thinking, not strobe tests, and you never know who might have photosensitive epilepsy, and it&rsquo;s not funny for anyone to fidn this out when looking at your creation. So&hellip; yea, I took precautions not to see people getting seizures looking at my creation. :-)</p>
<hr>
<h2 id="5-the-color-language-for-air">5) The Color Language for Air</h2>
<p>The <code>co2_monitor</code> animation maps CO₂ ppm → color and adds slow motion so it doesnt feel like a stoplight:</p>
<ul>
<li><strong>&lt; 500 ppm</strong>: Cyan — outdoorlike fresh air.</li>
<li><strong>500799</strong>: Green — good.</li>
<li><strong>8001199</strong>: Yellow — getting stale.</li>
<li><strong>12001499</strong>: Orange — poor; youll feel it.</li>
<li><strong>15001999</strong>: Red — ventilate now.</li>
<li><strong>≥ 2000</strong>: Purple — the logo is politely yelling.</li>
</ul>
<p>I blend the readings with an <strong>exponential moving average</strong> and use <strong>hysteresis</strong> around thresholds so it doesnt pingpong colors when the room hovers at 800 ppm. Then I ease the current hue toward the target color and apply a very slow <strong>breathing brightness</strong>. The result feels alive but never flashy.</p>
<p>By default, the lights turn off from 20 to 6, then from 6 to 9, and 5 to 8 the standby red pulse is shown. From 9-5 on workdays the logo does it&rsquo;s daily job of Co2 monitoring. Well, technically it does that all the time, but during those hours it shows the Co2 level by it&rsquo;s color, the remaining time it just keeps a record of the Co2, temprature and humidity of room in the database.</p>
<hr>
<h2 id="6-the-code--a-guided-tour-no-giant-blob-promise">6) The Code — a guided tour (no giant blob, promise)</h2>
<p>This whole project runs from a single Python file. Think of it like a tiny orchestra:<br>
one thread conducts the <strong>LEDs</strong>, one listens politely to the <strong>CO₂ sensor</strong>, one keeps time as the <strong>scheduler</strong>, and a small <strong>web server</strong> hands you the baton when you want to improvise.</p>
<p>Below is what each section does, with just enough code to be useful (and not enough to make your eyes cross).</p>
<hr>
<h3 id="61-configuration-the-knobs">6.1 Configuration (the knobs)</h3>
<p>Let&rsquo;s define where the LEDs live, how bright they can get, and where to save tiny bits of state (schedule and sensor DB). All of it can be overridden with environment variables so you dont edit code to change pin numbers.</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-python" data-lang="python"><span style="display:flex;"><span>LED_COUNT<span style="color:#f92672">=</span><span style="color:#ae81ff">78</span>, LED_PIN<span style="color:#f92672">=</span><span style="color:#ae81ff">18</span>, LED_BRIGHT<span style="color:#f92672">=</span><span style="color:#ae81ff">255</span>
</span></span><span style="display:flex;"><span>SCHEDULE_PATH<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;schedule.json&#34;</span>
</span></span><span style="display:flex;"><span>DB_PATH<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;sensor.db&#34;</span>
</span></span><span style="display:flex;"><span>SAFE_MODE<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span> <span style="color:#75715e"># hide flashy animations by default</span>
</span></span></code></pre></div><p><em>Why its like this:</em> simple, explicit defaults → less “why is it dark” debugging.</p>
<hr>
<h3 id="62-strip-setup--frame-diff-no-flicker-magic">6.2 Strip setup + frame-diff (no flicker magic)</h3>
<p>Now initialize <code>rpi_ws281x.PixelStrip</code> and put a <strong>shadow frame buffer</strong> in front of it.</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-python" data-lang="python"><span style="display:flex;"><span>frame <span style="color:#f92672">=</span> [(<span style="color:#ae81ff">0</span>,<span style="color:#ae81ff">0</span>,<span style="color:#ae81ff">0</span>)] <span style="color:#f92672">*</span> LED_COUNT
</span></span><span style="display:flex;"><span>_dirty <span style="color:#f92672">=</span> <span style="color:#66d9ef">False</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">_set_pixel</span>(i, r, g, b):
</span></span><span style="display:flex;"><span> <span style="color:#75715e"># only touch hardware if the value actually changed</span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> frame[i] <span style="color:#f92672">!=</span> (r,g,b):
</span></span><span style="display:flex;"><span> strip<span style="color:#f92672">.</span>setPixelColorRGB(i, r, g, b)
</span></span><span style="display:flex;"><span> frame[i] <span style="color:#f92672">=</span> (r,g,b)
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">global</span> _dirty; _dirty <span style="color:#f92672">=</span> <span style="color:#66d9ef">True</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">flush</span>():
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> _dirty: strip<span style="color:#f92672">.</span>show(); _dirty<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>
</span></span></code></pre></div><p><em>Why its like this:</em> LEDs are zen monks—disturb them only when you must. The frame-diff prevents those mysterious “one frame flash” moments that happen when you call <code>show()</code> with identical data.</p>
<hr>
<h3 id="63-a-tiny-color-wheel-helper">6.3 A tiny color wheel helper</h3>
<p>Classic rainbow helper used by a few animations:</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-python" data-lang="python"><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">wheel</span>(pos):
</span></span><span style="display:flex;"><span> <span style="color:#75715e"># 0..255 → (r,g,b)</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">...</span>
</span></span></code></pre></div><p><em>Why its like this:</em> I&rsquo;ll use it again and again; it keeps color math out of the animations.</p>
<hr>
<h3 id="64-state--orchestration">6.4 State &amp; orchestration</h3>
<p>Let&rsquo;s hold a registry of animations, a stop flag, and the currently playing thread.</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-python" data-lang="python"><span style="display:flex;"><span>animations <span style="color:#f92672">=</span> {}
</span></span><span style="display:flex;"><span>stop_event <span style="color:#f92672">=</span> threading<span style="color:#f92672">.</span>Event()
</span></span><span style="display:flex;"><span>current_thread <span style="color:#f92672">=</span> <span style="color:#66d9ef">None</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">register</span>(name, safe<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>):
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">wrap</span>(fn):
</span></span><span style="display:flex;"><span> animations[name] <span style="color:#f92672">=</span> {<span style="color:#e6db74">&#34;fn&#34;</span>: fn, <span style="color:#e6db74">&#34;safe&#34;</span>: safe}
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span> fn
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span> wrap
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">run_animation</span>(fn):
</span></span><span style="display:flex;"><span> <span style="color:#75715e"># cooperative hand-off: stop current, start next</span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> current_thread <span style="color:#f92672">and</span> current_thread<span style="color:#f92672">.</span>is_alive():
</span></span><span style="display:flex;"><span> stop_event<span style="color:#f92672">.</span>set(); current_thread<span style="color:#f92672">.</span>join()
</span></span><span style="display:flex;"><span> stop_event<span style="color:#f92672">.</span>clear()
</span></span><span style="display:flex;"><span> t <span style="color:#f92672">=</span> threading<span style="color:#f92672">.</span>Thread(target<span style="color:#f92672">=</span>fn, name<span style="color:#f92672">=</span>fn<span style="color:#f92672">.</span>__name__, daemon<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>)
</span></span><span style="display:flex;"><span> t<span style="color:#f92672">.</span>start(); globals()[<span style="color:#e6db74">&#34;current_thread&#34;</span>] <span style="color:#f92672">=</span> t
</span></span></code></pre></div><p><em>Why its like this:</em> adding a new animation is a one-liner <code>@register(&quot;name&quot;)</code>. Clean exits prevent torn frames and half-drawn comets.</p>
<hr>
<h3 id="65-animations-the-mood">6.5 Animations (the mood)</h3>
<p>Each animation is a loop that checks <code>stop_event.is_set()</code> and draws frames with <code>_set_pixel(...)</code> + <code>flush()</code>.</p>
<ul>
<li><strong><code>redpulse</code></strong> — calm breathing in red; great default.</li>
<li><strong><code>colorwave</code></strong> — slow rainbow that reads clearly behind diffusers.</li>
<li><strong><code>comet</code></strong> — a single head with a fading tail orbiting the logo.</li>
</ul>
<p>They all end with <code>finally: clear_strip()</code> so the panel doesnt freeze on the last pose if you stop mid-frame.</p>
<p><em>Why its like this:</em> cooperative loops + frame-diff = smooth, interruption-safe effects.</p>
<hr>
<h3 id="66-co-color-language-with-smoothing--hysteresis">6.6 CO₂ color language (with smoothing + hysteresis)</h3>
<p>The star of the show. Now map ppm to color <strong>bands</strong> and keep transitions human-pleasant.</p>
<ul>
<li><code>&lt; 500</code>: <strong>Cyan</strong> (fresh)</li>
<li><code>500799</code>: <strong>Green</strong></li>
<li><code>8001199</code>: <strong>Yellow</strong></li>
<li><code>12001499</code>: <strong>Orange</strong></li>
<li><code>15001999</code>: <strong>Red</strong></li>
<li><code>≥ 2000</code>: <strong>Purple</strong></li>
</ul>
<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-python" data-lang="python"><span style="display:flex;"><span>BANDS<span style="color:#f92672">=</span>[(<span style="color:#ae81ff">0</span>,<span style="color:#ae81ff">500</span>,(<span style="color:#ae81ff">0</span>,<span style="color:#ae81ff">214</span>,<span style="color:#ae81ff">255</span>)), (<span style="color:#ae81ff">500</span>,<span style="color:#ae81ff">800</span>,(<span style="color:#ae81ff">17</span>,<span style="color:#ae81ff">204</span>,<span style="color:#ae81ff">85</span>)), <span style="color:#f92672">...</span> (<span style="color:#ae81ff">2000</span>,<span style="color:#960050;background-color:#1e0010">∞</span>,(<span style="color:#ae81ff">123</span>,<span style="color:#ae81ff">44</span>,<span style="color:#ae81ff">191</span>))]
</span></span><span style="display:flex;"><span>EMA_ALPHA<span style="color:#f92672">=</span><span style="color:#ae81ff">0.15</span>; HYST<span style="color:#f92672">=</span><span style="color:#ae81ff">35</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Every tick:</span>
</span></span><span style="display:flex;"><span>ema <span style="color:#f92672">=</span> (<span style="color:#ae81ff">1</span><span style="color:#f92672">-</span>EMA_ALPHA)<span style="color:#f92672">*</span>ema <span style="color:#f92672">+</span> EMA_ALPHA<span style="color:#f92672">*</span>co2
</span></span><span style="display:flex;"><span>band <span style="color:#f92672">=</span> hysteresis_aware_band(ema, last_band, HYST)
</span></span><span style="display:flex;"><span>target <span style="color:#f92672">=</span> BANDS[band]<span style="color:#f92672">.</span>color
</span></span><span style="display:flex;"><span>current <span style="color:#f92672">=</span> lerp(current, target, <span style="color:#ae81ff">0.10</span>) <span style="color:#75715e"># gentle hue easing</span>
</span></span><span style="display:flex;"><span>level <span style="color:#f92672">=</span> breathe(<span style="color:#ae81ff">0.25</span> Hz, low<span style="color:#f92672">=</span><span style="color:#ae81ff">10</span>, high<span style="color:#f92672">=</span><span style="color:#ae81ff">220</span>)
</span></span><span style="display:flex;"><span>draw all pixels <span style="color:#f92672">=</span> current <span style="color:#f92672">*</span> level
</span></span></code></pre></div><p><em>Why its like this:</em> EMA + hysteresis prevents “800↔801 disco.” The slow breathing keeps the panel feeling alive, not like a traffic light.</p>
<hr>
<h3 id="67-optional-scd41-sensor-thread-the-polite-listener">6.7 Optional SCD41 sensor thread (the polite listener)</h3>
<p>If the SCD41 is present, a <strong>dedicated thread</strong> owns I²C and updates a global <code>co2_data</code> dict every few seconds. It also logs to a tiny SQLite database.</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-python" data-lang="python"><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">_start_sensor</span>():
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">try</span>:
</span></span><span style="display:flex;"><span> scd4x <span style="color:#f92672">=</span> adafruit_scd4x<span style="color:#f92672">.</span>SCD4X(i2c); scd4x<span style="color:#f92672">.</span>start_periodic_measurement()
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">except</span>: <span style="color:#66d9ef">return</span> <span style="color:#75715e"># sensor optional</span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">loop</span>():
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">while</span> <span style="color:#66d9ef">True</span>:
</span></span><span style="display:flex;"><span> time<span style="color:#f92672">.</span>sleep(<span style="color:#ae81ff">5</span>)
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> scd4x<span style="color:#f92672">.</span>data_ready:
</span></span><span style="display:flex;"><span> co2_data<span style="color:#f92672">.</span>update({<span style="color:#f92672">...</span>})
</span></span><span style="display:flex;"><span> db<span style="color:#f92672">.</span>insert(timestamp, co2, temperature, humidity)
</span></span><span style="display:flex;"><span> threading<span style="color:#f92672">.</span>Thread(target<span style="color:#f92672">=</span>loop, daemon<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>)<span style="color:#f92672">.</span>start()
</span></span></code></pre></div><p><em>Why its like this:</em> one thread owns the bus → no read contention. The controller keeps working even without a sensor.</p>
<hr>
<h3 id="68-scheduler--90-minute-override-humans-win-then-reset">6.8 Scheduler + 90-minute override (humans win, then reset)</h3>
<p>A background thread asks, every few seconds, <strong>which</strong> mode should be running:</p>
<ol>
<li>If the user chose something recently (override), respect it for <code>TTL = 90 min</code> (or the duration set in the UI).</li>
<li>Otherwise, apply the <strong>default schedule</strong> (Wednesday 1417 → <code>slow</code>, else <code>redpulse</code>).</li>
<li>Save/load the schedule atomically to <code>schedule.json</code>.</li>
</ol>
<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-python" data-lang="python"><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">scheduler_pick</span>():
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> now <span style="color:#f92672">&lt;</span> override_until: <span style="color:#66d9ef">return</span> override_mode
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">for</span> row <span style="color:#f92672">in</span> load_schedule():
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> matches(now, row): <span style="color:#66d9ef">return</span> row[<span style="color:#e6db74">&#34;mode&#34;</span>]
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span> <span style="color:#e6db74">&#34;redpulse&#34;</span>
</span></span></code></pre></div><p><em>Why its like this:</em> you can play DJ for a meeting, and the room quietly returns to its routine afterward.</p>
<hr>
<h3 id="69-the-web-panel-tiny-flask-big-buttons">6.9 The web panel (tiny Flask, big buttons)</h3>
<p>Three pages, no fuss:</p>
<ul>
<li><code>/status</code> — live JSON (<code>/status_data</code>) every second → current mode + CO₂/Temp/Humidity.</li>
<li><code>/control</code> — grid of same-size buttons (respects <strong>Safe Mode</strong>), optional <strong>duration</strong> (0180 min) with validation.</li>
<li><code>/schedule</code> — simple table editor; <strong>Add Row</strong> and <strong>Save</strong>; writes atomically.</li>
</ul>
<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-python" data-lang="python"><span style="display:flex;"><span><span style="color:#a6e22e">@app.post</span>(<span style="color:#e6db74">&#34;/set&#34;</span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">set_mode</span>():
</span></span><span style="display:flex;"><span> mode <span style="color:#f92672">=</span> request<span style="color:#f92672">.</span>form[<span style="color:#e6db74">&#34;mode&#34;</span>]
</span></span><span style="display:flex;"><span> minutes <span style="color:#f92672">=</span> clamp( int(form[<span style="color:#e6db74">&#34;duration&#34;</span>]), <span style="color:#ae81ff">0</span>, <span style="color:#ae81ff">180</span> )
</span></span><span style="display:flex;"><span> set_override(mode, minutes)
</span></span><span style="display:flex;"><span> run_animation(animations[mode][<span style="color:#e6db74">&#34;fn&#34;</span>])
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span> redirect(<span style="color:#e6db74">&#34;/control&#34;</span>)
</span></span></code></pre></div><p><em>Why its like this:</em> minimal routes are easier to maintain and dont compete with the art piece.</p>
<hr>
<h3 id="610-boot-choreography">6.10 Boot choreography</h3>
<p>At startup I:</p>
<ol>
<li>Try the <strong>sensor thread</strong> (if hardware is there).</li>
<li>Start the <strong>scheduler thread</strong>.</li>
<li>Immediately play <strong>redpulse</strong> (so theres a friendly glow right away).</li>
<li>Start Flask; on shutdown I <strong>clear the strip</strong>.</li>
</ol>
<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-python" data-lang="python"><span style="display:flex;"><span><span style="color:#66d9ef">if</span> __name__ <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;__main__&#34;</span>:
</span></span><span style="display:flex;"><span> _start_sensor()
</span></span><span style="display:flex;"><span> threading<span style="color:#f92672">.</span>Thread(target<span style="color:#f92672">=</span>scheduler_loop, daemon<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>)<span style="color:#f92672">.</span>start()
</span></span><span style="display:flex;"><span> run_animation(redpulse)
</span></span><span style="display:flex;"><span> app<span style="color:#f92672">.</span>run(host<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;0.0.0.0&#34;</span>, port<span style="color:#f92672">=</span><span style="color:#ae81ff">5000</span>)
</span></span></code></pre></div><p><em>Why its like this:</em> the room sees something alive instantly; the scheduler will swap in the “real” choice within a few seconds.</p>
<hr>
<h3 id="tldr-but-keep-the-vibes">TL;DR (but keep the vibes)</h3>
<ul>
<li><strong>Frame-diff</strong> = no flashes.</li>
<li><strong>EMA + hysteresis</strong> = smooth, truthful color.</li>
<li><strong>Breathing</strong> = presence, not signage.</li>
<li><strong>Threads per thing</strong> (LEDs / sensor / scheduler) = no fights.</li>
<li><strong>Atomic files</strong> = no corrupted schedules.</li>
<li><strong>Safe Mode</strong> by default = people &gt; pixels.</li>
</ul>
<p>Thats it — the code behaves like a considerate colleague: dependable, quiet, and occasionally very pretty.</p>
<h3 id="why-this-shape-of-code">Why this shape of code?</h3>
<ul>
<li><strong>One thread per hardware thing.</strong> The sensor thread owns I²C. The animation thread owns LEDs. The scheduler just decides <em>what</em> to run and when. No bus fights.</li>
<li><strong>Frame diff.</strong> I only call <code>strip.show()</code> when a pixel actually changes. Thats why it doesnt randomly flash during web requests.</li>
<li><strong>Atomic schedule saves.</strong> The code writes to a temporary file and replaces the old one so a midsave power cut cant corrupt the schedule.</li>
</ul>
<blockquote>
<p>No, you dont <em>need</em> the sensor. If its not connected, the app still runs; <code>co2_monitor</code> just sits at the default value. Well, technically it shows NaN as the numbers! But it is fun to have a sensor, don&rsquo;t you agree?</p></blockquote>
<hr>
<h2 id="7-sensor-database--what-it-stores-where-it-lives-and-how-far-it-goes">7) Sensor Database — what it stores, where it lives, and how far it goes</h2>
<p>This project logs room conditions to a <strong>tiny SQLite database</strong> so you can graph trends, spot stuffy meetings, and keep a record without running a separate server.</p>
<h3 id="whats-stored">Whats stored</h3>
<p>A single table called <code>readings</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-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">CREATE</span> <span style="color:#66d9ef">TABLE</span> <span style="color:#66d9ef">IF</span> <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">EXISTS</span> readings (
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">timestamp</span> TEXT <span style="color:#66d9ef">PRIMARY</span> <span style="color:#66d9ef">KEY</span>, <span style="color:#75715e">-- &#34;YYYY-MM-DD HH:MM:SS&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> co2 INTEGER, <span style="color:#75715e">-- ppm
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> temperature REAL, <span style="color:#75715e">-- °C
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> humidity REAL <span style="color:#75715e">-- %
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>);
</span></span></code></pre></div><ul>
<li>One row per sample (typically every <strong>560 seconds</strong>; slow it down to reduce writes).</li>
<li><code>timestamp</code> is the primary key → easy “latest” queries and natural time ordering.</li>
</ul>
<h3 id="where-the-file-lives">Where the file lives</h3>
<ul>
<li>Path is configurable via env var <code>DB_PATH</code> (see your systemd unit).</li>
<li>Default: <code>sensor.db</code> in the apps working directory (e.g. <code>/home/pi/inet-led/sensor.db</code>).</li>
</ul>
<p>Check size:</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>ls -lh /home/pi/inet-led/sensor.db
</span></span></code></pre></div><h3 id="how-writes-happen-and-why-its-safe">How writes happen (and why its safe)</h3>
<ul>
<li>The <strong>sensor thread</strong> owns I²C and inserts a row only when the sensor reports <code>data_ready</code>.</li>
<li>Inserts are short and journaling protects against power loss.</li>
<li>For friendlier read/write concurrency, enable WAL once at startup:</li>
</ul>
<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-sql" data-lang="sql"><span style="display:flex;"><span>PRAGMA journal_mode <span style="color:#f92672">=</span> WAL;
</span></span><span style="display:flex;"><span>PRAGMA synchronous <span style="color:#f92672">=</span> NORMAL;
</span></span></code></pre></div><h3 id="typical-size--retention">Typical size &amp; retention</h3>
<p>Back-of-envelope:</p>
<ul>
<li>~100200 bytes per row (including overhead).</li>
<li>1 sample/minute → ~1,440 rows/day → <strong>~150300 KB/day</strong> → <strong>55110 MB/year</strong>.</li>
<li>If you log every 5 seconds, multiply by ~12 (consider slower logging or downsampling).</li>
</ul>
<p>Prune old data (keep last 90 days):</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-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">DELETE</span> <span style="color:#66d9ef">FROM</span> readings
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">WHERE</span> <span style="color:#66d9ef">timestamp</span> <span style="color:#f92672">&lt;</span> date(<span style="color:#e6db74">&#39;now&#39;</span>,<span style="color:#e6db74">&#39;-90 day&#39;</span>);
</span></span></code></pre></div><p>(Optionally run <code>VACUUM;</code> after large deletes to reclaim file space.)</p>
<h3 id="useful-queries">Useful queries</h3>
<p><strong>Latest reading:</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-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">SELECT</span> <span style="color:#f92672">*</span> <span style="color:#66d9ef">FROM</span> readings <span style="color:#66d9ef">ORDER</span> <span style="color:#66d9ef">BY</span> <span style="color:#66d9ef">timestamp</span> <span style="color:#66d9ef">DESC</span> <span style="color:#66d9ef">LIMIT</span> <span style="color:#ae81ff">1</span>;
</span></span></code></pre></div><p><strong>Range for charts (last 7 days):</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-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">SELECT</span> <span style="color:#f92672">*</span> <span style="color:#66d9ef">FROM</span> readings
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">WHERE</span> <span style="color:#66d9ef">timestamp</span> <span style="color:#f92672">&gt;=</span> datetime(<span style="color:#e6db74">&#39;now&#39;</span>,<span style="color:#e6db74">&#39;-7 day&#39;</span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">ORDER</span> <span style="color:#66d9ef">BY</span> <span style="color:#66d9ef">timestamp</span>;
</span></span></code></pre></div><p><strong>Downsample to hourly averages (30 days):</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-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">SELECT</span> strftime(<span style="color:#e6db74">&#39;%Y-%m-%d %H:00:00&#39;</span>, <span style="color:#66d9ef">timestamp</span>) <span style="color:#66d9ef">AS</span> hour,
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">AVG</span>(co2) <span style="color:#66d9ef">AS</span> co2, <span style="color:#66d9ef">AVG</span>(temperature) <span style="color:#66d9ef">AS</span> t, <span style="color:#66d9ef">AVG</span>(humidity) <span style="color:#66d9ef">AS</span> h
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">FROM</span> readings
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">WHERE</span> <span style="color:#66d9ef">timestamp</span> <span style="color:#f92672">&gt;=</span> datetime(<span style="color:#e6db74">&#39;now&#39;</span>,<span style="color:#e6db74">&#39;-30 day&#39;</span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">GROUP</span> <span style="color:#66d9ef">BY</span> hour
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">ORDER</span> <span style="color:#66d9ef">BY</span> hour;
</span></span></code></pre></div><p><strong>Export to CSV (example via sqlite3 CLI):</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>sqlite3 /home/pi/inet-led/sensor.db <span style="color:#e6db74">&lt;&lt;&#39;SQL&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">.headers on
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">.mode csv
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">.output /home/pi/inet-led/export_readings.csv
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">SELECT * FROM readings ORDER BY timestamp;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">.output stdout
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">SQL</span>
</span></span></code></pre></div><h3 id="sd-card-friendliness">SD card friendliness</h3>
<ul>
<li>Choose a sensible interval (e.g., <strong>3060 s</strong>).</li>
<li>Optionally buffer in memory and write every N samples.</li>
<li>Prefer WAL mode; periodically prune &amp; vacuum.</li>
<li>If you care about card wear, place the DB on USB/SSD.</li>
</ul>
<h3 id="backups--restore">Backups &amp; restore</h3>
<p><strong>Nightly backup (simple):</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>sqlite3 /home/pi/inet-led/sensor.db <span style="color:#e6db74">&#34;.backup &#39;/home/pi/backup/sensor-</span><span style="color:#66d9ef">$(</span>date +%F<span style="color:#66d9ef">)</span><span style="color:#e6db74">.db&#39;&#34;</span>
</span></span></code></pre></div><p><strong>Restore:</strong></p>
<ol>
<li><code>sudo systemctl stop inet-led</code></li>
<li>Copy backup file back to <code>/home/pi/inet-led/sensor.db</code></li>
<li><code>sudo systemctl start inet-led</code></li>
</ol>
<h3 id="security--permissions">Security &amp; permissions</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>chown pi:pi /home/pi/inet-led/sensor.db
</span></span><span style="display:flex;"><span>chmod <span style="color:#ae81ff">600</span> /home/pi/inet-led/sensor.db
</span></span></code></pre></div><p>Keep the app directory non-world-readable.</p>
<h3 id="is-sqlite-the-right-choice">Is SQLite the right choice?</h3>
<p><strong>Yes, for a single Pi</strong> logging every few seconds and rendering local charts:</p>
<ul>
<li>✅ Zero admin, a single file, reliable journaling, great performance at this scale.</li>
<li>⚠️ One writer at a time (I only have the sensor thread writing, so its fine).</li>
<li>⬆️ If you later need multi-device ingestion, alerts, or &gt;10s of millions of rows, consider a time-series DB (TimescaleDB/InfluxDB/VictoriaMetrics) and migrate using CSV exports.</li>
</ul>
<h3 id="tldr">TL;DR</h3>
<p>SQLite is perfect here: <strong>simple, robust, easy to back up</strong>. Start with it, and only upgrade when your ambitions outgrow a single Raspberry Pi.</p>
<hr>
<h2 id="8-run-at-boot-systemd">8) Run at Boot (systemd)</h2>
<p>I wrote this simple systemd to <code>/etc/systemd/system/inet-led.service</code> so that it runs the script when RPi boots 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-ini" data-lang="ini"><span style="display:flex;"><span><span style="color:#66d9ef">[Unit]</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">Description</span><span style="color:#f92672">=</span><span style="color:#e6db74">INET LED Panel</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">After</span><span style="color:#f92672">=</span><span style="color:#e6db74">network.target</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">[Service]</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">ExecStart</span><span style="color:#f92672">=</span><span style="color:#e6db74">/usr/bin/python3 /home/pi/inet-led/inet_led_panel.py</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">WorkingDirectory</span><span style="color:#f92672">=</span><span style="color:#e6db74">/home/pi/inet-led</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">Restart</span><span style="color:#f92672">=</span><span style="color:#e6db74">always</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">User</span><span style="color:#f92672">=</span><span style="color:#e6db74">pi</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">Environment</span><span style="color:#f92672">=</span><span style="color:#e6db74">LED_COUNT=78</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">Environment</span><span style="color:#f92672">=</span><span style="color:#e6db74">SCHEDULE_FILE=/home/pi/inet-led/schedule.json</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">Environment</span><span style="color:#f92672">=</span><span style="color:#e6db74">DB_PATH=/home/pi/inet-led/sensor.db</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">[Install]</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">WantedBy</span><span style="color:#f92672">=</span><span style="color:#e6db74">multi-user.target</span>
</span></span></code></pre></div><p>Then you can run:</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 systemctl daemon-reload
</span></span><span style="display:flex;"><span>sudo systemctl enable --now inet-led
</span></span><span style="display:flex;"><span>journalctl -u inet-led -f
</span></span></code></pre></div><p>to control the systemd service.</p>
<hr>
<h2 id="9-soldering-notes-a-love-letter-to-hot-glue">9) Soldering Notes (a love letter to hot glue)</h2>
<ul>
<li><strong>Tin first, solder second.</strong> Tiny pads like tiny puddles. Fast in/out, no lifted pads.</li>
<li><strong>Stagger joints</strong> so nothing stacks under the diffuser.</li>
<li><strong>Heatshrink + a dot of hot glue</strong> = happier future you.</li>
<li><strong>Continuity before power.</strong> Multimeter first, electrons last.</li>
<li>If a pad <em>does</em> lift: magnet wire to a trace, UV mask to seal. Ugly heroics, but it works.</li>
</ul>
<p>Besides the soldering part, I also used m2 screws to fix the diffusers. Initially magnets were suggested, but I felt like figuring that out might take me long, and screws just feel more maintainable&hellip; So&hellip; yea.</p>
<hr>
<h2 id="10-why-these-design-choices">10) Why these design choices?</h2>
<ul>
<li><strong>Calm motion, not flashy.</strong> Meeting rooms breed fatigue; the light should be a helper, not a distraction.</li>
<li><strong>Cyan→Purple language.</strong> Easy to learn, visible at a glance, meaningful without numbers.</li>
<li><strong>White charts, dark UI.</strong> Data should be legible on a projector; buttons shouldnt shout.</li>
<li><strong>Safe Mode default.</strong> People first. Demos are optin.</li>
<li><strong>PLA body, PLA diffuser.</strong> Fast and easy rapid prototyping, easy and fast printing.</li>
</ul>
<hr>
<h2 id="11-what-i-learned">11) What I learned</h2>
<ul>
<li>A logo is a better messenger than a dashboard.</li>
<li>Everyone knows what <strong>orange</strong> means without a legend.</li>
<li>If you show the room a mirror, it corrects itself. Someone will open the door long before you have to ask, and if someone notices the orange/red color of the logo, they would hint the end of the gathering!</li>
<li>Sometimes debugging is not fun at all, and you want to bang your head to the wall, but try not to! Life is short. ^^</li>
</ul>
<hr>
<h2 id="12-final-notes">12) Final notes</h2>
<p>I did this project as a fun distraction and &ldquo;not work&rdquo; as my advisor tells me to do all the time. My advisor provided the LED strip, RPi zero 2 W, and the voltage divider. I got the sensor, designed and coded everything else, and had a lot of fun doing so. I&rsquo;m so so thankful of my advisor for this cool idea, and all the support. It&rsquo;s not the first time I&rsquo;ve been gifted such toys, and as I have recieved other things after that, I know it&rsquo;s not the last.</p>
<p>There is a bug I never was able to figure out, sometimes when I stop the script, I get segmentation fault. I know it is not directly my code with extensive testing, and that the problem is with the library, but I never understood why it happens. Let&rsquo;s hope that doesn&rsquo;t turn into a backdoor into my logo. * Shakingly crosses fingers and sighs in distress!*</p>
<p>The whole project took around 8 days, 4 of which were part of a long weekend. I did do some &ldquo;not work&rdquo; as Tobias always tells me to do, and honestly it was fun and refreshing! I really enjoyed it. I don&rsquo;t know how the group feels/thinks about the logo, but I love it! I hope it won&rsquo;t die after I leave, but even if so, so be it. I had my fun with it. :)</p>
]]></content:encoded></item><item><title>Movie review: Scent of a Woman</title><link>http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/posts/scent_of_a_woman/</link><pubDate>Mon, 13 Oct 2025 08:52:10 +0000</pubDate><guid>http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/posts/scent_of_a_woman/</guid><description>My reaction to the movie as a not so pro movie watcher</description><content:encoded><![CDATA[<p>I&rsquo;m not a big movie watcher.
In fact, I don&rsquo;t remember the last time I watched a whole movie in a single day.
It&rsquo;s not that I don&rsquo;t enjoy it, it&rsquo;s that I want to do this with at least another person.
It feels much better to not be alone when watching a movie for me for some reason. Buuuut, I watched <a href="https://www.imdb.com/title/tt0105323">Scent of a Woman</a> on Sunday, and after my therapist encouraged me to do so.</p>
<p>Few points before I begin.</p>
<ul>
<li>This was aside from the therapy session.</li>
<li>My therapist does not tell me what to do, or if I&rsquo;m wrong or right or anything like that.</li>
<li>I watched this movie in about two parts in a single day!</li>
<li>I knew Al Pacino&rsquo;s face from Godfather series, but didn&rsquo;t know his name.</li>
<li>I&rsquo;m not a big movie person, so whatever I say should be taken with a grain of salt. Just don&rsquo;t be offended, take it face up and as is.</li>
</ul>
<p>The movie is about a good young student named Chris O&rsquo;Donnell, played by Charlie Simms ,who is dependent on a scholarship to continue his studies after school, worthy of going to Harvard.</p>
<p>The second main character of the movie is a blind man called Lt. Col. Frank Slade, played by Al Pacino, who is an arrogant, rude, assertive, proud, self-centered person that gives bad vibes throughout the movie.</p>
<p>Chris needs to work over the thanksgiving weekend, just to be able to make it home during Christmas.
He lives with his mother, and a not so nice step-father who isn&rsquo;t really there for him.</p>
<p>When taking the job to watch over Frank, he quickly realizes what a hard task this will be, but the lady who hires him (Karen Rossie) being his niece assures him that he is a pile of suger under these bad vibes, and that she needs him to do this for her for these fea days.
At last Chris agrees.</p>
<p>Upon Karen&rsquo;s leaving, Frank gets ready for a trip to New York.
To do his last vows before taking his own life without Chris knowing this.</p>
<p>He plans staying at a nice hotel, having an awesome Meal at a gorgeous restaurant, seeing a woman, his brother, and then taking his own life.</p>
<p>Frank is in love with Jack Daniel&rsquo;s and cannot get enough of it.
Throughout the movie this drunk, addiction-like behaviour is portrayed.</p>
<p>Frank also learns about a problem Chris is facing.
Chris is being forced to snitch on a group of the people he knows, or he will be expelled and loses his chance to got o Harvard with another scholarship.
He doesn&rsquo;t want to snitch, but frank learns about this, he encourages him to take the deal.</p>
<p>Frank then tries to teach Chris how to flirt, how to get woman, and how to be a gentleman.
There is an amazing scene in the movie where Frank tries to setup Chris with a girl who is waiting for her boyfriend!
He dances Tango with her and this scene is probably the most majestic scene in any movie I have ever seen.
The feelings I got were so strong and positive that I just cannot express!</p>
<p>In the end, when Frank wants to take his own life, Chris stops him and talks him out of it.
He helps him to ride a Ferrari, the second thing he liked in his life (the first thing was woman lmao!).
A police officer stopped them because they were going too fast, and Frank spoke his way out of the ticket!
The officer didn&rsquo;t even recognize Frank was blind!!!!</p>
<p>After these emotional roller coaster, Chris became ever-so attached to Frank, and so did Frank become attached to Chris, showing up to an open student hearing in place of Chris&rsquo;s parent.</p>
<p>Chris ended up not snitching, and Frank spoke up for him, saying how big of a man is he!
How valuable he is, and how disgusting the acts of the other kid who also could snitch but hid behind his father&rsquo;s back was.</p>
<p>In essense, the movie was amazing, with great acting all over it.
The characters portreayed their roles as amazingly as possible.
I loved the movie and how we got to know the characters more and more as the movie went on, and how the story progressed.</p>
<p>The story was also amazing.
Characters really added life to the scenes by their amazing play.
Donna did the tango dance professionaly.
Al Pacino played to role of a blind man very well, you would easily be fooled by his amazing acting.</p>
<p>That leaves me thinking about why did my therapist suggested me to watch this movie and asked me to tell him how I felt and how I thought about the movie?</p>
<p>Was it about Frank?
That I was trying to be Chris, protecting and caring for Frank, although his behaviour is horrible?
But Frank gave so much back to Chris.
I guess it is the same for me too, I&rsquo;m also getting a lot back while making some sacrifices.</p>
<p>Or maybe was it about that amazing Tango dancing scene?
The sensations that Frank describes about relationships, and how he interacts with woman?</p>
<p>I honestly do not know.
I just know that watching the movie gave me good feelings.
That I&rsquo;m a happier Iman than the Iman I was before watching it.
That my feelings and experiences are not abnormal, they are valid.
That I&rsquo;m on the right track, but still need time to figure out the way.
That I shouldn&rsquo;t worry too much.
That I should look at everything as a way to have &ldquo;fun&rdquo;.
That I shouldn&rsquo;t overthink things.
That if I mess up, oh well, I should take the responsibility and move on.</p>
<p>I don&rsquo;t know what the intention of my therapist was, but I&rsquo;m so curious!
I want to figure it out. :)
Fell free to cross your fingers for me!</p>
]]></content:encoded></item><item><title>Hobbies</title><link>http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/posts/hobbies/</link><pubDate>Fri, 10 Oct 2025 07:04:54 +0000</pubDate><guid>http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/posts/hobbies/</guid><description>My point of view on hobbies</description><content:encoded><![CDATA[<p>When I started my PhD, I was told &ldquo;get yourself a hobby!&rdquo; many many times by both my advisor, and the director of the group I worked in.
In fact, when being interviewed for the position, I was asked about my hobbies.</p>
<p>For some reason I think I have like the weirdest hobbies of all time.
Like, you know, people binge series, go to clubs, bars, hang out, and so on.
But I always had interests in things that when I talk about people get weirded out, or at least some of them do so.
To be honest though, when I talk more about some of my hobbies, they do show enthusiasm, but it feels like there is a clear gap between us afterwards.</p>
<p>Let&rsquo;s go back in time.
What hobbies have I had over the course of my life?
Actually as a kid I used to play games on Mobile for some reason.
I was also quite competative, reaching the highest possible ranks in pretty much every game I played by solo-queueing.
I also enjoyed playing with my brother, but for some reason he always liked games that were not necesserially my most favorite.
Like I liked Vain Glory, and my brother liked Fifa.
We did play both in the end.</p>
<p>After my brother went to the capital to attend univesity, we bacame more distant.
I had to also focus on my own studies, and was actually struggling in junior-high.
Now that is a story in itself for another time.
I used to play games back then as hobby.
But my real interest was chess.
I was really good at calculating.
I was extremely sharp for my age, but since I was obbese my parents wanted me to take &ldquo;active&rdquo; sport classes.
So&hellip; getting into a chess class was conditional.</p>
<p>I have&rsquo;t really done any chess studying after those early years, and I just know a few openings with little depth.
I do have a relatively good understanding of piece synergy and also ok-ish calculation skills to beat a good portion of people I see on a daily basis, but any professional player would easily beat me out of the opening.</p>
<p>In high-school I didn&rsquo;t really have any hobbies.
I did become interested in Basketball, but nothing serious.
It&rsquo;s funny how I was able to become good at any sport I tried.
I tried Football, Basketball, Volleyball, Table Tennis, Tennis, Chess, Footsal, Swimming, and Badminton.
I think swimming was, and still is my best sport.
I never pursued it though, which is a shame.</p>
<p>Before starting my Bachelors, I became really interested in Cicada3301, and cryptography.
Well, not the fancy state-of-the-art cryptography, the simple classic methods.
The notion of delivering a message in plain sight that only the two parties envolved can understand it was so cool to me!
That reminds me, we had these special things in junior-high called &ldquo;Karsoogh&rdquo; for math, physics, chemistry, and biology.
I think I quilified for pretty much every one of them, and had a blast every time!
In the chemistry one we cooked honey(!), in the biology one they dissected a bunny wabbit (with w to pay respect!), in the physics one we did cool experiments and tried to solve fancy problems.
But the math one was different, it happend between a few close-by NODET cities, and annualy.
The idea was simple.
They gave us questions that did were solveable by developing ideas, with almost always little to no pre-requisite.
Only in one of the years this event was internal, and they taught us 13-15 year young folks basics of cryptography, and ciphers.
We then competed against each other.
The competition was simple, develope a cipher with your team in a n*n (I think n was 6 or 8), then you split up into two groups, one team encrypts a message and you recieve the delivered message, the other team has to decrypt it.
The fastest team gets the most points.
The the message is common between teams.
After this phase the teams got together and had to figure out other teams encryption algorithm, if successful, they got points.
First teams to get an algorithm gets more points.
Our team won the best algorithm in the end, and my ideas were most influential in this achievement!
My teammates became really good friends with me after this competition which felt really cool!
I think this was the reason I became interested in cryptography.
I don&rsquo;t remember the first year, but the first and last year in the final phase and after qualifying they tought us basics of game theory, and we had a set of two player fair and unfair games for which we had to compete against other players.
If your team solved a game, you got more points.
It was really fun, and since the games happened in parallel, all teammtes had to be active.</p>
<p>I said all of this just to say thinking on problems became a hobby for me out of all of these cool expereinces.
I like to casually get lost in questions and think about weird equations, or natural problems.
Something like Feynman&rsquo;s obsession with the spaghetti problem!
Initially when starting my PhD, I did this, but then I diverged to other things I will talk about later on.
But I do want to get back to this fun hobby.
I just need some questions, and an empty mind.
Or I guess some time slots to chill and not stress about my other research.</p>
<p>During my bachelors, I became fascinated by hardware.
Like sensors, actuators, micro-controllers, and anything that I could program to do something I do but do not want to do manually.
Later on I realized this is related to IoT and Embedded systems.
I did take the embedded systems course, and for the project that the cap was 120 points, we got a wopping 125 out of 100!
Not only we implemented the whole project parts that we had to implement, but also we went beyond and just surprised the professor and his TAs!
Unfortunately I don&rsquo;t have any images from the project, but the code can be found <a href="https://github.com/AlipourIm/Embedded_Systems_Project_Spring-1402">here</a>.</p>
<p>I bought myself a raspberrypi, many sensors and actuators, and did small fun projects!
I deployed a VPN on my home network, made my Rpi accessible with dynamic IP over the Internet via DDNS, deployed my very own nextcloud, website, and so on.
I smartified my room AC s.t. it would keep the temprature at a certain range in a way to avoid hysteresis via a simple temprature and humidity sensor, and an IR-transceiver by recording the controllers signal.
I could not figure out how to fix the state when commands don&rsquo;t go through correctly though, a challenge that I never solved or came up with a solution for.
I would say my love with computers, making things smart, and networking became my main hobby!</p>
<p>After some protests and Internet blockages, I became interested in setting up VPNs that could penetrate through censorship attempts.
It was, and still is a rat race.
Well, it&rsquo;s the story of my PhD pretty much now.</p>
<p>Reading books became a hobby for a while, but Youtube kinda distroyed that.
I really love to get back to reading more books, I have a really exciting list of books to be read in my library.
Listening to Music is another amazing hobby I have, specifically when I walk around or do chores.
I find doing chores so relaxing!
Since we talked about music I should mention I also tried to learn Piano, but didn&rsquo;t pursue it.
I actaully wanted to learn Violine, but the consultant we talked to said &ldquo;if you&rsquo;re not a hard worker it won&rsquo;t workout for you&rdquo;, and since my brother was going to learn Piano, I followed suit.
And oh well, our teacher although he though I was doing really good for my age, did not give me the same set of practices that my brother got, and naturally since I was learning from kids books, I felt sad since his music always sounded better, and I eventually gave up feeling sad.
This is a pattern that has happened in my life a lot, something I need to stop from happening.
Comparing myself with other, and competing with them.
It&rsquo;s just distroying me mentally.
I am me, and the best me ever to exist.
And that&rsquo;s how it should be.</p>
<p>Somewhere during my bachelors I also became fascinated by coffee!
I watched many James Hoffman videos and learned how to use different berewing methods for coffee, and did lots of experiments with it.
Then came matcha, though with matcha things are much more limited.</p>
<p>Another thing that comes to mind is boardgames.
I love boardgames that you need to think and be smart!
An example is Cluedo.
People usually don&rsquo;t like to play it with me because I pay attention to &ldquo;everything&rdquo;.
But I also anjoy playing other simpler games like UNO, Risk, Catan, card games, Coup, and many many other fun party games.
I have a whole collection of boardgames that I don&rsquo;t get to play! :-P
One of my all time favorite games is &ldquo;Zaar&rdquo; (a persian game that was discontinued), and a game kinda similar to it called &ldquo;The Night Cage&rdquo;.
I like them because there is a bit of strategy, luck, and a lot of co-op in them.
In the later you either all win together, or get doomed.
In the first one there is a comperition aspect to the game which makes it cool.</p>
<p>Cooking is another hobby of mine, although I only enjoy it when done with someone, or in a group. Aaaaaand guess what, I&rsquo;m a loner! (Drat. :P)
I love to &ldquo;not follow recipes&rdquo; and try new things.
Foods I make usually turn out to be quite yummy actually, though definitly not authentic.
I also think I do a good job with the presentation part when I try.
And I&rsquo;m open to cooking anything and everything!</p>
<p>I was also interested in Hiking, but never really got to know someone who is both interested and willing to go with me, and when I&rsquo;m alone, I rather do my other hobbies.</p>
<p>And that leaves me with my latest two hobbies. CTFs, and 3D printing! Oh ,and I guess maybe blogging and sharing photos online? Idk! xD</p>
<p>3D printing is an interesting one.
I was fascinated about it from before, but never got my hands on a 3D printer until like 7 months ago.
When I went to 38c3 last year, I saw so many printers, and how cool they were.
And somehting in my heart was touched, that I need one!
The thing is, I tend to like thinks that limit me to my creativity, like the IoT stuff, or how I always loved to play Minecraft as a kid.
And oh well, 3D printing is just the hobby!
I also tried to do some 3D design, but I&rsquo;m quite a noob at it still.
I will probably share some of the things I&rsquo;ve made somewhere somehow, but not for now at least.
Well, one of my cool projects inspired and mostly funded by my advisor was the INET logo (post comming soon!).
It&rsquo;s so cute and fascinating, and I had an absolute blast working on it for the one week I did.
So much designing, fixing measurements, printing, coding, soldering, wiring, debugging, etc.!
I also 3D printed and painted many gifts and organizers and other figues for either myself, or my friends.
It&rsquo;s just a fun thing to have, and to play around with.
The fixing part of it, and maintaining it is not as fun, but it&rsquo;s part of the journey.
I will probably write about me and my 3D printer a lot more in the future.
Another cool thing I can do with it, and have been doing so, is to do prints for the PhD hat of the people who will be graduating, a German cute and cool tradition!</p>
<p>And now let&rsquo;s talk about the CTF stuff.
This is somewhat related to my interest in computers, problem solving, and cryptography (kinda).
I&rsquo;ve been wanting to do CTFs for a long time and throughout my Bachelors, but never did so.
After starting my PhD, I was introduced to Saarsec, and now I&rsquo;m a proud member, trying to contribute as much as my time allows me to.
I&rsquo;m not good at CTFs, but the joy of getting stuck on a problem, and maybe finally solving it is just too good to pass.
I love it!
I want to do more of it.
The only sad part is that it takes a long time, and well, I need to spend time on the social aspects of my life too.
Too shay.
I would love to share some of my write-ups here too, and also write about it in the future, but there is only so much time I can spend on the blog.</p>
<p>And last but not least, blogging.
Well, I kinda started it for no reason to be honest.
I just want to share my stories, and to show my vulnerable side with no guilt.
It feels freeing to do this, and I hope I continue!
I hope people won&rsquo;t get mad if they are a part of these stories I share.
I try to not name any names if not required, but I do think putting my PoV helps me with reducing some anxiety and social pressure.
I really enjoy it!</p>
<p>With all this being said, I think that&rsquo;s it.
If I remember other hobbies that I missed, I will add them to the end of the article or maybe write a new post about it, Idk.
The only thing I want to emphasize is that I&rsquo;m into things that make me limited to my creativity.
Oh, and also books, if only I read them instead of watching Youtube!!!!</p>
]]></content:encoded></item><item><title>Relationships</title><link>http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/posts/relationships/</link><pubDate>Sun, 05 Oct 2025 08:03:31 +0000</pubDate><guid>http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/posts/relationships/</guid><description>My experience so far with relationships</description><content:encoded><![CDATA[<blockquote>
<p>Notice: You&rsquo;ll hear me ranting in this post, so buckle up, or just go to another fun post. I am very vulnerable right now and want to put out my story without naming anyone.</p></blockquote>
<p>So&hellip; do you also think you&rsquo;re a lover boy, kind, nice person?
So do I!
I think I&rsquo;m actually really good at making friends with people.
Like you know, you start introducing yourself, asking them some fun personal questions about hobbies or other stuff, and then you eventually start talking about other things.</p>
<p>My problem is when things are to become more serious!
Like, you know&hellip; connecting at a more emotional level.
A &ldquo;relationship&rdquo;.
I&rsquo;m really bad at those.
In fact, I haven&rsquo;t ever been in one!
Now let&rsquo;s be honest, I&rsquo;m not your typical jacked, handsome boy, but I think I&rsquo;m somewhere around the average if not higher a bit?
Well, at least I hope so! :D</p>
<p>I actually haven&rsquo;t even dated once so far.
My closest dating experience was something that turned out not to be a date, although most people I spoke to, were like&hellip; yea dude&hellip; it was a date buddy.
Why do I say it wasn&rsquo;t a date?
Well, because I was told so yesterday by her.
That if it&rsquo;s a date, you&rsquo;re supposed to say it beforehand.
I find that very fair actually.
I really do.
I wasn&rsquo;t even looking for anything more than a friendship from whatever these two weeks of interactions were, but things seemed abnormal.
Like we planned a future together.
We planned planting flowers, hosting Kareoki. We planned cooking/baking together for fun. We talked about me taaching her cycling since she said she was interested in it. And many many other things!<br>
We even found our &ldquo;favorite shops&rdquo; in the city together!
It was weird! I&rsquo;m so confused.
I remember the second time we went out together, we sat next to the river and close to eachother.
She leaned into me at some point.
She had also touched my arm gently when making a joke earlier when we went shopping together.
It felt weird.
I did feel she was flirting with me, making plans with me.
She sought my opinion on many things, the house she was considering moving into, color and design of the cushions for her house, and many other things.
We also had many deep conversations in this short amount of time.</p>
<p>But then it happened.
Suddenly she messaged me saying how she is going to have a bf soon &ldquo;hopefully&rdquo;.
That others know him, but not me.
That where we live there are not that many people.
That supply is low, and demand is high.
That this person asked her fast so that &ldquo;the slots won&rsquo;t be filled&rdquo;.
It was weird, as if she was telling me I&rsquo;m going to be taken if you don&rsquo;t do anything&hellip;?
After this weird encounter I talked to two of my friends, and my married brother and his wife.
When she heard about our interactions and all of the messages we passed, she did feel like I&rsquo;ve been friendzoning her.
That I&rsquo;m not resiprocating her feelings.
That she perhaps has a crush on me.
Even my brother felt the same!
My friend was a bit more unsure though, but he also felt like it could really be that she is playing games.
With all honestly, in her messages when she was teaching me the &ldquo;ways&rdquo;, she said &ldquo;I don&rsquo;t play games and I also advise you not to get together with people that do so&rdquo;.
Last time we went out together, one-on-one, many of the people we knew saw us together and some of them gave us looks that gave me more confidence that what I&rsquo;m doing is normal and things are going well.
But I guess I was wrong?</p>
<p>I do want to talk about some things that we exchanged on this weird conversation that made me confused.
She initially asked why I didn&rsquo;t go out with them last night.
I had an exam on 7th, but we had planned two one-on-one going outs for 4th and 5th, hence the reason I skipped going out with a group of people rather to have less on my plate for those days we were going to hang out together.
With all honestly I didn&rsquo;t even know she was joining them or not.
It&rsquo;s kinda weird, but the plans were made in a group she was not inside of.
Anyway.</p>
<p>October 4th was a rainy day.
She said she might be catching a cold, and that going out in this weather is maybe not a good choice.
She said let&rsquo;s go next weekend as the weather is better, and also that we can go out after work hours in weekdays.
You see? She proposed a different time and date, she didn&rsquo;t just cancel last second.
And then she started talking about last night, the things that happened.
A guy apparently knew him, called her and then hugged her, and she insisted that she had only seen him once in Mensa last year and that she doesn&rsquo;t know him.
Then she started talking about how there is so much gossip behind her back, and people who know those gossips are wrong do not speak up for her.
And then she talked about how hard it is to be a girl, and how boys do not understand difference between flirting and being polite.
Then she talked about how when two people see each other, a click can happen, and that&rsquo;s how you can know if you like someone or not. If it&rsquo;s two way ofc.
I asked her what is this &ldquo;click&rdquo;?
She said &ldquo;you know when you know, and if you think you don&rsquo;t know then there isn&rsquo;t one&rdquo;.
Interesting.
My poor brain started looking back at our own interactions.
I remembered how she smiled at me, leaned toward me, touched my arm, planned a future with me in it&hellip;
And again I felt she is hinting something.
Poor me was stuck between a rock and a hard place.
She told me that if I like someone I should ask them on a date &ldquo;early&rdquo;, as time is of essence.
Again, I was confused.
I was lost.
We&rsquo;ve been hanging out for two weeks, we have our favorite similar shops, hobbies.
We have plans together.
We understood eachother.
Or maybe that&rsquo;s just how I felt in my mind?
Maybe things in my brain are just different?
I don&rsquo;t know.</p>
<p>My sister in law told me that she probably had a crush on me and that I didn&rsquo;t reciprocate her feelings perhaps?
That I&rsquo;m friendzoning her by not touching her back or asking her out on a &ldquo;date&rdquo;.
Her reasoning is that she initiated all of this.
She asked me to go to see the house she was considering renting, and then asked me to go out with her after that.
She told me that I should invite her to a date.
That I have to do something romantic now, maybe get flowers, and a gift.
And I did just that.
I messaged her talking about all of the good traits I had seen from her, her behaviour, and habits.
And I asked her on a date.</p>
<p>She said she enjoyed the company too, and thanked me.
She then said she is starting a new relationship, but even if that was not the case, &ldquo;we were so different from eachother at a much deeper level&rdquo;.
That her &ldquo;life experiecnes&rdquo; are just different, &ldquo;and so on&rdquo;.
She did actually say &ldquo;and so on&rdquo;.</p>
<p>You know what it reminds me of?
Of when your paper gets rejected by saying &ldquo;lacks novelty&rdquo;. xD</p>
<p>Another extremely funny thing is that she said we&rsquo;re so different at a much deeper level, but she doesn&rsquo;t even know me.
What was meant at a deeper level?
I&rsquo;m confused again.
She definitly does not know my hobbies, most of my interests, my stories, my family, friends, nothing.
She knew nothing about me.
How are we different so deeply if you don&rsquo;t even know me? I&rsquo;m so so incredibly confused.
I guess it could be the looks, and the &ldquo;vibes&rdquo;?
But again, I do think she did not have a more polite better reason but did want to provide some sort of an explanation so that I won&rsquo;t pursue her I guess?
Idk.</p>
<p>I want to also come back to this point she made that she is &ldquo;starting a new relationship&rdquo;.
She told me in the same conversation that someone asked her out (which she technically didn&rsquo;t even say this, but it could be induced), and that going on a date &ldquo;does not mean you&rsquo;re in a relationship, that you want to know eachother&rdquo;.
Her saying that &ldquo;I said I&rsquo;m starting a new relationship&rdquo; hurt me, not because she is doing that, because she didn&rsquo;t technically tell me that.
She then said &ldquo;I always tell people this to avoid confusion&rdquo;.
I definitly didn&rsquo;t miss it if she did. She didn&rsquo;t, but whatever. It&rsquo;s fine. I did apologize.
The reason I say this is that some other guys we knew came up to her and she was encouraging them to go see other girls, but not me&hellip;
Did I miss it?
She didn&rsquo;t. Just trust me on this one. ;)</p>
<p>I&rsquo;m just confused.
The last three times I had a crush on someone I confessed too.
First case was in a relationship which became awkward as she told her bf and he bullied me (drat!).
Second case ended up in a tragedy that I do not want to talk about, lol!
All I can say is that she definitly panicked and I don&rsquo;t blame her.
I do think in her case she had avoidant attachment and my anxious attachment style made her uncomfortable.
The third one said that she is in an &ldquo;undefined state&rdquo; of a relationship, and that she wants to keep it &ldquo;friendly&rdquo; in university, which is completely fair.
But this last one.
This was the weird one.
The first and third one were really sweet to me when I confessed/asked them out.
The last one didn&rsquo;t even show interest about being friends, which is again totally fine.
But I&rsquo;m just so baffeled by all of this.</p>
<p>So&hellip; yea. This last &ldquo;thing&rdquo;, whatever it was, was my closest encounter with a relationship, and maybe it will be for a while.
I do think I need to take a break from trying to get into a relationship.
I&rsquo;ve been hurt a lot by the second one, and this was just confusing.
In her defence she was an amazing person.
Smart, cute, kind, positive, jolly, witty, social, and her taste was just amazing. She also had deep knowledge in many things I was also interested in. Oh well. such is life!
I can&rsquo;t just say the good stuff though. She was definitly a bit self-centered. It&rsquo;s funny how she told me that &ldquo;people say I&rsquo;m so proud in a negative way, but anyone who talks with me knows I&rsquo;m not like that&rdquo;. Which is true, she wasn&rsquo;t proud, the correct term is self-centered, or &ldquo;narcissistic&rdquo; if you will, which I don&rsquo;t neceserrialy think is bad, but it is definitly an orange/yellow flag.</p>
<p>One thing I know is that I do wish her and whoever she dates the best! &lt;3
And that I would be ok to stay friends with her, but since I&rsquo;m hurt, I will be much colder.
Sorry. :)
And another thing I know is that I will never think about her, or people like her romantically, or at least I try not to.
Fast progression ends like this?
But then maybe we never progressed?
I don&rsquo;t know.</p>
<p>Well. I don&rsquo;t know what&rsquo;s gonna happen next, but I&rsquo;m going to just live my life and have fun.
Aaaaaaaand maybe not worry about being single?
Being single is fun too! ^^ You have so much freedom. I love to also work on myself a bit, lose some weight, do the sports I love, and make new friends. Maybe someday this hopeless romantic little lover boy won&rsquo;t be alone? Who knows? haha.</p>
<blockquote>
<p>P.S.: My therapist thought this relationship was abusive in essense, and the thing that happened in the end was purely seductive. I was chasing bread crumbs to be taken advantage of. I&rsquo;m worth more than that. :) In my therapy session I remembered that in each of these so called &ldquo;not dates&rdquo; we had a conversation about &ldquo;her&rdquo;, and she was dumping her emotional baggage on me, just like what happened this last time that confused me.</p></blockquote>
]]></content:encoded></item><item><title>Dockerizing my Minecraft Server + Geyser: from 'no space left' to stable releases</title><link>http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/posts/minecraft_server/</link><pubDate>Mon, 29 Sep 2025 09:06:29 +0000</pubDate><guid>http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/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>Ive 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>Heres 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 dont 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>
<div class="goat svg-container ">
<svg
xmlns="http://www.w3.org/2000/svg"
font-family="Menlo,Lucida Console,monospace"
viewBox="0 0 448 25"
>
<g transform='translate(8,16)'>
<path d='M 404,8 L 404,24' fill='none' stroke='currentColor'></path>
<text text-anchor='middle' x='0' y='4' fill='currentColor' style='font-size:1em'>/</text>
<text text-anchor='middle' x='8' y='4' fill='currentColor' style='font-size:1em'>d</text>
<text text-anchor='middle' x='16' y='4' fill='currentColor' style='font-size:1em'>e</text>
<text text-anchor='middle' x='24' y='4' fill='currentColor' style='font-size:1em'>v</text>
<text text-anchor='middle' x='32' y='4' fill='currentColor' style='font-size:1em'>/</text>
<text text-anchor='middle' x='40' y='4' fill='currentColor' style='font-size:1em'>m</text>
<text text-anchor='middle' x='48' y='4' fill='currentColor' style='font-size:1em'>a</text>
<text text-anchor='middle' x='56' y='4' fill='currentColor' style='font-size:1em'>p</text>
<text text-anchor='middle' x='64' y='4' fill='currentColor' style='font-size:1em'>p</text>
<text text-anchor='middle' x='72' y='4' fill='currentColor' style='font-size:1em'>e</text>
<text text-anchor='middle' x='80' y='4' fill='currentColor' style='font-size:1em'>r</text>
<text text-anchor='middle' x='88' y='4' fill='currentColor' style='font-size:1em'>/</text>
<text text-anchor='middle' x='96' y='4' fill='currentColor' style='font-size:1em'>u</text>
<text text-anchor='middle' x='104' y='4' fill='currentColor' style='font-size:1em'>b</text>
<text text-anchor='middle' x='112' y='4' fill='currentColor' style='font-size:1em'>u</text>
<text text-anchor='middle' x='120' y='4' fill='currentColor' style='font-size:1em'>n</text>
<text text-anchor='middle' x='128' y='4' fill='currentColor' style='font-size:1em'>t</text>
<text text-anchor='middle' x='136' y='4' fill='currentColor' style='font-size:1em'>u</text>
<text text-anchor='middle' x='144' y='4' fill='currentColor' style='font-size:1em'>-</text>
<text text-anchor='middle' x='152' y='4' fill='currentColor' style='font-size:1em'>-</text>
<text text-anchor='middle' x='160' y='4' fill='currentColor' style='font-size:1em'>v</text>
<text text-anchor='middle' x='168' y='4' fill='currentColor' style='font-size:1em'>g</text>
<text text-anchor='middle' x='176' y='4' fill='currentColor' style='font-size:1em'>-</text>
<text text-anchor='middle' x='184' y='4' fill='currentColor' style='font-size:1em'>v</text>
<text text-anchor='middle' x='192' y='4' fill='currentColor' style='font-size:1em'>a</text>
<text text-anchor='middle' x='200' y='4' fill='currentColor' style='font-size:1em'>r</text>
<text text-anchor='middle' x='232' y='4' fill='currentColor' style='font-size:1em'>3</text>
<text text-anchor='middle' x='240' y='4' fill='currentColor' style='font-size:1em'>.</text>
<text text-anchor='middle' x='248' y='4' fill='currentColor' style='font-size:1em'>9</text>
<text text-anchor='middle' x='256' y='4' fill='currentColor' style='font-size:1em'>G</text>
<text text-anchor='middle' x='280' y='4' fill='currentColor' style='font-size:1em'>3</text>
<text text-anchor='middle' x='288' y='4' fill='currentColor' style='font-size:1em'>.</text>
<text text-anchor='middle' x='296' y='4' fill='currentColor' style='font-size:1em'>4</text>
<text text-anchor='middle' x='304' y='4' fill='currentColor' style='font-size:1em'>G</text>
<text text-anchor='middle' x='328' y='4' fill='currentColor' style='font-size:1em'>3</text>
<text text-anchor='middle' x='336' y='4' fill='currentColor' style='font-size:1em'>3</text>
<text text-anchor='middle' x='344' y='4' fill='currentColor' style='font-size:1em'>3</text>
<text text-anchor='middle' x='352' y='4' fill='currentColor' style='font-size:1em'>M</text>
<text text-anchor='middle' x='376' y='4' fill='currentColor' style='font-size:1em'>9</text>
<text text-anchor='middle' x='384' y='4' fill='currentColor' style='font-size:1em'>2</text>
<text text-anchor='middle' x='392' y='4' fill='currentColor' style='font-size:1em'>%</text>
<text text-anchor='middle' x='416' y='4' fill='currentColor' style='font-size:1em'>v</text>
<text text-anchor='middle' x='424' y='4' fill='currentColor' style='font-size:1em'>a</text>
<text text-anchor='middle' x='432' y='4' fill='currentColor' style='font-size:1em'>r</text>
</g>
</svg>
</div>
<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 thats not enough, you have two good options:</p>
<h3 id="option-a--move-dockers-data-off-var">Option A — Move Dockers 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>
<div class="goat svg-container ">
<svg
xmlns="http://www.w3.org/2000/svg"
font-family="Menlo,Lucida Console,monospace"
viewBox="0 0 440 25"
>
<g transform='translate(8,16)'>
<text text-anchor='middle' x='0' y='4' fill='currentColor' style='font-size:1em'>S</text>
<text text-anchor='middle' x='8' y='4' fill='currentColor' style='font-size:1em'>t</text>
<text text-anchor='middle' x='16' y='4' fill='currentColor' style='font-size:1em'>a</text>
<text text-anchor='middle' x='24' y='4' fill='currentColor' style='font-size:1em'>r</text>
<text text-anchor='middle' x='32' y='4' fill='currentColor' style='font-size:1em'>t</text>
<text text-anchor='middle' x='40' y='4' fill='currentColor' style='font-size:1em'>i</text>
<text text-anchor='middle' x='48' y='4' fill='currentColor' style='font-size:1em'>n</text>
<text text-anchor='middle' x='56' y='4' fill='currentColor' style='font-size:1em'>g</text>
<text text-anchor='middle' x='72' y='4' fill='currentColor' style='font-size:1em'>m</text>
<text text-anchor='middle' x='80' y='4' fill='currentColor' style='font-size:1em'>i</text>
<text text-anchor='middle' x='88' y='4' fill='currentColor' style='font-size:1em'>n</text>
<text text-anchor='middle' x='96' y='4' fill='currentColor' style='font-size:1em'>e</text>
<text text-anchor='middle' x='104' y='4' fill='currentColor' style='font-size:1em'>c</text>
<text text-anchor='middle' x='112' y='4' fill='currentColor' style='font-size:1em'>r</text>
<text text-anchor='middle' x='120' y='4' fill='currentColor' style='font-size:1em'>a</text>
<text text-anchor='middle' x='128' y='4' fill='currentColor' style='font-size:1em'>f</text>
<text text-anchor='middle' x='136' y='4' fill='currentColor' style='font-size:1em'>t</text>
<text text-anchor='middle' x='152' y='4' fill='currentColor' style='font-size:1em'>s</text>
<text text-anchor='middle' x='160' y='4' fill='currentColor' style='font-size:1em'>e</text>
<text text-anchor='middle' x='168' y='4' fill='currentColor' style='font-size:1em'>r</text>
<text text-anchor='middle' x='176' y='4' fill='currentColor' style='font-size:1em'>v</text>
<text text-anchor='middle' x='184' y='4' fill='currentColor' style='font-size:1em'>e</text>
<text text-anchor='middle' x='192' y='4' fill='currentColor' style='font-size:1em'>r</text>
<text text-anchor='middle' x='208' y='4' fill='currentColor' style='font-size:1em'>v</text>
<text text-anchor='middle' x='216' y='4' fill='currentColor' style='font-size:1em'>e</text>
<text text-anchor='middle' x='224' y='4' fill='currentColor' style='font-size:1em'>r</text>
<text text-anchor='middle' x='232' y='4' fill='currentColor' style='font-size:1em'>s</text>
<text text-anchor='middle' x='240' y='4' fill='currentColor' style='font-size:1em'>i</text>
<text text-anchor='middle' x='248' y='4' fill='currentColor' style='font-size:1em'>o</text>
<text text-anchor='middle' x='256' y='4' fill='currentColor' style='font-size:1em'>n</text>
<text text-anchor='middle' x='272' y='4' fill='currentColor' style='font-size:1em'>1</text>
<text text-anchor='middle' x='280' y='4' fill='currentColor' style='font-size:1em'>.</text>
<text text-anchor='middle' x='288' y='4' fill='currentColor' style='font-size:1em'>2</text>
<text text-anchor='middle' x='296' y='4' fill='currentColor' style='font-size:1em'>1</text>
<text text-anchor='middle' x='304' y='4' fill='currentColor' style='font-size:1em'>.</text>
<text text-anchor='middle' x='312' y='4' fill='currentColor' style='font-size:1em'>9</text>
<text text-anchor='middle' x='328' y='4' fill='currentColor' style='font-size:1em'>P</text>
<text text-anchor='middle' x='336' y='4' fill='currentColor' style='font-size:1em'>r</text>
<text text-anchor='middle' x='344' y='4' fill='currentColor' style='font-size:1em'>e</text>
<text text-anchor='middle' x='352' y='4' fill='currentColor' style='font-size:1em'>-</text>
<text text-anchor='middle' x='360' y='4' fill='currentColor' style='font-size:1em'>R</text>
<text text-anchor='middle' x='368' y='4' fill='currentColor' style='font-size:1em'>e</text>
<text text-anchor='middle' x='376' y='4' fill='currentColor' style='font-size:1em'>l</text>
<text text-anchor='middle' x='384' y='4' fill='currentColor' style='font-size:1em'>e</text>
<text text-anchor='middle' x='392' y='4' fill='currentColor' style='font-size:1em'>a</text>
<text text-anchor='middle' x='400' y='4' fill='currentColor' style='font-size:1em'>s</text>
<text text-anchor='middle' x='408' y='4' fill='currentColor' style='font-size:1em'>e</text>
<text text-anchor='middle' x='424' y='4' fill='currentColor' style='font-size:1em'>2</text>
</g>
</svg>
</div>
<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 youve validated).</p></blockquote>
<hr>
<h2 id="no-whitelist">No whitelist</h2>
<p>Because I dont 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 Dockers 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>
]]></content:encoded></item><item><title>Running my blog on Tor (.onion) and the Clearnet with Hugo + PaperMod</title><link>http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/posts/tor_clearnet_blog/</link><pubDate>Fri, 19 Sep 2025 00:00:00 +0000</pubDate><guid>http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/posts/tor_clearnet_blog/</guid><description>How I hosted a Hugo + PaperMod site both as a Tor onion service and a normal HTTPS site behind Cloudflare, with clean configs, dual builds, and a one-command deploy.</description><content:encoded><![CDATA[<blockquote>
<p>TL;DR — The site is built once (Hugo project), <strong>published twice</strong>:</p>
<ul>
<li><strong>Onion</strong>: <code>http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/</code> via Tor, no TLS/HSTS, bound to <code>127.0.0.1:3301</code>.</li>
<li><strong>Clearnet</strong>: <code>https://blog.alipourimjourneys.ir</code> behind Cloudflare, Lets Encrypt cert, <code>Onion-Location</code> header pointing to the onion mirror.</li>
</ul></blockquote>
<hr>
<h2 id="1-tor-hidden-service-onion-basics">1) Tor hidden service (onion) basics</h2>
<p>I used Tors v3 onion services and mapped onion port 80 → my local web server on <code>127.0.0.1:3301</code>.</p>
<p><strong>Install &amp; configure Tor (Debian/Ubuntu):</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 apt update <span style="color:#f92672">&amp;&amp;</span> sudo apt install -y tor
</span></span><span style="display:flex;"><span>sudoedit /etc/tor/torrc
</span></span></code></pre></div><p>Add:</p>
<div class="goat svg-container ">
<svg
xmlns="http://www.w3.org/2000/svg"
font-family="Menlo,Lucida Console,monospace"
viewBox="0 0 344 41"
>
<g transform='translate(8,16)'>
<path d='M 132,8 L 124,40' fill='none' stroke='currentColor'></path>
<path d='M 196,8 L 188,40' fill='none' stroke='currentColor'></path>
<path d='M 228,8 L 220,40' fill='none' stroke='currentColor'></path>
<text text-anchor='middle' x='0' y='4' fill='currentColor' style='font-size:1em'>H</text>
<text text-anchor='middle' x='0' y='20' fill='currentColor' style='font-size:1em'>H</text>
<text text-anchor='middle' x='8' y='4' fill='currentColor' style='font-size:1em'>i</text>
<text text-anchor='middle' x='8' y='20' fill='currentColor' style='font-size:1em'>i</text>
<text text-anchor='middle' x='16' y='4' fill='currentColor' style='font-size:1em'>d</text>
<text text-anchor='middle' x='16' y='20' fill='currentColor' style='font-size:1em'>d</text>
<text text-anchor='middle' x='24' y='4' fill='currentColor' style='font-size:1em'>d</text>
<text text-anchor='middle' x='24' y='20' fill='currentColor' style='font-size:1em'>d</text>
<text text-anchor='middle' x='32' y='4' fill='currentColor' style='font-size:1em'>e</text>
<text text-anchor='middle' x='32' y='20' fill='currentColor' style='font-size:1em'>e</text>
<text text-anchor='middle' x='40' y='4' fill='currentColor' style='font-size:1em'>n</text>
<text text-anchor='middle' x='40' y='20' fill='currentColor' style='font-size:1em'>n</text>
<text text-anchor='middle' x='48' y='4' fill='currentColor' style='font-size:1em'>S</text>
<text text-anchor='middle' x='48' y='20' fill='currentColor' style='font-size:1em'>S</text>
<text text-anchor='middle' x='56' y='4' fill='currentColor' style='font-size:1em'>e</text>
<text text-anchor='middle' x='56' y='20' fill='currentColor' style='font-size:1em'>e</text>
<text text-anchor='middle' x='64' y='4' fill='currentColor' style='font-size:1em'>r</text>
<text text-anchor='middle' x='64' y='20' fill='currentColor' style='font-size:1em'>r</text>
<text text-anchor='middle' x='72' y='4' fill='currentColor' style='font-size:1em'>v</text>
<text text-anchor='middle' x='72' y='20' fill='currentColor' style='font-size:1em'>v</text>
<text text-anchor='middle' x='80' y='4' fill='currentColor' style='font-size:1em'>i</text>
<text text-anchor='middle' x='80' y='20' fill='currentColor' style='font-size:1em'>i</text>
<text text-anchor='middle' x='88' y='4' fill='currentColor' style='font-size:1em'>c</text>
<text text-anchor='middle' x='88' y='20' fill='currentColor' style='font-size:1em'>c</text>
<text text-anchor='middle' x='96' y='4' fill='currentColor' style='font-size:1em'>e</text>
<text text-anchor='middle' x='96' y='20' fill='currentColor' style='font-size:1em'>e</text>
<text text-anchor='middle' x='104' y='4' fill='currentColor' style='font-size:1em'>D</text>
<text text-anchor='middle' x='104' y='20' fill='currentColor' style='font-size:1em'>P</text>
<text text-anchor='middle' x='112' y='4' fill='currentColor' style='font-size:1em'>i</text>
<text text-anchor='middle' x='112' y='20' fill='currentColor' style='font-size:1em'>o</text>
<text text-anchor='middle' x='120' y='4' fill='currentColor' style='font-size:1em'>r</text>
<text text-anchor='middle' x='120' y='20' fill='currentColor' style='font-size:1em'>r</text>
<text text-anchor='middle' x='128' y='20' fill='currentColor' style='font-size:1em'>t</text>
<text text-anchor='middle' x='144' y='4' fill='currentColor' style='font-size:1em'>v</text>
<text text-anchor='middle' x='144' y='20' fill='currentColor' style='font-size:1em'>8</text>
<text text-anchor='middle' x='152' y='4' fill='currentColor' style='font-size:1em'>a</text>
<text text-anchor='middle' x='152' y='20' fill='currentColor' style='font-size:1em'>0</text>
<text text-anchor='middle' x='160' y='4' fill='currentColor' style='font-size:1em'>r</text>
<text text-anchor='middle' x='168' y='4' fill='currentColor' style='font-size:1em'>/</text>
<text text-anchor='middle' x='168' y='20' fill='currentColor' style='font-size:1em'>1</text>
<text text-anchor='middle' x='176' y='4' fill='currentColor' style='font-size:1em'>l</text>
<text text-anchor='middle' x='176' y='20' fill='currentColor' style='font-size:1em'>2</text>
<text text-anchor='middle' x='184' y='4' fill='currentColor' style='font-size:1em'>i</text>
<text text-anchor='middle' x='184' y='20' fill='currentColor' style='font-size:1em'>7</text>
<text text-anchor='middle' x='192' y='4' fill='currentColor' style='font-size:1em'>b</text>
<text text-anchor='middle' x='192' y='20' fill='currentColor' style='font-size:1em'>.</text>
<text text-anchor='middle' x='200' y='20' fill='currentColor' style='font-size:1em'>0</text>
<text text-anchor='middle' x='208' y='4' fill='currentColor' style='font-size:1em'>t</text>
<text text-anchor='middle' x='208' y='20' fill='currentColor' style='font-size:1em'>.</text>
<text text-anchor='middle' x='216' y='4' fill='currentColor' style='font-size:1em'>o</text>
<text text-anchor='middle' x='216' y='20' fill='currentColor' style='font-size:1em'>0</text>
<text text-anchor='middle' x='224' y='4' fill='currentColor' style='font-size:1em'>r</text>
<text text-anchor='middle' x='224' y='20' fill='currentColor' style='font-size:1em'>.</text>
<text text-anchor='middle' x='232' y='20' fill='currentColor' style='font-size:1em'>1</text>
<text text-anchor='middle' x='240' y='4' fill='currentColor' style='font-size:1em'>h</text>
<text text-anchor='middle' x='240' y='20' fill='currentColor' style='font-size:1em'>:</text>
<text text-anchor='middle' x='248' y='4' fill='currentColor' style='font-size:1em'>i</text>
<text text-anchor='middle' x='248' y='20' fill='currentColor' style='font-size:1em'>3</text>
<text text-anchor='middle' x='256' y='4' fill='currentColor' style='font-size:1em'>d</text>
<text text-anchor='middle' x='256' y='20' fill='currentColor' style='font-size:1em'>3</text>
<text text-anchor='middle' x='264' y='4' fill='currentColor' style='font-size:1em'>d</text>
<text text-anchor='middle' x='264' y='20' fill='currentColor' style='font-size:1em'>0</text>
<text text-anchor='middle' x='272' y='4' fill='currentColor' style='font-size:1em'>e</text>
<text text-anchor='middle' x='272' y='20' fill='currentColor' style='font-size:1em'>1</text>
<text text-anchor='middle' x='280' y='4' fill='currentColor' style='font-size:1em'>n</text>
<text text-anchor='middle' x='288' y='4' fill='currentColor' style='font-size:1em'>_</text>
<text text-anchor='middle' x='296' y='4' fill='currentColor' style='font-size:1em'>s</text>
<text text-anchor='middle' x='304' y='4' fill='currentColor' style='font-size:1em'>i</text>
<text text-anchor='middle' x='312' y='4' fill='currentColor' style='font-size:1em'>t</text>
<text text-anchor='middle' x='320' y='4' fill='currentColor' style='font-size:1em'>e</text>
<text text-anchor='middle' x='328' y='4' fill='currentColor' style='font-size:1em'>/</text>
</g>
</svg>
</div>
<p>Make sure the directory is owned by Tors user and private:</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 /var/lib/tor/hidden_site
</span></span><span style="display:flex;"><span>sudo chown -R debian-tor:debian-tor /var/lib/tor/hidden_site
</span></span><span style="display:flex;"><span>sudo chmod <span style="color:#ae81ff">700</span> /var/lib/tor/hidden_site
</span></span></code></pre></div><p><strong>Important:</strong> use the right systemd unit:</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"># validate as the Tor user</span>
</span></span><span style="display:flex;"><span>sudo -u debian-tor tor -f /etc/tor/torrc --verify-config
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># (re)start the real service</span>
</span></span><span style="display:flex;"><span>sudo systemctl enable --now tor@default
</span></span><span style="display:flex;"><span>sudo systemctl restart tor@default
</span></span></code></pre></div><p>Get the onion address:</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 cat /var/lib/tor/hidden_site/hostname
</span></span></code></pre></div><p>Quick check (do remote DNS via SOCKS):</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 --socks5-hostname 127.0.0.1:9050 <span style="color:#e6db74">&#34;http://</span><span style="color:#66d9ef">$(</span>sudo cat /var/lib/tor/hidden_site/hostname<span style="color:#66d9ef">)</span><span style="color:#e6db74">&#34;</span>
</span></span></code></pre></div><hr>
<h2 id="2-nginx-for-the-onion-localhost-only">2) Nginx for the onion (localhost-only)</h2>
<p>I keep the onion site strictly on localhost: <strong>no HTTPS, no redirects, no HSTS</strong>. Tor already provides e2e encryption and authenticity.</p>
<p><code>/etc/nginx/sites-available/onion-blog</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">listen</span> 127.0.0.1:<span style="color:#ae81ff">3301</span> <span style="color:#e6db74">default_server</span>;
</span></span><span style="display:flex;"><span> <span style="color:#f92672">server_name</span> <span style="color:#e6db74">&lt;your-56-char&gt;.onion</span> 127.0.0.1 <span style="color:#e6db74">localhost</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#75715e"># keep onion simple; no HSTS/redirects here
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> <span style="color:#f92672">add_header</span> <span style="color:#e6db74">Referrer-Policy</span> <span style="color:#e6db74">no-referrer</span> <span style="color:#e6db74">always</span>;
</span></span><span style="display:flex;"><span> <span style="color:#f92672">add_header</span> <span style="color:#e6db74">X-Content-Type-Options</span> <span style="color:#e6db74">nosniff</span> <span style="color:#e6db74">always</span>;
</span></span><span style="display:flex;"><span> <span style="color:#f92672">add_header</span> <span style="color:#e6db74">X-Frame-Options</span> <span style="color:#e6db74">SAMEORIGIN</span> <span style="color:#e6db74">always</span>;
</span></span><span style="display:flex;"><span> <span style="color:#75715e"># (optional) strict CSP so nothing leaks to clearnet
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> <span style="color:#75715e"># add_header Content-Security-Policy &#34;default-src &#39;self&#39;; img-src &#39;self&#39; data:; style-src &#39;self&#39; &#39;unsafe-inline&#39;; script-src &#39;self&#39;&#34; always;
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">root</span> <span style="color:#e6db74">/srv/hugo/mysite/public-onion</span>;
</span></span><span style="display:flex;"><span> <span style="color:#f92672">index</span> <span style="color:#e6db74">index.html</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 style="color:#f92672">try_files</span> $uri $uri/ <span style="color:#e6db74">/index.html</span>; }
</span></span><span style="display:flex;"><span> <span style="color:#f92672">location</span> ~<span style="color:#e6db74">*</span> <span style="color:#e6db74">\.(css|js|ico|png|jpg|jpeg|gif|svg|webp|txt|xml)</span>$ {
</span></span><span style="display:flex;"><span> <span style="color:#f92672">access_log</span> <span style="color:#66d9ef">off</span>;
</span></span><span style="display:flex;"><span> <span style="color:#f92672">add_header</span> <span style="color:#e6db74">Cache-Control</span> <span style="color:#e6db74">&#34;public,</span> <span style="color:#e6db74">max-age=31536000,</span> <span style="color:#e6db74">immutable&#34;</span>;
</span></span><span style="display:flex;"><span> <span style="color:#f92672">try_files</span> $uri =<span style="color:#ae81ff">404</span>;
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Enable/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 ln -sf /etc/nginx/sites-available/onion-blog /etc/nginx/sites-enabled/onion-blog
</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><blockquote>
<p>Gotcha I hit: I had <strong>two</strong> server blocks on <code>127.0.0.1:3301</code>, which caused 404s via onion. Make sure only the intended vhost listens there (or mark it <code>default_server</code>). Also, if you ever use a regex in <code>server_name</code>, the syntax is <code>server_name ~* \.onion$</code> (note the space after <code>~*</code>).</p></blockquote>
<hr>
<h2 id="3-hugo--papermod-setup-and-version-bumps">3) Hugo + PaperMod setup (and version bumps)</h2>
<p>PaperMod now requires <strong>Hugo Extended ≥ 0.146.0</strong>. I installed the extended binary from the official tarball to avoid Snaps sandbox limitations (Snap cant read <code>/srv</code> paths by default).</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"># install Hugo extended (example)</span>
</span></span><span style="display:flex;"><span>VER<span style="color:#f92672">=</span>0.146.0
</span></span><span style="display:flex;"><span>cd /tmp
</span></span><span style="display:flex;"><span>wget https://github.com/gohugoio/hugo/releases/download/v<span style="color:#e6db74">${</span>VER<span style="color:#e6db74">}</span>/hugo_extended_<span style="color:#e6db74">${</span>VER<span style="color:#e6db74">}</span>_Linux-amd64.tar.gz
</span></span><span style="display:flex;"><span>tar -xzf hugo_extended_<span style="color:#e6db74">${</span>VER<span style="color:#e6db74">}</span>_Linux-amd64.tar.gz
</span></span><span style="display:flex;"><span>sudo mv hugo /usr/local/bin/hugo
</span></span><span style="display:flex;"><span>hugo version <span style="color:#75715e"># should say &#34;extended&#34; and &gt;= 0.146.0</span>
</span></span></code></pre></div><p>Create the site and theme:</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/hugo <span style="color:#f92672">&amp;&amp;</span> sudo chown -R <span style="color:#e6db74">&#34;</span>$USER<span style="color:#e6db74">&#34;</span>:<span style="color:#e6db74">&#34;</span>$USER<span style="color:#e6db74">&#34;</span> /srv/hugo
</span></span><span style="display:flex;"><span>cd /srv/hugo
</span></span><span style="display:flex;"><span>hugo new site mysite
</span></span><span style="display:flex;"><span>cd mysite
</span></span><span style="display:flex;"><span>git init
</span></span><span style="display:flex;"><span>git submodule add https://github.com/adityatelange/hugo-PaperMod themes/PaperMod
</span></span></code></pre></div><p><strong>Config updates:</strong> in newer Hugo, <code>paginate</code> is deprecated → use:</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-toml" data-lang="toml"><span style="display:flex;"><span><span style="color:#75715e"># config/_default/hugo.toml</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">title</span> = <span style="color:#e6db74">&#34;My Blog&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">theme</span> = <span style="color:#e6db74">&#34;PaperMod&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">enableRobotsTXT</span> = <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>[<span style="color:#a6e22e">pagination</span>]
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">pagerSize</span> = <span style="color:#ae81ff">10</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>[<span style="color:#a6e22e">params</span>]
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">defaultTheme</span> = <span style="color:#e6db74">&#34;auto&#34;</span>
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">showReadingTime</span> = <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">showPostNavLinks</span> = <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">showBreadCrumbs</span> = <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">showCodeCopyButtons</span> = <span style="color:#66d9ef">true</span>
</span></span></code></pre></div><p>I added per-environment overrides so I can build two outputs with different <code>baseURL</code>s:</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-toml" data-lang="toml"><span style="display:flex;"><span><span style="color:#75715e"># config/clearnet/hugo.toml</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">baseURL</span> = <span style="color:#e6db74">&#34;https://blog.alipourimjourneys.ir/&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># config/onion/hugo.toml</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">baseURL</span> = <span style="color:#e6db74">&#34;http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/&#34;</span>
</span></span></code></pre></div><hr>
<h2 id="4-dual-builds-clearnet--onion-and-one-command-deploy">4) Dual builds (clearnet + onion) and one-command deploy</h2>
<p>I publish the same content twice—once for each base URL and docroot:</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/hugo/mysite
</span></span><span style="display:flex;"><span><span style="color:#75715e"># clearnet build (served over HTTPS)</span>
</span></span><span style="display:flex;"><span>hugo --minify --environment clearnet -d public-clearnet
</span></span><span style="display:flex;"><span><span style="color:#75715e"># onion build (served via Tor)</span>
</span></span><span style="display:flex;"><span>hugo --minify --environment onion -d public-onion
</span></span><span style="display:flex;"><span>sudo systemctl reload nginx
</span></span></code></pre></div><p>Helper script I use:</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 tee /usr/local/bin/build-both &gt;/dev/null <span style="color:#e6db74">&lt;&lt;&#39;EOF&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">#!/usr/bin/env bash
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">set -euo pipefail
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">cd /srv/hugo/mysite
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">hugo --minify --environment clearnet -d public-clearnet
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">hugo --minify --environment onion -d public-onion
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">sudo systemctl reload nginx
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">echo &#34;Deployed both at $(date)&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">EOF</span>
</span></span><span style="display:flex;"><span>sudo chmod +x /usr/local/bin/build-both
</span></span></code></pre></div><p>While editing, I sometimes auto-rebuild on file save:</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 install -y entr
</span></span><span style="display:flex;"><span>cd /srv/hugo/mysite
</span></span><span style="display:flex;"><span>find content layouts assets static config -type f | entr -r build-both
</span></span></code></pre></div><hr>
<h2 id="5-clearnet-behind-cloudflare--lets-encrypt-manual-dns-01">5) Clearnet behind Cloudflare + Lets Encrypt (manual DNS-01)</h2>
<p>Cloudflare is set to <strong>Full (strict)</strong>. I issued a public cert for <code>blog.alipourimjourneys.ir</code> using <strong>manual DNS-01</strong> (no API token):</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 snap install --classic certbot
</span></span><span style="display:flex;"><span>sudo certbot certonly --manual --preferred-challenges dns -d blog.alipourimjourneys.ir --agree-tos -m you@example.com --no-eff-email
</span></span></code></pre></div><p>Certbot tells you to add a TXT record <code>_acme-challenge.blog.alipourimjourneys.ir</code>. Add it in Cloudflare DNS, verify with <code>dig</code>, then continue. Cert ends up at:</p>
<div class="goat svg-container ">
<svg
xmlns="http://www.w3.org/2000/svg"
font-family="Menlo,Lucida Console,monospace"
viewBox="0 0 496 41"
>
<g transform='translate(8,16)'>
<text text-anchor='middle' x='0' y='4' fill='currentColor' style='font-size:1em'>/</text>
<text text-anchor='middle' x='0' y='20' fill='currentColor' style='font-size:1em'>/</text>
<text text-anchor='middle' x='8' y='4' fill='currentColor' style='font-size:1em'>e</text>
<text text-anchor='middle' x='8' y='20' fill='currentColor' style='font-size:1em'>e</text>
<text text-anchor='middle' x='16' y='4' fill='currentColor' style='font-size:1em'>t</text>
<text text-anchor='middle' x='16' y='20' fill='currentColor' style='font-size:1em'>t</text>
<text text-anchor='middle' x='24' y='4' fill='currentColor' style='font-size:1em'>c</text>
<text text-anchor='middle' x='24' y='20' fill='currentColor' style='font-size:1em'>c</text>
<text text-anchor='middle' x='32' y='4' fill='currentColor' style='font-size:1em'>/</text>
<text text-anchor='middle' x='32' y='20' fill='currentColor' style='font-size:1em'>/</text>
<text text-anchor='middle' x='40' y='4' fill='currentColor' style='font-size:1em'>l</text>
<text text-anchor='middle' x='40' y='20' fill='currentColor' style='font-size:1em'>l</text>
<text text-anchor='middle' x='48' y='4' fill='currentColor' style='font-size:1em'>e</text>
<text text-anchor='middle' x='48' y='20' fill='currentColor' style='font-size:1em'>e</text>
<text text-anchor='middle' x='56' y='4' fill='currentColor' style='font-size:1em'>t</text>
<text text-anchor='middle' x='56' y='20' fill='currentColor' style='font-size:1em'>t</text>
<text text-anchor='middle' x='64' y='4' fill='currentColor' style='font-size:1em'>s</text>
<text text-anchor='middle' x='64' y='20' fill='currentColor' style='font-size:1em'>s</text>
<text text-anchor='middle' x='72' y='4' fill='currentColor' style='font-size:1em'>e</text>
<text text-anchor='middle' x='72' y='20' fill='currentColor' style='font-size:1em'>e</text>
<text text-anchor='middle' x='80' y='4' fill='currentColor' style='font-size:1em'>n</text>
<text text-anchor='middle' x='80' y='20' fill='currentColor' style='font-size:1em'>n</text>
<text text-anchor='middle' x='88' y='4' fill='currentColor' style='font-size:1em'>c</text>
<text text-anchor='middle' x='88' y='20' fill='currentColor' style='font-size:1em'>c</text>
<text text-anchor='middle' x='96' y='4' fill='currentColor' style='font-size:1em'>r</text>
<text text-anchor='middle' x='96' y='20' fill='currentColor' style='font-size:1em'>r</text>
<text text-anchor='middle' x='104' y='4' fill='currentColor' style='font-size:1em'>y</text>
<text text-anchor='middle' x='104' y='20' fill='currentColor' style='font-size:1em'>y</text>
<text text-anchor='middle' x='112' y='4' fill='currentColor' style='font-size:1em'>p</text>
<text text-anchor='middle' x='112' y='20' fill='currentColor' style='font-size:1em'>p</text>
<text text-anchor='middle' x='120' y='4' fill='currentColor' style='font-size:1em'>t</text>
<text text-anchor='middle' x='120' y='20' fill='currentColor' style='font-size:1em'>t</text>
<text text-anchor='middle' x='128' y='4' fill='currentColor' style='font-size:1em'>/</text>
<text text-anchor='middle' x='128' y='20' fill='currentColor' style='font-size:1em'>/</text>
<text text-anchor='middle' x='136' y='4' fill='currentColor' style='font-size:1em'>l</text>
<text text-anchor='middle' x='136' y='20' fill='currentColor' style='font-size:1em'>l</text>
<text text-anchor='middle' x='144' y='4' fill='currentColor' style='font-size:1em'>i</text>
<text text-anchor='middle' x='144' y='20' fill='currentColor' style='font-size:1em'>i</text>
<text text-anchor='middle' x='152' y='4' fill='currentColor' style='font-size:1em'>v</text>
<text text-anchor='middle' x='152' y='20' fill='currentColor' style='font-size:1em'>v</text>
<text text-anchor='middle' x='160' y='4' fill='currentColor' style='font-size:1em'>e</text>
<text text-anchor='middle' x='160' y='20' fill='currentColor' style='font-size:1em'>e</text>
<text text-anchor='middle' x='168' y='4' fill='currentColor' style='font-size:1em'>/</text>
<text text-anchor='middle' x='168' y='20' fill='currentColor' style='font-size:1em'>/</text>
<text text-anchor='middle' x='176' y='4' fill='currentColor' style='font-size:1em'>b</text>
<text text-anchor='middle' x='176' y='20' fill='currentColor' style='font-size:1em'>b</text>
<text text-anchor='middle' x='184' y='4' fill='currentColor' style='font-size:1em'>l</text>
<text text-anchor='middle' x='184' y='20' fill='currentColor' style='font-size:1em'>l</text>
<text text-anchor='middle' x='192' y='4' fill='currentColor' style='font-size:1em'>o</text>
<text text-anchor='middle' x='192' y='20' fill='currentColor' style='font-size:1em'>o</text>
<text text-anchor='middle' x='200' y='4' fill='currentColor' style='font-size:1em'>g</text>
<text text-anchor='middle' x='200' y='20' fill='currentColor' style='font-size:1em'>g</text>
<text text-anchor='middle' x='208' y='4' fill='currentColor' style='font-size:1em'>.</text>
<text text-anchor='middle' x='208' y='20' fill='currentColor' style='font-size:1em'>.</text>
<text text-anchor='middle' x='216' y='4' fill='currentColor' style='font-size:1em'>a</text>
<text text-anchor='middle' x='216' y='20' fill='currentColor' style='font-size:1em'>a</text>
<text text-anchor='middle' x='224' y='4' fill='currentColor' style='font-size:1em'>l</text>
<text text-anchor='middle' x='224' y='20' fill='currentColor' style='font-size:1em'>l</text>
<text text-anchor='middle' x='232' y='4' fill='currentColor' style='font-size:1em'>i</text>
<text text-anchor='middle' x='232' y='20' fill='currentColor' style='font-size:1em'>i</text>
<text text-anchor='middle' x='240' y='4' fill='currentColor' style='font-size:1em'>p</text>
<text text-anchor='middle' x='240' y='20' fill='currentColor' style='font-size:1em'>p</text>
<text text-anchor='middle' x='248' y='4' fill='currentColor' style='font-size:1em'>o</text>
<text text-anchor='middle' x='248' y='20' fill='currentColor' style='font-size:1em'>o</text>
<text text-anchor='middle' x='256' y='4' fill='currentColor' style='font-size:1em'>u</text>
<text text-anchor='middle' x='256' y='20' fill='currentColor' style='font-size:1em'>u</text>
<text text-anchor='middle' x='264' y='4' fill='currentColor' style='font-size:1em'>r</text>
<text text-anchor='middle' x='264' y='20' fill='currentColor' style='font-size:1em'>r</text>
<text text-anchor='middle' x='272' y='4' fill='currentColor' style='font-size:1em'>i</text>
<text text-anchor='middle' x='272' y='20' fill='currentColor' style='font-size:1em'>i</text>
<text text-anchor='middle' x='280' y='4' fill='currentColor' style='font-size:1em'>m</text>
<text text-anchor='middle' x='280' y='20' fill='currentColor' style='font-size:1em'>m</text>
<text text-anchor='middle' x='288' y='4' fill='currentColor' style='font-size:1em'>j</text>
<text text-anchor='middle' x='288' y='20' fill='currentColor' style='font-size:1em'>j</text>
<text text-anchor='middle' x='296' y='4' fill='currentColor' style='font-size:1em'>o</text>
<text text-anchor='middle' x='296' y='20' fill='currentColor' style='font-size:1em'>o</text>
<text text-anchor='middle' x='304' y='4' fill='currentColor' style='font-size:1em'>u</text>
<text text-anchor='middle' x='304' y='20' fill='currentColor' style='font-size:1em'>u</text>
<text text-anchor='middle' x='312' y='4' fill='currentColor' style='font-size:1em'>r</text>
<text text-anchor='middle' x='312' y='20' fill='currentColor' style='font-size:1em'>r</text>
<text text-anchor='middle' x='320' y='4' fill='currentColor' style='font-size:1em'>n</text>
<text text-anchor='middle' x='320' y='20' fill='currentColor' style='font-size:1em'>n</text>
<text text-anchor='middle' x='328' y='4' fill='currentColor' style='font-size:1em'>e</text>
<text text-anchor='middle' x='328' y='20' fill='currentColor' style='font-size:1em'>e</text>
<text text-anchor='middle' x='336' y='4' fill='currentColor' style='font-size:1em'>y</text>
<text text-anchor='middle' x='336' y='20' fill='currentColor' style='font-size:1em'>y</text>
<text text-anchor='middle' x='344' y='4' fill='currentColor' style='font-size:1em'>s</text>
<text text-anchor='middle' x='344' y='20' fill='currentColor' style='font-size:1em'>s</text>
<text text-anchor='middle' x='352' y='4' fill='currentColor' style='font-size:1em'>.</text>
<text text-anchor='middle' x='352' y='20' fill='currentColor' style='font-size:1em'>.</text>
<text text-anchor='middle' x='360' y='4' fill='currentColor' style='font-size:1em'>i</text>
<text text-anchor='middle' x='360' y='20' fill='currentColor' style='font-size:1em'>i</text>
<text text-anchor='middle' x='368' y='4' fill='currentColor' style='font-size:1em'>r</text>
<text text-anchor='middle' x='368' y='20' fill='currentColor' style='font-size:1em'>r</text>
<text text-anchor='middle' x='376' y='4' fill='currentColor' style='font-size:1em'>/</text>
<text text-anchor='middle' x='376' y='20' fill='currentColor' style='font-size:1em'>/</text>
<text text-anchor='middle' x='384' y='4' fill='currentColor' style='font-size:1em'>f</text>
<text text-anchor='middle' x='384' y='20' fill='currentColor' style='font-size:1em'>p</text>
<text text-anchor='middle' x='392' y='4' fill='currentColor' style='font-size:1em'>u</text>
<text text-anchor='middle' x='392' y='20' fill='currentColor' style='font-size:1em'>r</text>
<text text-anchor='middle' x='400' y='4' fill='currentColor' style='font-size:1em'>l</text>
<text text-anchor='middle' x='400' y='20' fill='currentColor' style='font-size:1em'>i</text>
<text text-anchor='middle' x='408' y='4' fill='currentColor' style='font-size:1em'>l</text>
<text text-anchor='middle' x='408' y='20' fill='currentColor' style='font-size:1em'>v</text>
<text text-anchor='middle' x='416' y='4' fill='currentColor' style='font-size:1em'>c</text>
<text text-anchor='middle' x='416' y='20' fill='currentColor' style='font-size:1em'>k</text>
<text text-anchor='middle' x='424' y='4' fill='currentColor' style='font-size:1em'>h</text>
<text text-anchor='middle' x='424' y='20' fill='currentColor' style='font-size:1em'>e</text>
<text text-anchor='middle' x='432' y='4' fill='currentColor' style='font-size:1em'>a</text>
<text text-anchor='middle' x='432' y='20' fill='currentColor' style='font-size:1em'>y</text>
<text text-anchor='middle' x='440' y='4' fill='currentColor' style='font-size:1em'>i</text>
<text text-anchor='middle' x='440' y='20' fill='currentColor' style='font-size:1em'>.</text>
<text text-anchor='middle' x='448' y='4' fill='currentColor' style='font-size:1em'>n</text>
<text text-anchor='middle' x='448' y='20' fill='currentColor' style='font-size:1em'>p</text>
<text text-anchor='middle' x='456' y='4' fill='currentColor' style='font-size:1em'>.</text>
<text text-anchor='middle' x='456' y='20' fill='currentColor' style='font-size:1em'>e</text>
<text text-anchor='middle' x='464' y='4' fill='currentColor' style='font-size:1em'>p</text>
<text text-anchor='middle' x='464' y='20' fill='currentColor' style='font-size:1em'>m</text>
<text text-anchor='middle' x='472' y='4' fill='currentColor' style='font-size:1em'>e</text>
<text text-anchor='middle' x='480' y='4' fill='currentColor' style='font-size:1em'>m</text>
</g>
</svg>
</div>
<p>Clearnet Nginx vhosts:</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"># HTTP → HTTPS redirect (optional; CF usually talks HTTPS to origin anyway)
</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 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">blog.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://blog.alipourimjourneys.ir</span>$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:#75715e"># HTTPS origin (behind Cloudflare)
</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">443</span> <span style="color:#e6db74">ssl</span> <span style="color:#e6db74">http2</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 style="color:#f92672">server_name</span> <span style="color:#e6db74">blog.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/blog.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/blog.alipourimjourneys.ir/privkey.pem</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#75715e"># HSTS on clearnet only
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> <span style="color:#f92672">add_header</span> <span style="color:#e6db74">Strict-Transport-Security</span> <span style="color:#e6db74">&#34;max-age=31536000</span>; <span style="color:#f92672">includeSubDomains</span>; <span style="color:#f92672">preload&#34;</span> <span style="color:#e6db74">always</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#75715e"># Help Tor Browser discover the onion mirror (safe on clearnet)
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> <span style="color:#f92672">add_header</span> <span style="color:#e6db74">Onion-Location</span> <span style="color:#e6db74">&#34;http://&lt;your-56-char&gt;.onion</span>$request_uri&#34; <span style="color:#e6db74">always</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">root</span> <span style="color:#e6db74">/srv/hugo/mysite/public-clearnet</span>;
</span></span><span style="display:flex;"><span> <span style="color:#f92672">index</span> <span style="color:#e6db74">index.html</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 style="color:#f92672">try_files</span> $uri $uri/ <span style="color:#e6db74">/index.html</span>; }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><blockquote>
<p>Note: dont add HSTS or HTTPS redirects to the <strong>onion</strong> vhost. Keep onion pure HTTP on localhost.</p></blockquote>
<hr>
<h2 id="6-troubleshooting-i-ran-into-and-fixes">6) Troubleshooting I ran into (and fixes)</h2>
<ul>
<li><strong>Tor unit confusion:</strong> <code>tor.service</code> is a tiny master; the real daemon is <code>tor@default</code>. Use that unit and verify config as <code>debian-tor</code>.</li>
<li><strong>Permissions:</strong> <code>HiddenServiceDir</code> must be owned by <code>debian-tor</code> and mode <code>700</code>.</li>
<li><strong>Mapping mismatch:</strong> If <code>HiddenServicePort 80 127.0.0.1:3301</code> is set, visit <code>http://&lt;onion&gt;/</code> (no <code>:3301</code>). If you set <code>HiddenServicePort 3301 127.0.0.1:3301</code>, you must use <code>http://&lt;onion&gt;:3301/</code>.</li>
<li><strong>Curl &amp; .onion:</strong> modern curl refuses <code>.onion</code> unless you use remote DNS via SOCKS:
<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 --socks5-hostname 127.0.0.1:9050 <span style="color:#e6db74">&#34;http://&lt;onion&gt;/&#34;</span>
</span></span></code></pre></div></li>
<li><strong>Two Nginx vhosts on the same port:</strong> I had a duplicate server on <code>127.0.0.1:3301</code> pointing somewhere else, which caused onion 404s. Keep only one (or mark one <code>default_server</code>).</li>
<li><strong>Regex in <code>server_name</code>:</strong> if you use it, write <code>server_name ~* \.onion$</code> (space after <code>~*</code>). I fixed an <code>invalid variable name</code> error caused by a missing space.</li>
<li><strong>PaperMod with old Hugo:</strong> upgraded to <strong>extended ≥ 0.146.0</strong>. Also updated <code>paginate</code> → <code>[pagination].pagerSize</code>.</li>
<li><strong>Snap confinement:</strong> Snaps <code>hugo</code> couldnt read <code>/srv</code> (<code>.../void: permission denied</code>). Switched to the tarball build in <code>/usr/local/bin</code>.</li>
</ul>
<hr>
<h2 id="7-not-secure-in-tor-browser">7) “Not secure” in Tor Browser?</h2>
<p>That message can appear because onion uses <strong>HTTP</strong>. Its OK: Tor provides e2e encryption + onion auth. If you enable HTTPS-Only Mode, add an exception for the site. Also ensure the onion build doesnt reference <strong>clearnet</strong> resources (scan the built HTML for <code>http(s)://</code> links that arent your onion).</p>
<hr>
<h2 id="8-day-to-day-workflow">8) Day-to-day workflow</h2>
<ul>
<li>Create content:
<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>hugo new posts/my-first-post.md <span style="color:#75715e"># then set draft: false</span>
</span></span></code></pre></div></li>
<li>Publish both:
<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>build-both
</span></span></code></pre></div></li>
<li>(Optional) Auto on save:
<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>find content layouts assets static config -type f | entr -r build-both
</span></span></code></pre></div></li>
<li>(Optional) Git push-to-deploy with a bare repo + post-receive hook that runs <code>build-both</code>.</li>
</ul>
<hr>
<h2 id="final-notes">Final notes</h2>
<ul>
<li>Clearnet gets <strong>HTTPS + HSTS</strong> and an <code>Onion-Location</code> header.</li>
<li>Onion gets <strong>no HSTS/redirects</strong>, and all assets are self-hosted to avoid mixed content.</li>
<li>Serving both worlds from one Hugo repo is easy: <strong>two builds, two vhosts, one workflow</strong>.</li>
</ul>
]]></content:encoded></item><item><title>Stealth Trojan VPN Behind Cloudflare Guide</title><link>http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/posts/vpn/</link><pubDate>Tue, 09 Sep 2025 11:05:00 +0200</pubDate><guid>http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/posts/vpn/</guid><description>&lt;h2 id="introduction">Introduction&lt;/h2>
&lt;p>So you want a VPN that &lt;strong>doesn&amp;rsquo;t scream &amp;ldquo;I am a VPN&amp;rdquo;&lt;/strong> to every censor and firewall out there?&lt;br>
Welcome to the world of &lt;strong>Trojan over WebSocket + TLS behind Cloudflare&lt;/strong>.&lt;/p>
&lt;p>This guide not only shows you how to set it up but also sprinkles in some &lt;strong>debugging magic&lt;/strong> so you can figure out why things break (and they &lt;em>will&lt;/em> break, trust me).&lt;/p>
&lt;p>Well anonymise domains and secrets, so substitute with your own:&lt;/p></description><content:encoded><![CDATA[<h2 id="introduction">Introduction</h2>
<p>So you want a VPN that <strong>doesn&rsquo;t scream &ldquo;I am a VPN&rdquo;</strong> to every censor and firewall out there?<br>
Welcome to the world of <strong>Trojan over WebSocket + TLS behind Cloudflare</strong>.</p>
<p>This guide not only shows you how to set it up but also sprinkles in some <strong>debugging magic</strong> so you can figure out why things break (and they <em>will</em> break, trust me).</p>
<p>Well anonymise domains and secrets, so substitute with your own:</p>
<ul>
<li>VPN domain: <code>web.example.com</code></li>
<li>Panel domain: <code>panel.example.com</code></li>
<li>Secret WS path: <code>/stealth-path_abcd1234</code></li>
<li>Password: <code>&lt;PASSWORD&gt;</code></li>
</ul>
<hr>
<h2 id="architecture-at-a-glance">Architecture at a Glance</h2>
<p>Think of it as a disguise party:</p>
<ul>
<li><strong>Trojan</strong> = the shy guest (your VPN protocol)</li>
<li><strong>Nginx</strong> = the bouncer checking IDs (reverse proxy)</li>
<li><strong>Cloudflare</strong> = the doorman who makes sure nobody sees who&rsquo;s inside (CDN &amp; proxy)</li>
<li><strong>Your fake website</strong> = the mask (camouflage page)</li>
</ul>
<p>Traffic flow:</p>
<div class="goat svg-container ">
<svg
xmlns="http://www.w3.org/2000/svg"
font-family="Menlo,Lucida Console,monospace"
viewBox="0 0 536 25"
>
<g transform='translate(8,16)'>
<text text-anchor='middle' x='0' y='4' fill='currentColor' style='font-size:1em'>C</text>
<text text-anchor='middle' x='8' y='4' fill='currentColor' style='font-size:1em'>l</text>
<text text-anchor='middle' x='16' y='4' fill='currentColor' style='font-size:1em'>i</text>
<text text-anchor='middle' x='24' y='4' fill='currentColor' style='font-size:1em'>e</text>
<text text-anchor='middle' x='32' y='4' fill='currentColor' style='font-size:1em'>n</text>
<text text-anchor='middle' x='40' y='4' fill='currentColor' style='font-size:1em'>t</text>
<text text-anchor='middle' x='56' y='4' fill='currentColor' style='font-size:1em'>→</text>
<text text-anchor='middle' x='72' y='4' fill='currentColor' style='font-size:1em'>C</text>
<text text-anchor='middle' x='80' y='4' fill='currentColor' style='font-size:1em'>l</text>
<text text-anchor='middle' x='88' y='4' fill='currentColor' style='font-size:1em'>o</text>
<text text-anchor='middle' x='96' y='4' fill='currentColor' style='font-size:1em'>u</text>
<text text-anchor='middle' x='104' y='4' fill='currentColor' style='font-size:1em'>d</text>
<text text-anchor='middle' x='112' y='4' fill='currentColor' style='font-size:1em'>f</text>
<text text-anchor='middle' x='120' y='4' fill='currentColor' style='font-size:1em'>l</text>
<text text-anchor='middle' x='128' y='4' fill='currentColor' style='font-size:1em'>a</text>
<text text-anchor='middle' x='136' y='4' fill='currentColor' style='font-size:1em'>r</text>
<text text-anchor='middle' x='144' y='4' fill='currentColor' style='font-size:1em'>e</text>
<text text-anchor='middle' x='160' y='4' fill='currentColor' style='font-size:1em'>(</text>
<text text-anchor='middle' x='168' y='4' fill='currentColor' style='font-size:1em'>4</text>
<text text-anchor='middle' x='176' y='4' fill='currentColor' style='font-size:1em'>4</text>
<text text-anchor='middle' x='184' y='4' fill='currentColor' style='font-size:1em'>3</text>
<text text-anchor='middle' x='192' y='4' fill='currentColor' style='font-size:1em'>)</text>
<text text-anchor='middle' x='208' y='4' fill='currentColor' style='font-size:1em'>→</text>
<text text-anchor='middle' x='224' y='4' fill='currentColor' style='font-size:1em'>N</text>
<text text-anchor='middle' x='232' y='4' fill='currentColor' style='font-size:1em'>g</text>
<text text-anchor='middle' x='240' y='4' fill='currentColor' style='font-size:1em'>i</text>
<text text-anchor='middle' x='248' y='4' fill='currentColor' style='font-size:1em'>n</text>
<text text-anchor='middle' x='256' y='4' fill='currentColor' style='font-size:1em'>x</text>
<text text-anchor='middle' x='272' y='4' fill='currentColor' style='font-size:1em'>(</text>
<text text-anchor='middle' x='280' y='4' fill='currentColor' style='font-size:1em'>4</text>
<text text-anchor='middle' x='288' y='4' fill='currentColor' style='font-size:1em'>4</text>
<text text-anchor='middle' x='296' y='4' fill='currentColor' style='font-size:1em'>3</text>
<text text-anchor='middle' x='304' y='4' fill='currentColor' style='font-size:1em'>)</text>
<text text-anchor='middle' x='320' y='4' fill='currentColor' style='font-size:1em'>→</text>
<text text-anchor='middle' x='336' y='4' fill='currentColor' style='font-size:1em'>T</text>
<text text-anchor='middle' x='344' y='4' fill='currentColor' style='font-size:1em'>r</text>
<text text-anchor='middle' x='352' y='4' fill='currentColor' style='font-size:1em'>o</text>
<text text-anchor='middle' x='360' y='4' fill='currentColor' style='font-size:1em'>j</text>
<text text-anchor='middle' x='368' y='4' fill='currentColor' style='font-size:1em'>a</text>
<text text-anchor='middle' x='376' y='4' fill='currentColor' style='font-size:1em'>n</text>
<text text-anchor='middle' x='392' y='4' fill='currentColor' style='font-size:1em'>(</text>
<text text-anchor='middle' x='400' y='4' fill='currentColor' style='font-size:1em'>l</text>
<text text-anchor='middle' x='408' y='4' fill='currentColor' style='font-size:1em'>o</text>
<text text-anchor='middle' x='416' y='4' fill='currentColor' style='font-size:1em'>c</text>
<text text-anchor='middle' x='424' y='4' fill='currentColor' style='font-size:1em'>a</text>
<text text-anchor='middle' x='432' y='4' fill='currentColor' style='font-size:1em'>l</text>
<text text-anchor='middle' x='440' y='4' fill='currentColor' style='font-size:1em'>h</text>
<text text-anchor='middle' x='448' y='4' fill='currentColor' style='font-size:1em'>o</text>
<text text-anchor='middle' x='456' y='4' fill='currentColor' style='font-size:1em'>s</text>
<text text-anchor='middle' x='464' y='4' fill='currentColor' style='font-size:1em'>t</text>
<text text-anchor='middle' x='472' y='4' fill='currentColor' style='font-size:1em'>:</text>
<text text-anchor='middle' x='480' y='4' fill='currentColor' style='font-size:1em'>5</text>
<text text-anchor='middle' x='488' y='4' fill='currentColor' style='font-size:1em'>4</text>
<text text-anchor='middle' x='496' y='4' fill='currentColor' style='font-size:1em'>3</text>
<text text-anchor='middle' x='504' y='4' fill='currentColor' style='font-size:1em'>2</text>
<text text-anchor='middle' x='512' y='4' fill='currentColor' style='font-size:1em'>1</text>
<text text-anchor='middle' x='520' y='4' fill='currentColor' style='font-size:1em'>)</text>
</g>
</svg>
</div>
<hr>
<h2 id="step-1-panel-setup-panelexamplecom">Step 1: Panel Setup (<code>panel.example.com</code>)</h2>
<ul>
<li>Bind the 3x-ui panel to localhost (e.g., <code>127.0.0.1:46309</code>).</li>
<li>Choose a funky <strong>web base path</strong> like <code>/panel-bananas_42/</code>.</li>
<li>Proxy it through Nginx with HTTPS + Basic Auth.</li>
<li>Test it at <code>https://panel.example.com/panel-bananas_42/</code>.</li>
</ul>
<p><strong>Pro tip:</strong> If you see a blank page → your base path is mismatched or Nginx is eating it. Check logs!</p>
<hr>
<h2 id="step-2-trojan-inbound">Step 2: Trojan Inbound</h2>
<p>In 3x-ui, create a Trojan inbound:</p>
<ul>
<li>Local port: <code>54321</code></li>
<li>Transport: WebSocket</li>
<li>Path: <code>/stealth-path_abcd1234</code></li>
<li>Security: none</li>
<li>Password: <code>&lt;PASSWORD&gt;</code></li>
</ul>
<hr>
<h2 id="step-3-nginx-for-vpn-domain-webexamplecom">Step 3: Nginx for VPN Domain (<code>web.example.com</code>)</h2>
<p>Your Nginx is the gatekeeper. Sample 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-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">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">server_name</span> <span style="color:#e6db74">web.example.com</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/web.example.com/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/web.example.com/privkey.pem</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#75715e"># Fake website at root
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> <span style="color:#f92672">location</span> <span style="color:#e6db74">/</span> {
</span></span><span style="display:flex;"><span> <span style="color:#f92672">root</span> <span style="color:#e6db74">/var/www/html</span>;
</span></span><span style="display:flex;"><span> <span style="color:#f92672">index</span> <span style="color:#e6db74">index.html</span>;
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#75715e"># Real VPN under secret WS path
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> <span style="color:#f92672">location</span> <span style="color:#e6db74">/stealth-path_abcd1234</span> {
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_pass</span> <span style="color:#e6db74">http://127.0.0.1:54321</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 style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">Host</span> $host;
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><hr>
<h2 id="step-4-firewall-rules">Step 4: Firewall Rules</h2>
<p>Lock things down! Only Cloudflare should reach you:</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>ufw allow 22/tcp
</span></span><span style="display:flex;"><span>ufw allow 80/tcp
</span></span><span style="display:flex;"><span>ufw allow 443/tcp
</span></span><span style="display:flex;"><span>ufw deny 54321/tcp
</span></span></code></pre></div><hr>
<h2 id="step-5-client-config">Step 5: Client Config</h2>
<p>Trojan URI:</p>
<div class="goat svg-container ">
<svg
xmlns="http://www.w3.org/2000/svg"
font-family="Menlo,Lucida Console,monospace"
viewBox="0 0 1160 25"
>
<g transform='translate(8,16)'>
<text text-anchor='middle' x='0' y='4' fill='currentColor' style='font-size:1em'>t</text>
<text text-anchor='middle' x='8' y='4' fill='currentColor' style='font-size:1em'>r</text>
<text text-anchor='middle' x='16' y='4' fill='currentColor' style='font-size:1em'>o</text>
<text text-anchor='middle' x='24' y='4' fill='currentColor' style='font-size:1em'>j</text>
<text text-anchor='middle' x='32' y='4' fill='currentColor' style='font-size:1em'>a</text>
<text text-anchor='middle' x='40' y='4' fill='currentColor' style='font-size:1em'>n</text>
<text text-anchor='middle' x='48' y='4' fill='currentColor' style='font-size:1em'>:</text>
<text text-anchor='middle' x='56' y='4' fill='currentColor' style='font-size:1em'>/</text>
<text text-anchor='middle' x='64' y='4' fill='currentColor' style='font-size:1em'>/</text>
<text text-anchor='middle' x='72' y='4' fill='currentColor' style='font-size:1em'>&lt;</text>
<text text-anchor='middle' x='80' y='4' fill='currentColor' style='font-size:1em'>P</text>
<text text-anchor='middle' x='88' y='4' fill='currentColor' style='font-size:1em'>A</text>
<text text-anchor='middle' x='96' y='4' fill='currentColor' style='font-size:1em'>S</text>
<text text-anchor='middle' x='104' y='4' fill='currentColor' style='font-size:1em'>S</text>
<text text-anchor='middle' x='112' y='4' fill='currentColor' style='font-size:1em'>W</text>
<text text-anchor='middle' x='120' y='4' fill='currentColor' style='font-size:1em'>O</text>
<text text-anchor='middle' x='128' y='4' fill='currentColor' style='font-size:1em'>R</text>
<text text-anchor='middle' x='136' y='4' fill='currentColor' style='font-size:1em'>D</text>
<text text-anchor='middle' x='144' y='4' fill='currentColor' style='font-size:1em'>&gt;</text>
<text text-anchor='middle' x='152' y='4' fill='currentColor' style='font-size:1em'>@</text>
<text text-anchor='middle' x='160' y='4' fill='currentColor' style='font-size:1em'>w</text>
<text text-anchor='middle' x='168' y='4' fill='currentColor' style='font-size:1em'>e</text>
<text text-anchor='middle' x='176' y='4' fill='currentColor' style='font-size:1em'>b</text>
<text text-anchor='middle' x='184' y='4' fill='currentColor' style='font-size:1em'>.</text>
<text text-anchor='middle' x='192' y='4' fill='currentColor' style='font-size:1em'>e</text>
<text text-anchor='middle' x='200' y='4' fill='currentColor' style='font-size:1em'>x</text>
<text text-anchor='middle' x='208' y='4' fill='currentColor' style='font-size:1em'>a</text>
<text text-anchor='middle' x='216' y='4' fill='currentColor' style='font-size:1em'>m</text>
<text text-anchor='middle' x='224' y='4' fill='currentColor' style='font-size:1em'>p</text>
<text text-anchor='middle' x='232' y='4' fill='currentColor' style='font-size:1em'>l</text>
<text text-anchor='middle' x='240' y='4' fill='currentColor' style='font-size:1em'>e</text>
<text text-anchor='middle' x='248' y='4' fill='currentColor' style='font-size:1em'>.</text>
<text text-anchor='middle' x='256' y='4' fill='currentColor' style='font-size:1em'>c</text>
<text text-anchor='middle' x='264' y='4' fill='currentColor' style='font-size:1em'>o</text>
<text text-anchor='middle' x='272' y='4' fill='currentColor' style='font-size:1em'>m</text>
<text text-anchor='middle' x='280' y='4' fill='currentColor' style='font-size:1em'>:</text>
<text text-anchor='middle' x='288' y='4' fill='currentColor' style='font-size:1em'>4</text>
<text text-anchor='middle' x='296' y='4' fill='currentColor' style='font-size:1em'>4</text>
<text text-anchor='middle' x='304' y='4' fill='currentColor' style='font-size:1em'>3</text>
<text text-anchor='middle' x='312' y='4' fill='currentColor' style='font-size:1em'>?</text>
<text text-anchor='middle' x='320' y='4' fill='currentColor' style='font-size:1em'>t</text>
<text text-anchor='middle' x='328' y='4' fill='currentColor' style='font-size:1em'>y</text>
<text text-anchor='middle' x='336' y='4' fill='currentColor' style='font-size:1em'>p</text>
<text text-anchor='middle' x='344' y='4' fill='currentColor' style='font-size:1em'>e</text>
<text text-anchor='middle' x='352' y='4' fill='currentColor' style='font-size:1em'>=</text>
<text text-anchor='middle' x='360' y='4' fill='currentColor' style='font-size:1em'>w</text>
<text text-anchor='middle' x='368' y='4' fill='currentColor' style='font-size:1em'>s</text>
<text text-anchor='middle' x='376' y='4' fill='currentColor' style='font-size:1em'>&amp;</text>
<text text-anchor='middle' x='384' y='4' fill='currentColor' style='font-size:1em'>s</text>
<text text-anchor='middle' x='392' y='4' fill='currentColor' style='font-size:1em'>e</text>
<text text-anchor='middle' x='400' y='4' fill='currentColor' style='font-size:1em'>c</text>
<text text-anchor='middle' x='408' y='4' fill='currentColor' style='font-size:1em'>u</text>
<text text-anchor='middle' x='416' y='4' fill='currentColor' style='font-size:1em'>r</text>
<text text-anchor='middle' x='424' y='4' fill='currentColor' style='font-size:1em'>i</text>
<text text-anchor='middle' x='432' y='4' fill='currentColor' style='font-size:1em'>t</text>
<text text-anchor='middle' x='440' y='4' fill='currentColor' style='font-size:1em'>y</text>
<text text-anchor='middle' x='448' y='4' fill='currentColor' style='font-size:1em'>=</text>
<text text-anchor='middle' x='456' y='4' fill='currentColor' style='font-size:1em'>t</text>
<text text-anchor='middle' x='464' y='4' fill='currentColor' style='font-size:1em'>l</text>
<text text-anchor='middle' x='472' y='4' fill='currentColor' style='font-size:1em'>s</text>
<text text-anchor='middle' x='480' y='4' fill='currentColor' style='font-size:1em'>&amp;</text>
<text text-anchor='middle' x='488' y='4' fill='currentColor' style='font-size:1em'>h</text>
<text text-anchor='middle' x='496' y='4' fill='currentColor' style='font-size:1em'>o</text>
<text text-anchor='middle' x='504' y='4' fill='currentColor' style='font-size:1em'>s</text>
<text text-anchor='middle' x='512' y='4' fill='currentColor' style='font-size:1em'>t</text>
<text text-anchor='middle' x='520' y='4' fill='currentColor' style='font-size:1em'>=</text>
<text text-anchor='middle' x='528' y='4' fill='currentColor' style='font-size:1em'>w</text>
<text text-anchor='middle' x='536' y='4' fill='currentColor' style='font-size:1em'>e</text>
<text text-anchor='middle' x='544' y='4' fill='currentColor' style='font-size:1em'>b</text>
<text text-anchor='middle' x='552' y='4' fill='currentColor' style='font-size:1em'>.</text>
<text text-anchor='middle' x='560' y='4' fill='currentColor' style='font-size:1em'>e</text>
<text text-anchor='middle' x='568' y='4' fill='currentColor' style='font-size:1em'>x</text>
<text text-anchor='middle' x='576' y='4' fill='currentColor' style='font-size:1em'>a</text>
<text text-anchor='middle' x='584' y='4' fill='currentColor' style='font-size:1em'>m</text>
<text text-anchor='middle' x='592' y='4' fill='currentColor' style='font-size:1em'>p</text>
<text text-anchor='middle' x='600' y='4' fill='currentColor' style='font-size:1em'>l</text>
<text text-anchor='middle' x='608' y='4' fill='currentColor' style='font-size:1em'>e</text>
<text text-anchor='middle' x='616' y='4' fill='currentColor' style='font-size:1em'>.</text>
<text text-anchor='middle' x='624' y='4' fill='currentColor' style='font-size:1em'>c</text>
<text text-anchor='middle' x='632' y='4' fill='currentColor' style='font-size:1em'>o</text>
<text text-anchor='middle' x='640' y='4' fill='currentColor' style='font-size:1em'>m</text>
<text text-anchor='middle' x='648' y='4' fill='currentColor' style='font-size:1em'>&amp;</text>
<text text-anchor='middle' x='656' y='4' fill='currentColor' style='font-size:1em'>p</text>
<text text-anchor='middle' x='664' y='4' fill='currentColor' style='font-size:1em'>a</text>
<text text-anchor='middle' x='672' y='4' fill='currentColor' style='font-size:1em'>t</text>
<text text-anchor='middle' x='680' y='4' fill='currentColor' style='font-size:1em'>h</text>
<text text-anchor='middle' x='688' y='4' fill='currentColor' style='font-size:1em'>=</text>
<text text-anchor='middle' x='696' y='4' fill='currentColor' style='font-size:1em'>%</text>
<text text-anchor='middle' x='704' y='4' fill='currentColor' style='font-size:1em'>2</text>
<text text-anchor='middle' x='712' y='4' fill='currentColor' style='font-size:1em'>F</text>
<text text-anchor='middle' x='720' y='4' fill='currentColor' style='font-size:1em'>s</text>
<text text-anchor='middle' x='728' y='4' fill='currentColor' style='font-size:1em'>t</text>
<text text-anchor='middle' x='736' y='4' fill='currentColor' style='font-size:1em'>e</text>
<text text-anchor='middle' x='744' y='4' fill='currentColor' style='font-size:1em'>a</text>
<text text-anchor='middle' x='752' y='4' fill='currentColor' style='font-size:1em'>l</text>
<text text-anchor='middle' x='760' y='4' fill='currentColor' style='font-size:1em'>t</text>
<text text-anchor='middle' x='768' y='4' fill='currentColor' style='font-size:1em'>h</text>
<text text-anchor='middle' x='776' y='4' fill='currentColor' style='font-size:1em'>-</text>
<text text-anchor='middle' x='784' y='4' fill='currentColor' style='font-size:1em'>p</text>
<text text-anchor='middle' x='792' y='4' fill='currentColor' style='font-size:1em'>a</text>
<text text-anchor='middle' x='800' y='4' fill='currentColor' style='font-size:1em'>t</text>
<text text-anchor='middle' x='808' y='4' fill='currentColor' style='font-size:1em'>h</text>
<text text-anchor='middle' x='816' y='4' fill='currentColor' style='font-size:1em'>_</text>
<text text-anchor='middle' x='824' y='4' fill='currentColor' style='font-size:1em'>a</text>
<text text-anchor='middle' x='832' y='4' fill='currentColor' style='font-size:1em'>b</text>
<text text-anchor='middle' x='840' y='4' fill='currentColor' style='font-size:1em'>c</text>
<text text-anchor='middle' x='848' y='4' fill='currentColor' style='font-size:1em'>d</text>
<text text-anchor='middle' x='856' y='4' fill='currentColor' style='font-size:1em'>1</text>
<text text-anchor='middle' x='864' y='4' fill='currentColor' style='font-size:1em'>2</text>
<text text-anchor='middle' x='872' y='4' fill='currentColor' style='font-size:1em'>3</text>
<text text-anchor='middle' x='880' y='4' fill='currentColor' style='font-size:1em'>4</text>
<text text-anchor='middle' x='888' y='4' fill='currentColor' style='font-size:1em'>&amp;</text>
<text text-anchor='middle' x='896' y='4' fill='currentColor' style='font-size:1em'>s</text>
<text text-anchor='middle' x='904' y='4' fill='currentColor' style='font-size:1em'>n</text>
<text text-anchor='middle' x='912' y='4' fill='currentColor' style='font-size:1em'>i</text>
<text text-anchor='middle' x='920' y='4' fill='currentColor' style='font-size:1em'>=</text>
<text text-anchor='middle' x='928' y='4' fill='currentColor' style='font-size:1em'>w</text>
<text text-anchor='middle' x='936' y='4' fill='currentColor' style='font-size:1em'>e</text>
<text text-anchor='middle' x='944' y='4' fill='currentColor' style='font-size:1em'>b</text>
<text text-anchor='middle' x='952' y='4' fill='currentColor' style='font-size:1em'>.</text>
<text text-anchor='middle' x='960' y='4' fill='currentColor' style='font-size:1em'>e</text>
<text text-anchor='middle' x='968' y='4' fill='currentColor' style='font-size:1em'>x</text>
<text text-anchor='middle' x='976' y='4' fill='currentColor' style='font-size:1em'>a</text>
<text text-anchor='middle' x='984' y='4' fill='currentColor' style='font-size:1em'>m</text>
<text text-anchor='middle' x='992' y='4' fill='currentColor' style='font-size:1em'>p</text>
<text text-anchor='middle' x='1000' y='4' fill='currentColor' style='font-size:1em'>l</text>
<text text-anchor='middle' x='1008' y='4' fill='currentColor' style='font-size:1em'>e</text>
<text text-anchor='middle' x='1016' y='4' fill='currentColor' style='font-size:1em'>.</text>
<text text-anchor='middle' x='1024' y='4' fill='currentColor' style='font-size:1em'>c</text>
<text text-anchor='middle' x='1032' y='4' fill='currentColor' style='font-size:1em'>o</text>
<text text-anchor='middle' x='1040' y='4' fill='currentColor' style='font-size:1em'>m</text>
<text text-anchor='middle' x='1048' y='4' fill='currentColor' style='font-size:1em'>#</text>
<text text-anchor='middle' x='1056' y='4' fill='currentColor' style='font-size:1em'>M</text>
<text text-anchor='middle' x='1064' y='4' fill='currentColor' style='font-size:1em'>y</text>
<text text-anchor='middle' x='1072' y='4' fill='currentColor' style='font-size:1em'>S</text>
<text text-anchor='middle' x='1080' y='4' fill='currentColor' style='font-size:1em'>t</text>
<text text-anchor='middle' x='1088' y='4' fill='currentColor' style='font-size:1em'>e</text>
<text text-anchor='middle' x='1096' y='4' fill='currentColor' style='font-size:1em'>a</text>
<text text-anchor='middle' x='1104' y='4' fill='currentColor' style='font-size:1em'>l</text>
<text text-anchor='middle' x='1112' y='4' fill='currentColor' style='font-size:1em'>t</text>
<text text-anchor='middle' x='1120' y='4' fill='currentColor' style='font-size:1em'>h</text>
<text text-anchor='middle' x='1128' y='4' fill='currentColor' style='font-size:1em'>V</text>
<text text-anchor='middle' x='1136' y='4' fill='currentColor' style='font-size:1em'>P</text>
<text text-anchor='middle' x='1144' y='4' fill='currentColor' style='font-size:1em'>N</text>
</g>
</svg>
</div>
<p>iOS clients: Shadowrocket, Stash, FoXray<br>
Android clients: v2rayNG, Clash Meta, NekoBox</p>
<hr>
<h2 id="debugging-time-aka-why-the-heck-doesnt-it-work">Debugging Time (a.k.a. “Why the heck doesnt it work?!”)</h2>
<h3 id="symptom-520-unknown-error-cloudflare">Symptom: <strong>520 Unknown Error (Cloudflare)</strong></h3>
<ul>
<li>Likely cause: Nginx couldnt talk to Trojan.</li>
<li>Check Nginx error log:
<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>tail -n <span style="color:#ae81ff">50</span> /var/log/nginx/error.log
</span></span></code></pre></div></li>
<li>If you see <code>upstream sent no valid HTTP/1.0 header</code> → youre mixing HTTPS/HTTP between Nginx and Trojan.</li>
</ul>
<hr>
<h3 id="symptom-526-invalid-ssl-certificate">Symptom: <strong>526 Invalid SSL certificate</strong></h3>
<ul>
<li>Cloudflare → Nginx cert mismatch.</li>
<li>Run:
<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 s_client -connect web.example.com:443 -servername web.example.com -showcerts
</span></span></code></pre></div></li>
<li>Make sure CN = <code>web.example.com</code>. If not, fix your cert.</li>
</ul>
<hr>
<h3 id="symptom-blank-page-on-panel">Symptom: <strong>Blank page on panel</strong></h3>
<ul>
<li>Check if the base path matches exactly (case-sensitive, slash-sensitive).</li>
<li>Watch for sneaky trailing spaces!</li>
<li>Test locally:
<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 -s -D- http://127.0.0.1:46309/panel-bananas_42/
</span></span></code></pre></div></li>
</ul>
<hr>
<h3 id="symptom-client-wont-connect">Symptom: <strong>Client wont connect</strong></h3>
<ul>
<li>Run a WebSocket test:
<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 -k -H <span style="color:#e6db74">&#34;Connection: Upgrade&#34;</span> -H <span style="color:#e6db74">&#34;Upgrade: websocket&#34;</span> https://web.example.com/stealth-path_abcd1234
</span></span></code></pre></div>Expect <code>101 Switching Protocols</code>. If not, check Nginx config.</li>
</ul>
<hr>
<h3 id="symptom-censor-still-blocks-you">Symptom: <strong>Censor still blocks you</strong></h3>
<ul>
<li>Did you expose another service (like SSH, Matrix, or Minecraft) on the same IP?<br>
→ Theyll find your origin IP. Use a second VPS or lock those ports down.</li>
<li>Did you use an obvious path like <code>/ws</code>?<br>
→ Use a random-looking one like <code>/cdn-assets-329df/</code>.</li>
</ul>
<hr>
<h2 id="pro-tips--fun-tricks">Pro Tips &amp; Fun Tricks</h2>
<ul>
<li>Serve a fake blog or portfolio at root so your domain looks legit.</li>
<li>Rotate WebSocket paths occasionally.</li>
<li>Use Cloudflare <strong>Page Rules</strong> to disable Rocket Loader &amp; Minify for your VPN domain.</li>
<li>Make friends with your Nginx error.log — it will roast you but it tells the truth.</li>
</ul>
<hr>
<h2 id="conclusion">Conclusion</h2>
<p>By putting Trojan behind Cloudflare, youve given your VPN a shiny new disguise:</p>
<ul>
<li>Looks like normal HTTPS.</li>
<li>Hides your origin IP.</li>
<li>Forces censors into a tough choice: block Cloudflare (and half the web) or let you pass.</li>
</ul>
<p>Congrats — youve built a VPN with both <strong>style</strong> and <strong>stealth</strong>. 🥷</p>
]]></content:encoded></item><item><title>Self-Hosting HedgeDoc with Docker + Nginx + Let's Encrypt</title><link>http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/posts/hedge_doc/</link><pubDate>Fri, 29 Aug 2025 00:00:00 +0000</pubDate><guid>http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/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 well 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 Lets 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 well 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 Lets 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 Lets 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>Youll 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>
]]></content:encoded></item><item><title>Self-hosting Matrix + Element Call with LiveKit: from zero to working (and the [not so!!] fun debugging along the way)</title><link>http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/posts/matrix_setup/</link><pubDate>Tue, 26 Aug 2025 00:00:00 +0000</pubDate><guid>http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/posts/matrix_setup/</guid><description>How I set up a Matrix homeserver (Synapse) with TURN and Element Call using LiveKit, plus the exact debugging steps that took it from &amp;#39;waiting for media&amp;#39; to solid calls.</description><content:encoded><![CDATA[<blockquote>
<p>TL;DR: The call kept saying <strong>“waiting for media”</strong> because the browser never opened a WebSocket to LiveKit. The root cause was <strong>duplicate <code>Access-Control-Allow-Origin</code> headers</strong> on <code>/sfu/get</code> (CORS), which stopped the JWT response. Fixing CORS and ensuring the WS proxy worked (HTTP <strong>101</strong> in logs) solved it.</p></blockquote>
<h2 id="what-im-building">What I&rsquo;m building</h2>
<ul>
<li>A <strong>Matrix homeserver</strong> (Synapse) at <code>matrix.example.com</code> (replace with your domain).</li>
<li><strong>TURN/STUN</strong> (coTURN) for NAT traversal.</li>
<li><strong>Element Call</strong> backed by <strong>LiveKit</strong>, fronted by <code>rtc.example.com</code>.</li>
<li><strong>Nginx (host)</strong> as the single reverse proxy for everything.</li>
<li><strong>Cloudflare</strong> DNS (with <code>rtc.*</code> set to <strong>DNS-only</strong>, no orange cloud).</li>
<li><strong>UFW</strong> firewall opened for Matrix federation, TURN, and LiveKit media ports.</li>
</ul>
<blockquote>
<p>I used Docker for Synapse, PostgreSQL, LiveKit and the JWT helper. I used <strong>host</strong> Nginx (not Nginx in Docker) to avoid port binding conflicts on 80/443/8448.</p></blockquote>
<hr>
<h2 id="prereqs">Prereqs</h2>
<ul>
<li>DNS A/AAAA:
<ul>
<li><code>matrix.example.com</code> → your server (v4/v6)</li>
<li><code>rtc.example.com</code> → your server (v4/v6)</li>
</ul>
</li>
<li>Certificates:
<ul>
<li><code>matrix.example.com</code> and <code>rtc.example.com</code> via Lets Encrypt on the host</li>
</ul>
</li>
<li>Cloudflare: <strong>DNS-only (grey cloud)</strong> for <code>rtc.example.com</code> so WebSockets &amp; UDP work without interference.</li>
<li>UFW / firewall open:
<ul>
<li>80/tcp, 443/tcp</li>
<li>8448/tcp (Matrix federation)</li>
<li>3478/tcp, 3478/udp and <strong>5349/tcp</strong> (TURN/TLS)</li>
<li><strong>LiveKit</strong>: 7881/tcp and <strong>5010050200/udp</strong> (or your chosen range)</li>
</ul>
</li>
<li>Docker + docker compose installed.</li>
</ul>
<hr>
<h2 id="synapse--postgresql">Synapse + PostgreSQL</h2>
<h3 id="1-the-postgresql-collation-gotcha">1) The PostgreSQL collation gotcha</h3>
<p>Synapse prefers the database collation <strong><code>C</code></strong>. If your Postgres cluster was initialized with <code>en_US.utf8</code>, Synapse will error like:</p>
<div class="goat svg-container ">
<svg
xmlns="http://www.w3.org/2000/svg"
font-family="Menlo,Lucida Console,monospace"
viewBox="0 0 512 25"
>
<g transform='translate(8,16)'>
<text text-anchor='middle' x='0' y='4' fill='currentColor' style='font-size:1em'>D</text>
<text text-anchor='middle' x='8' y='4' fill='currentColor' style='font-size:1em'>a</text>
<text text-anchor='middle' x='16' y='4' fill='currentColor' style='font-size:1em'>t</text>
<text text-anchor='middle' x='24' y='4' fill='currentColor' style='font-size:1em'>a</text>
<text text-anchor='middle' x='32' y='4' fill='currentColor' style='font-size:1em'>b</text>
<text text-anchor='middle' x='40' y='4' fill='currentColor' style='font-size:1em'>a</text>
<text text-anchor='middle' x='48' y='4' fill='currentColor' style='font-size:1em'>s</text>
<text text-anchor='middle' x='56' y='4' fill='currentColor' style='font-size:1em'>e</text>
<text text-anchor='middle' x='72' y='4' fill='currentColor' style='font-size:1em'>h</text>
<text text-anchor='middle' x='80' y='4' fill='currentColor' style='font-size:1em'>a</text>
<text text-anchor='middle' x='88' y='4' fill='currentColor' style='font-size:1em'>s</text>
<text text-anchor='middle' x='104' y='4' fill='currentColor' style='font-size:1em'>i</text>
<text text-anchor='middle' x='112' y='4' fill='currentColor' style='font-size:1em'>n</text>
<text text-anchor='middle' x='120' y='4' fill='currentColor' style='font-size:1em'>c</text>
<text text-anchor='middle' x='128' y='4' fill='currentColor' style='font-size:1em'>o</text>
<text text-anchor='middle' x='136' y='4' fill='currentColor' style='font-size:1em'>r</text>
<text text-anchor='middle' x='144' y='4' fill='currentColor' style='font-size:1em'>r</text>
<text text-anchor='middle' x='152' y='4' fill='currentColor' style='font-size:1em'>e</text>
<text text-anchor='middle' x='160' y='4' fill='currentColor' style='font-size:1em'>c</text>
<text text-anchor='middle' x='168' y='4' fill='currentColor' style='font-size:1em'>t</text>
<text text-anchor='middle' x='184' y='4' fill='currentColor' style='font-size:1em'>c</text>
<text text-anchor='middle' x='192' y='4' fill='currentColor' style='font-size:1em'>o</text>
<text text-anchor='middle' x='200' y='4' fill='currentColor' style='font-size:1em'>l</text>
<text text-anchor='middle' x='208' y='4' fill='currentColor' style='font-size:1em'>l</text>
<text text-anchor='middle' x='216' y='4' fill='currentColor' style='font-size:1em'>a</text>
<text text-anchor='middle' x='224' y='4' fill='currentColor' style='font-size:1em'>t</text>
<text text-anchor='middle' x='232' y='4' fill='currentColor' style='font-size:1em'>i</text>
<text text-anchor='middle' x='240' y='4' fill='currentColor' style='font-size:1em'>o</text>
<text text-anchor='middle' x='248' y='4' fill='currentColor' style='font-size:1em'>n</text>
<text text-anchor='middle' x='264' y='4' fill='currentColor' style='font-size:1em'>o</text>
<text text-anchor='middle' x='272' y='4' fill='currentColor' style='font-size:1em'>f</text>
<text text-anchor='middle' x='288' y='4' fill='currentColor' style='font-size:1em'>'</text>
<text text-anchor='middle' x='296' y='4' fill='currentColor' style='font-size:1em'>e</text>
<text text-anchor='middle' x='304' y='4' fill='currentColor' style='font-size:1em'>n</text>
<text text-anchor='middle' x='312' y='4' fill='currentColor' style='font-size:1em'>_</text>
<text text-anchor='middle' x='320' y='4' fill='currentColor' style='font-size:1em'>U</text>
<text text-anchor='middle' x='328' y='4' fill='currentColor' style='font-size:1em'>S</text>
<text text-anchor='middle' x='336' y='4' fill='currentColor' style='font-size:1em'>.</text>
<text text-anchor='middle' x='344' y='4' fill='currentColor' style='font-size:1em'>u</text>
<text text-anchor='middle' x='352' y='4' fill='currentColor' style='font-size:1em'>t</text>
<text text-anchor='middle' x='360' y='4' fill='currentColor' style='font-size:1em'>f</text>
<text text-anchor='middle' x='368' y='4' fill='currentColor' style='font-size:1em'>8</text>
<text text-anchor='middle' x='376' y='4' fill='currentColor' style='font-size:1em'>'</text>
<text text-anchor='middle' x='384' y='4' fill='currentColor' style='font-size:1em'>.</text>
<text text-anchor='middle' x='400' y='4' fill='currentColor' style='font-size:1em'>S</text>
<text text-anchor='middle' x='408' y='4' fill='currentColor' style='font-size:1em'>h</text>
<text text-anchor='middle' x='416' y='4' fill='currentColor' style='font-size:1em'>o</text>
<text text-anchor='middle' x='424' y='4' fill='currentColor' style='font-size:1em'>u</text>
<text text-anchor='middle' x='432' y='4' fill='currentColor' style='font-size:1em'>l</text>
<text text-anchor='middle' x='440' y='4' fill='currentColor' style='font-size:1em'>d</text>
<text text-anchor='middle' x='456' y='4' fill='currentColor' style='font-size:1em'>b</text>
<text text-anchor='middle' x='464' y='4' fill='currentColor' style='font-size:1em'>e</text>
<text text-anchor='middle' x='480' y='4' fill='currentColor' style='font-size:1em'>'</text>
<text text-anchor='middle' x='488' y='4' fill='currentColor' style='font-size:1em'>C</text>
<text text-anchor='middle' x='496' y='4' fill='currentColor' style='font-size:1em'>'</text>
</g>
</svg>
</div>
<p><strong>Two ways to resolve:</strong></p>
<ul>
<li><strong>Preferred (clean)</strong>: Re-initialize the Postgres <strong>cluster</strong> with <code>C</code> and <code>UTF-8</code>:</li>
</ul>
<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:#75715e"># docker-compose.yml (excerpt for Postgres)</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_DB</span>: <span style="color:#ae81ff">synapse</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">POSTGRES_USER</span>: <span style="color:#ae81ff">synapse</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">POSTGRES_PASSWORD</span>: <span style="color:#ae81ff">&lt;strong-password&gt;</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">POSTGRES_INITDB_ARGS</span>: <span style="color:#e6db74">&#34;--locale=C --encoding=UTF8 --lc-collate=C --lc-ctype=C&#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">./pgdata:/var/lib/postgresql/data</span>
</span></span></code></pre></div><blockquote>
<p>Requires wiping the volume and recreating the DB.</p></blockquote>
<ul>
<li><strong>Pragmatic (works quickly)</strong>: In Synapses DB config, set <code>allow_unsafe_locale: true</code>. This bypasses the check. Its fine for hobby use; for production, prefer the clean <code>C</code> cluster.</li>
</ul>
<h3 id="2-start-synapse-and-generate-config">2) Start Synapse and generate config</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 compose up -d db synapse
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Logs</span>
</span></span><span style="display:flex;"><span>docker compose logs --tail<span style="color:#f92672">=</span><span style="color:#ae81ff">200</span> synapse
</span></span></code></pre></div><p>Ensure Synapse prints your <strong>server_name</strong> and <strong>public base URL</strong> and stays up.</p>
<h3 id="3-create-an-admin-user">3) Create an admin user</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><span style="color:#75715e"># Exec into the running Synapse container:</span>
</span></span><span style="display:flex;"><span>docker compose exec synapse register_new_matrix_user <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span> -c /data/homeserver.yaml -u &lt;username&gt; -p &lt;password&gt; <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span> -a -k
</span></span></code></pre></div><blockquote>
<p>If you see “Unknown execution mode”, you probably ran the binary with the wrong entrypoint. Use <code>docker compose exec synapse …</code> against the running container.</p></blockquote>
<hr>
<h2 id="turn-coturn">TURN (coTURN)</h2>
<h3 id="1-avoid-bad-inline-comments">1) Avoid bad inline comments</h3>
<p>If you see errors like:</p>
<div class="goat svg-container ">
<svg
xmlns="http://www.w3.org/2000/svg"
font-family="Menlo,Lucida Console,monospace"
viewBox="0 0 808 25"
>
<g transform='translate(8,16)'>
<text text-anchor='middle' x='0' y='4' fill='currentColor' style='font-size:1em'>E</text>
<text text-anchor='middle' x='8' y='4' fill='currentColor' style='font-size:1em'>R</text>
<text text-anchor='middle' x='16' y='4' fill='currentColor' style='font-size:1em'>R</text>
<text text-anchor='middle' x='24' y='4' fill='currentColor' style='font-size:1em'>O</text>
<text text-anchor='middle' x='32' y='4' fill='currentColor' style='font-size:1em'>R</text>
<text text-anchor='middle' x='40' y='4' fill='currentColor' style='font-size:1em'>:</text>
<text text-anchor='middle' x='56' y='4' fill='currentColor' style='font-size:1em'>U</text>
<text text-anchor='middle' x='64' y='4' fill='currentColor' style='font-size:1em'>n</text>
<text text-anchor='middle' x='72' y='4' fill='currentColor' style='font-size:1em'>k</text>
<text text-anchor='middle' x='80' y='4' fill='currentColor' style='font-size:1em'>n</text>
<text text-anchor='middle' x='88' y='4' fill='currentColor' style='font-size:1em'>o</text>
<text text-anchor='middle' x='96' y='4' fill='currentColor' style='font-size:1em'>w</text>
<text text-anchor='middle' x='104' y='4' fill='currentColor' style='font-size:1em'>n</text>
<text text-anchor='middle' x='120' y='4' fill='currentColor' style='font-size:1em'>b</text>
<text text-anchor='middle' x='128' y='4' fill='currentColor' style='font-size:1em'>o</text>
<text text-anchor='middle' x='136' y='4' fill='currentColor' style='font-size:1em'>o</text>
<text text-anchor='middle' x='144' y='4' fill='currentColor' style='font-size:1em'>l</text>
<text text-anchor='middle' x='152' y='4' fill='currentColor' style='font-size:1em'>e</text>
<text text-anchor='middle' x='160' y='4' fill='currentColor' style='font-size:1em'>a</text>
<text text-anchor='middle' x='168' y='4' fill='currentColor' style='font-size:1em'>n</text>
<text text-anchor='middle' x='184' y='4' fill='currentColor' style='font-size:1em'>v</text>
<text text-anchor='middle' x='192' y='4' fill='currentColor' style='font-size:1em'>a</text>
<text text-anchor='middle' x='200' y='4' fill='currentColor' style='font-size:1em'>l</text>
<text text-anchor='middle' x='208' y='4' fill='currentColor' style='font-size:1em'>u</text>
<text text-anchor='middle' x='216' y='4' fill='currentColor' style='font-size:1em'>e</text>
<text text-anchor='middle' x='224' y='4' fill='currentColor' style='font-size:1em'>:</text>
<text text-anchor='middle' x='240' y='4' fill='currentColor' style='font-size:1em'>#</text>
<text text-anchor='middle' x='256' y='4' fill='currentColor' style='font-size:1em'>l</text>
<text text-anchor='middle' x='264' y='4' fill='currentColor' style='font-size:1em'>o</text>
<text text-anchor='middle' x='272' y='4' fill='currentColor' style='font-size:1em'>g</text>
<text text-anchor='middle' x='288' y='4' fill='currentColor' style='font-size:1em'>t</text>
<text text-anchor='middle' x='296' y='4' fill='currentColor' style='font-size:1em'>o</text>
<text text-anchor='middle' x='312' y='4' fill='currentColor' style='font-size:1em'>j</text>
<text text-anchor='middle' x='320' y='4' fill='currentColor' style='font-size:1em'>o</text>
<text text-anchor='middle' x='328' y='4' fill='currentColor' style='font-size:1em'>u</text>
<text text-anchor='middle' x='336' y='4' fill='currentColor' style='font-size:1em'>r</text>
<text text-anchor='middle' x='344' y='4' fill='currentColor' style='font-size:1em'>n</text>
<text text-anchor='middle' x='352' y='4' fill='currentColor' style='font-size:1em'>a</text>
<text text-anchor='middle' x='360' y='4' fill='currentColor' style='font-size:1em'>l</text>
<text text-anchor='middle' x='368' y='4' fill='currentColor' style='font-size:1em'>d</text>
<text text-anchor='middle' x='376' y='4' fill='currentColor' style='font-size:1em'>/</text>
<text text-anchor='middle' x='384' y='4' fill='currentColor' style='font-size:1em'>s</text>
<text text-anchor='middle' x='392' y='4' fill='currentColor' style='font-size:1em'>y</text>
<text text-anchor='middle' x='400' y='4' fill='currentColor' style='font-size:1em'>s</text>
<text text-anchor='middle' x='408' y='4' fill='currentColor' style='font-size:1em'>l</text>
<text text-anchor='middle' x='416' y='4' fill='currentColor' style='font-size:1em'>o</text>
<text text-anchor='middle' x='424' y='4' fill='currentColor' style='font-size:1em'>g</text>
<text text-anchor='middle' x='432' y='4' fill='currentColor' style='font-size:1em'>.</text>
<text text-anchor='middle' x='448' y='4' fill='currentColor' style='font-size:1em'>Y</text>
<text text-anchor='middle' x='456' y='4' fill='currentColor' style='font-size:1em'>o</text>
<text text-anchor='middle' x='464' y='4' fill='currentColor' style='font-size:1em'>u</text>
<text text-anchor='middle' x='480' y='4' fill='currentColor' style='font-size:1em'>c</text>
<text text-anchor='middle' x='488' y='4' fill='currentColor' style='font-size:1em'>a</text>
<text text-anchor='middle' x='496' y='4' fill='currentColor' style='font-size:1em'>n</text>
<text text-anchor='middle' x='512' y='4' fill='currentColor' style='font-size:1em'>u</text>
<text text-anchor='middle' x='520' y='4' fill='currentColor' style='font-size:1em'>s</text>
<text text-anchor='middle' x='528' y='4' fill='currentColor' style='font-size:1em'>e</text>
<text text-anchor='middle' x='544' y='4' fill='currentColor' style='font-size:1em'>o</text>
<text text-anchor='middle' x='552' y='4' fill='currentColor' style='font-size:1em'>n</text>
<text text-anchor='middle' x='560' y='4' fill='currentColor' style='font-size:1em'>/</text>
<text text-anchor='middle' x='568' y='4' fill='currentColor' style='font-size:1em'>o</text>
<text text-anchor='middle' x='576' y='4' fill='currentColor' style='font-size:1em'>f</text>
<text text-anchor='middle' x='584' y='4' fill='currentColor' style='font-size:1em'>f</text>
<text text-anchor='middle' x='592' y='4' fill='currentColor' style='font-size:1em'>,</text>
<text text-anchor='middle' x='608' y='4' fill='currentColor' style='font-size:1em'>y</text>
<text text-anchor='middle' x='616' y='4' fill='currentColor' style='font-size:1em'>e</text>
<text text-anchor='middle' x='624' y='4' fill='currentColor' style='font-size:1em'>s</text>
<text text-anchor='middle' x='632' y='4' fill='currentColor' style='font-size:1em'>/</text>
<text text-anchor='middle' x='640' y='4' fill='currentColor' style='font-size:1em'>n</text>
<text text-anchor='middle' x='648' y='4' fill='currentColor' style='font-size:1em'>o</text>
<text text-anchor='middle' x='656' y='4' fill='currentColor' style='font-size:1em'>,</text>
<text text-anchor='middle' x='672' y='4' fill='currentColor' style='font-size:1em'>1</text>
<text text-anchor='middle' x='680' y='4' fill='currentColor' style='font-size:1em'>/</text>
<text text-anchor='middle' x='688' y='4' fill='currentColor' style='font-size:1em'>0</text>
<text text-anchor='middle' x='696' y='4' fill='currentColor' style='font-size:1em'>,</text>
<text text-anchor='middle' x='712' y='4' fill='currentColor' style='font-size:1em'>t</text>
<text text-anchor='middle' x='720' y='4' fill='currentColor' style='font-size:1em'>r</text>
<text text-anchor='middle' x='728' y='4' fill='currentColor' style='font-size:1em'>u</text>
<text text-anchor='middle' x='736' y='4' fill='currentColor' style='font-size:1em'>e</text>
<text text-anchor='middle' x='744' y='4' fill='currentColor' style='font-size:1em'>/</text>
<text text-anchor='middle' x='752' y='4' fill='currentColor' style='font-size:1em'>f</text>
<text text-anchor='middle' x='760' y='4' fill='currentColor' style='font-size:1em'>a</text>
<text text-anchor='middle' x='768' y='4' fill='currentColor' style='font-size:1em'>l</text>
<text text-anchor='middle' x='776' y='4' fill='currentColor' style='font-size:1em'>s</text>
<text text-anchor='middle' x='784' y='4' fill='currentColor' style='font-size:1em'>e</text>
<text text-anchor='middle' x='792' y='4' fill='currentColor' style='font-size:1em'>.</text>
</g>
</svg>
</div>
<p>…it means a <code>#</code> comment is on the <strong>same line</strong> as a boolean directive. Move comments to their own lines.</p>
<h3 id="2-minimal-turnserverconf">2) Minimal <code>turnserver.conf</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-ini" data-lang="ini"><span style="display:flex;"><span><span style="color:#a6e22e">listening-port</span><span style="color:#f92672">=</span><span style="color:#e6db74">3478</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">tls-listening-port</span><span style="color:#f92672">=</span><span style="color:#e6db74">5349</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">fingerprint</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">use-auth-secret</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">static-auth-secret</span><span style="color:#f92672">=</span><span style="color:#e6db74">&lt;shared-secret&gt; # also set in Synapse</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">realm</span><span style="color:#f92672">=</span><span style="color:#e6db74">example.com # used in creds generation</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">total-quota</span><span style="color:#f92672">=</span><span style="color:#e6db74">0</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">bps-capacity</span><span style="color:#f92672">=</span><span style="color:#e6db74">0</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">cli-password</span><span style="color:#f92672">=</span><span style="color:#e6db74">&lt;admin-pass&gt;</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">no-cli</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">cert</span><span style="color:#f92672">=</span><span style="color:#e6db74">/etc/letsencrypt/live/turn.example.com/fullchain.pem</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">pkey</span><span style="color:#f92672">=</span><span style="color:#e6db74">/etc/letsencrypt/live/turn.example.com/privkey.pem</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># If behind NAT:</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># external-ip=&lt;public-ip&gt;/&lt;internal-ip&gt;</span>
</span></span></code></pre></div><h3 id="3-wire-turn-into-synapse">3) Wire TURN into Synapse</h3>
<p>In <code>homeserver.yaml</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-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">turn_uris</span>:
</span></span><span style="display:flex;"><span> - <span style="color:#e6db74">&#34;turn:turn.example.com?transport=udp&#34;</span>
</span></span><span style="display:flex;"><span> - <span style="color:#e6db74">&#34;turn:turn.example.com?transport=tcp&#34;</span>
</span></span><span style="display:flex;"><span> - <span style="color:#e6db74">&#34;turns:turn.example.com:5349?transport=tcp&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">turn_shared_secret</span>: <span style="color:#e6db74">&#34;&lt;shared-secret&gt;&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">turn_user_lifetime</span>: <span style="color:#e6db74">&#34;1d&#34;</span>
</span></span></code></pre></div><p>Verify the homeserver issues TURN creds:</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>TOKEN<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;&lt;your matrix access token&gt;&#39;</span>
</span></span><span style="display:flex;"><span>curl -s -H <span style="color:#e6db74">&#34;Authorization: Bearer </span>$TOKEN<span style="color:#e6db74">&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span> https://matrix.example.com/_matrix/client/v3/voip/turnServer | jq
</span></span></code></pre></div><p>You should see <code>uris</code>, a time-limited <code>username</code> and <code>password</code>.</p>
<hr>
<h2 id="nginx-host-for-synapse-and-federation">Nginx (host) for Synapse and federation</h2>
<p>Create <code>/etc/nginx/conf.d/matrix.conf</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:#75715e"># Client traffic on 443
</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">443</span> <span style="color:#e6db74">ssl</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">matrix.example.com</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/matrix.example.com/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/matrix.example.com/privkey.pem</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#75715e"># Proxy client &amp; admin APIs to Synapse (container) on 8008
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> <span style="color:#f92672">location</span> ~ <span style="color:#e6db74">^(/_matrix|/_synapse/client)</span> {
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_pass</span> <span style="color:#e6db74">http://127.0.0.1:8008</span>;
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">X-Forwarded-For</span> $remote_addr;
</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 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">client_max_body_size</span> <span style="color:#e6db74">50M</span>;
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#75715e"># Advertise homeserver base + RTC focus (MSC4143) via .well-known
</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/matrix/client</span> {
</span></span><span style="display:flex;"><span> <span style="color:#f92672">default_type</span> <span style="color:#e6db74">application/json</span>;
</span></span><span style="display:flex;"><span> <span style="color:#f92672">add_header</span> <span style="color:#e6db74">Access-Control-Allow-Origin</span> <span style="color:#e6db74">&#34;*&#34;</span> <span style="color:#e6db74">always</span>;
</span></span><span style="display:flex;"><span> <span style="color:#f92672">return</span> <span style="color:#ae81ff">200</span> <span style="color:#e6db74">&#39;</span>{<span style="color:#f92672">&#34;m.homeserver&#34;:{&#34;base_url&#34;:&#34;https://matrix.example.com&#34;},&#34;org.matrix.msc4143.rtc_foci&#34;:[{&#34;type&#34;:&#34;livekit&#34;,&#34;livekit_service_url&#34;:&#34;https://rtc.example.com&#34;}]}&#39;</span>;
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Federation on 8448
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#f92672">server</span> {
</span></span><span style="display:flex;"><span> <span style="color:#f92672">listen</span> <span style="color:#ae81ff">8448</span> <span style="color:#e6db74">ssl</span> <span style="color:#e6db74">http2</span>; <span style="color:#f92672">listen</span> <span style="color:#e6db74">[::]:8448</span> <span style="color:#e6db74">ssl</span> <span style="color:#e6db74">http2</span>;
</span></span><span style="display:flex;"><span> <span style="color:#f92672">server_name</span> <span style="color:#e6db74">matrix.example.com</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/matrix.example.com/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/matrix.example.com/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:8008</span>;
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">X-Forwarded-For</span> $remote_addr;
</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 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">client_max_body_size</span> <span style="color:#e6db74">50M</span>;
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Reload and sanity check:</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 style="color:#f92672">&amp;&amp;</span> sudo systemctl reload nginx
</span></span><span style="display:flex;"><span>curl -s https://matrix.example.com/.well-known/matrix/client | jq
</span></span></code></pre></div><p>Also check Synapse supports RTC signaling (MSC4140) so clients actually use it:</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 -s https://matrix.example.com/_matrix/client/versions | jq <span style="color:#e6db74">&#39;.unstable_features.&#34;org.matrix.msc4140&#34;&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># expect: true</span>
</span></span></code></pre></div><hr>
<h2 id="element-call--livekit">Element Call + LiveKit</h2>
<p>Ill run <strong>LiveKit</strong> and the small <strong>JWT helper</strong> (Elements <code>elementcall_jwt</code>) in Docker. LiveKit handles media; the JWT helper mints access tokens for WebSocket connects.</p>
<h3 id="1-livekit-config-etclivekityaml-inside-container">1) LiveKit config (<code>/etc/livekit.yaml</code> inside container)</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-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">port</span>: <span style="color:#ae81ff">7880</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">bind_addresses</span>: [<span style="color:#e6db74">&#34;0.0.0.0&#34;</span>]
</span></span><span style="display:flex;"><span><span style="color:#f92672">rtc</span>:
</span></span><span style="display:flex;"><span> <span style="color:#f92672">tcp_port</span>: <span style="color:#ae81ff">7881</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">port_range_start</span>: <span style="color:#ae81ff">50100</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">port_range_end</span>: <span style="color:#ae81ff">50200</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">use_external_ip</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">logging</span>:
</span></span><span style="display:flex;"><span> <span style="color:#f92672">level</span>: <span style="color:#ae81ff">info</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">turn</span>:
</span></span><span style="display:flex;"><span> <span style="color:#f92672">enabled</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">keys</span>:
</span></span><span style="display:flex;"><span> <span style="color:#f92672">lk_prod_1</span>: <span style="color:#e6db74">&#34;REPLACE_WITH_A_64_CHAR_RANDOM_SECRET___________________________________&#34;</span>
</span></span></code></pre></div><blockquote>
<p><strong>Important:</strong> the secret must be <strong>&gt;= 32 chars</strong>. The default <code>devkey</code> will trigger <code>secret is too short</code> warnings and wont work with the JWT helper.</p></blockquote>
<h3 id="2-elementcall_jwt-env">2) elementcall_jwt env</h3>
<p>Run it with:</p>
<ul>
<li><code>LIVEKIT_URL=wss://rtc.example.com</code> <em>(root WS URL, <strong>no</strong> <code>/livekit/sfu</code> path)</em></li>
<li><code>LIVEKIT_KEY=lk_prod_1</code></li>
<li><code>LIVEKIT_SECRET=&lt;the long secret above&gt;</code></li>
<li><code>LIVEKIT_JWT_PORT=8080</code> (internal HTTP port the proxy will hit)</li>
<li>Optionally: <code>LIVEKIT_FULL_ACCESS_HOMESERVERS=*</code> during setup</li>
</ul>
<p>Check logs on start; it prints the LIVEKIT_URL it will advertise.</p>
<h3 id="3-nginx-host-for-rtcexamplecom">3) Nginx (host) for <code>rtc.example.com</code></h3>
<p>Create <code>/etc/nginx/conf.d/rtc.conf</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">listen</span> <span style="color:#ae81ff">443</span> <span style="color:#e6db74">ssl</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">rtc.example.com</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/rtc.example.com/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/rtc.example.com/privkey.pem</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">access_log</span> <span style="color:#e6db74">/var/log/nginx/rtc.access.log</span> <span style="color:#e6db74">combined</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#75715e"># 3a) JWT endpoint with clean CORS (avoid duplicate ACAO)
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> <span style="color:#f92672">location</span> = <span style="color:#e6db74">/sfu/get</span> {
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_hide_header</span> <span style="color:#e6db74">Access-Control-Allow-Origin</span>;
</span></span><span style="display:flex;"><span> <span style="color:#f92672">add_header</span> <span style="color:#e6db74">Access-Control-Allow-Origin</span> $http_origin <span style="color:#e6db74">always</span>;
</span></span><span style="display:flex;"><span> <span style="color:#f92672">add_header</span> <span style="color:#e6db74">Vary</span> <span style="color:#e6db74">&#34;Origin&#34;</span> <span style="color:#e6db74">always</span>;
</span></span><span style="display:flex;"><span> <span style="color:#f92672">add_header</span> <span style="color:#e6db74">Access-Control-Allow-Methods</span> <span style="color:#e6db74">&#34;POST,</span> <span style="color:#e6db74">OPTIONS&#34;</span> <span style="color:#e6db74">always</span>;
</span></span><span style="display:flex;"><span> <span style="color:#f92672">add_header</span> <span style="color:#e6db74">Access-Control-Allow-Headers</span> <span style="color:#e6db74">&#34;Accept,</span> <span style="color:#e6db74">Content-Type,</span> <span style="color:#e6db74">Content-Length,</span> <span style="color:#e6db74">Accept-Encoding,</span> <span style="color:#e6db74">X-CSRF-Token,</span> <span style="color:#e6db74">Authorization&#34;</span> <span style="color:#e6db74">always</span>;
</span></span><span style="display:flex;"><span> <span style="color:#f92672">if</span> <span style="color:#e6db74">(</span>$request_method = <span style="color:#e6db74">OPTIONS)</span> { <span style="color:#f92672">return</span> <span style="color:#ae81ff">204</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-Forwarded-Proto</span> $scheme;
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_pass</span> <span style="color:#e6db74">http://127.0.0.1:8070/sfu/get</span>; <span style="color:#75715e"># elementcall_jwt
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#75715e"># 3b) LiveKit WS &amp; HTTP (catch-all)
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> <span style="color:#f92672">location</span> <span style="color:#e6db74">/</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 style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">Sec-WebSocket-Protocol</span> $http_sec_websocket_protocol; <span style="color:#75715e"># &#34;livekit&#34;
</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">Origin</span> $http_origin;
</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-Forwarded-Proto</span> $scheme;
</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 style="color:#f92672">proxy_read_timeout</span> <span style="color:#e6db74">3600s</span>;
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_pass</span> <span style="color:#e6db74">http://127.0.0.1:7880</span>; <span style="color:#75715e"># livekit
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Reload and basic checks:</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 style="color:#f92672">&amp;&amp;</span> sudo systemctl reload nginx
</span></span><span style="display:flex;"><span><span style="color:#75715e"># JWT preflight (should return a single ACAO header)</span>
</span></span><span style="display:flex;"><span>curl -si -X OPTIONS https://rtc.example.com/sfu/get <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span> -H <span style="color:#e6db74">&#39;Origin: https://app.element.io&#39;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span> -H <span style="color:#e6db74">&#39;Access-Control-Request-Method: POST&#39;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span> -H <span style="color:#e6db74">&#39;Access-Control-Request-Headers: authorization, content-type&#39;</span> | sed -n <span style="color:#e6db74">&#39;1,30p&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Expected: one Access-Control-Allow-Origin and 200/204</span>
</span></span></code></pre></div><blockquote>
<p><strong>Why I did this:</strong> I initially had <strong>two</strong> <code>Access-Control-Allow-Origin</code> headers (one added by the upstream, one by Nginx). Browsers reject that with “Access-Control-Allow-Origin cannot contain more than one origin”, so the JWT response never reached the client. Fixing CORS fixed everything.</p></blockquote>
<hr>
<h2 id="the-waiting-for-media-debugging-story-how-i-found-it">The “waiting for media” debugging story (how I found it)</h2>
<p>Symptom: Element Call created rooms, but calls stayed on <strong>“waiting for media.”</strong></p>
<p>What worked:</p>
<ul>
<li><code>elementcall_jwt</code> could <strong>CreateRoom</strong> in LiveKit (seen in logs).</li>
<li>TURN creds endpoint returned time-limited credentials.</li>
</ul>
<p>What didnt appear:</p>
<ul>
<li>No <strong>HTTP 101</strong> lines in <code>rtc.access.log</code> → the <strong>browser never established a WebSocket</strong> to LiveKit.</li>
</ul>
<h3 id="step-1-prove-the-ws-vhost-works-even-without-auth">Step 1: prove the WS vhost works (even without auth)</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>curl -v --http1.1 <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span> -H <span style="color:#e6db74">&#39;Connection: Upgrade&#39;</span> -H <span style="color:#e6db74">&#39;Upgrade: websocket&#39;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span> -H <span style="color:#e6db74">&#39;Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==&#39;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span> -H <span style="color:#e6db74">&#39;Sec-WebSocket-Version: 13&#39;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span> https://rtc.example.com/rtc -o /dev/null
</span></span></code></pre></div><p>Result: I got a <code>401</code> (expected), but importantly I saw a log line in <code>rtc.access.log</code>. So <strong>Nginx WS proxying was fine</strong>.</p>
<h3 id="step-2-check-the-browser-console">Step 2: check the browser console</h3>
<p>The smoking gun in DevTools:</p>
<div class="goat svg-container ">
<svg
xmlns="http://www.w3.org/2000/svg"
font-family="Menlo,Lucida Console,monospace"
viewBox="0 0 672 41"
>
<g transform='translate(8,16)'>
<text text-anchor='middle' x='0' y='4' fill='currentColor' style='font-size:1em'>A</text>
<text text-anchor='middle' x='0' y='20' fill='currentColor' style='font-size:1em'>F</text>
<text text-anchor='middle' x='8' y='4' fill='currentColor' style='font-size:1em'>c</text>
<text text-anchor='middle' x='8' y='20' fill='currentColor' style='font-size:1em'>e</text>
<text text-anchor='middle' x='16' y='4' fill='currentColor' style='font-size:1em'>c</text>
<text text-anchor='middle' x='16' y='20' fill='currentColor' style='font-size:1em'>t</text>
<text text-anchor='middle' x='24' y='4' fill='currentColor' style='font-size:1em'>e</text>
<text text-anchor='middle' x='24' y='20' fill='currentColor' style='font-size:1em'>c</text>
<text text-anchor='middle' x='32' y='4' fill='currentColor' style='font-size:1em'>s</text>
<text text-anchor='middle' x='32' y='20' fill='currentColor' style='font-size:1em'>h</text>
<text text-anchor='middle' x='40' y='4' fill='currentColor' style='font-size:1em'>s</text>
<text text-anchor='middle' x='48' y='4' fill='currentColor' style='font-size:1em'>-</text>
<text text-anchor='middle' x='48' y='20' fill='currentColor' style='font-size:1em'>A</text>
<text text-anchor='middle' x='56' y='4' fill='currentColor' style='font-size:1em'>C</text>
<text text-anchor='middle' x='56' y='20' fill='currentColor' style='font-size:1em'>P</text>
<text text-anchor='middle' x='64' y='4' fill='currentColor' style='font-size:1em'>o</text>
<text text-anchor='middle' x='64' y='20' fill='currentColor' style='font-size:1em'>I</text>
<text text-anchor='middle' x='72' y='4' fill='currentColor' style='font-size:1em'>n</text>
<text text-anchor='middle' x='80' y='4' fill='currentColor' style='font-size:1em'>t</text>
<text text-anchor='middle' x='80' y='20' fill='currentColor' style='font-size:1em'>c</text>
<text text-anchor='middle' x='88' y='4' fill='currentColor' style='font-size:1em'>r</text>
<text text-anchor='middle' x='88' y='20' fill='currentColor' style='font-size:1em'>a</text>
<text text-anchor='middle' x='96' y='4' fill='currentColor' style='font-size:1em'>o</text>
<text text-anchor='middle' x='96' y='20' fill='currentColor' style='font-size:1em'>n</text>
<text text-anchor='middle' x='104' y='4' fill='currentColor' style='font-size:1em'>l</text>
<text text-anchor='middle' x='104' y='20' fill='currentColor' style='font-size:1em'>n</text>
<text text-anchor='middle' x='112' y='4' fill='currentColor' style='font-size:1em'>-</text>
<text text-anchor='middle' x='112' y='20' fill='currentColor' style='font-size:1em'>o</text>
<text text-anchor='middle' x='120' y='4' fill='currentColor' style='font-size:1em'>A</text>
<text text-anchor='middle' x='120' y='20' fill='currentColor' style='font-size:1em'>t</text>
<text text-anchor='middle' x='128' y='4' fill='currentColor' style='font-size:1em'>l</text>
<text text-anchor='middle' x='136' y='4' fill='currentColor' style='font-size:1em'>l</text>
<text text-anchor='middle' x='136' y='20' fill='currentColor' style='font-size:1em'>l</text>
<text text-anchor='middle' x='144' y='4' fill='currentColor' style='font-size:1em'>o</text>
<text text-anchor='middle' x='144' y='20' fill='currentColor' style='font-size:1em'>o</text>
<text text-anchor='middle' x='152' y='4' fill='currentColor' style='font-size:1em'>w</text>
<text text-anchor='middle' x='152' y='20' fill='currentColor' style='font-size:1em'>a</text>
<text text-anchor='middle' x='160' y='4' fill='currentColor' style='font-size:1em'>-</text>
<text text-anchor='middle' x='160' y='20' fill='currentColor' style='font-size:1em'>d</text>
<text text-anchor='middle' x='168' y='4' fill='currentColor' style='font-size:1em'>O</text>
<text text-anchor='middle' x='176' y='4' fill='currentColor' style='font-size:1em'>r</text>
<text text-anchor='middle' x='176' y='20' fill='currentColor' style='font-size:1em'>h</text>
<text text-anchor='middle' x='184' y='4' fill='currentColor' style='font-size:1em'>i</text>
<text text-anchor='middle' x='184' y='20' fill='currentColor' style='font-size:1em'>t</text>
<text text-anchor='middle' x='192' y='4' fill='currentColor' style='font-size:1em'>g</text>
<text text-anchor='middle' x='192' y='20' fill='currentColor' style='font-size:1em'>t</text>
<text text-anchor='middle' x='200' y='4' fill='currentColor' style='font-size:1em'>i</text>
<text text-anchor='middle' x='200' y='20' fill='currentColor' style='font-size:1em'>p</text>
<text text-anchor='middle' x='208' y='4' fill='currentColor' style='font-size:1em'>n</text>
<text text-anchor='middle' x='208' y='20' fill='currentColor' style='font-size:1em'>s</text>
<text text-anchor='middle' x='216' y='20' fill='currentColor' style='font-size:1em'>:</text>
<text text-anchor='middle' x='224' y='4' fill='currentColor' style='font-size:1em'>c</text>
<text text-anchor='middle' x='224' y='20' fill='currentColor' style='font-size:1em'>/</text>
<text text-anchor='middle' x='232' y='4' fill='currentColor' style='font-size:1em'>a</text>
<text text-anchor='middle' x='232' y='20' fill='currentColor' style='font-size:1em'>/</text>
<text text-anchor='middle' x='240' y='4' fill='currentColor' style='font-size:1em'>n</text>
<text text-anchor='middle' x='240' y='20' fill='currentColor' style='font-size:1em'>r</text>
<text text-anchor='middle' x='248' y='4' fill='currentColor' style='font-size:1em'>n</text>
<text text-anchor='middle' x='248' y='20' fill='currentColor' style='font-size:1em'>t</text>
<text text-anchor='middle' x='256' y='4' fill='currentColor' style='font-size:1em'>o</text>
<text text-anchor='middle' x='256' y='20' fill='currentColor' style='font-size:1em'>c</text>
<text text-anchor='middle' x='264' y='4' fill='currentColor' style='font-size:1em'>t</text>
<text text-anchor='middle' x='264' y='20' fill='currentColor' style='font-size:1em'>.</text>
<text text-anchor='middle' x='272' y='20' fill='currentColor' style='font-size:1em'>e</text>
<text text-anchor='middle' x='280' y='4' fill='currentColor' style='font-size:1em'>c</text>
<text text-anchor='middle' x='280' y='20' fill='currentColor' style='font-size:1em'>x</text>
<text text-anchor='middle' x='288' y='4' fill='currentColor' style='font-size:1em'>o</text>
<text text-anchor='middle' x='288' y='20' fill='currentColor' style='font-size:1em'>a</text>
<text text-anchor='middle' x='296' y='4' fill='currentColor' style='font-size:1em'>n</text>
<text text-anchor='middle' x='296' y='20' fill='currentColor' style='font-size:1em'>m</text>
<text text-anchor='middle' x='304' y='4' fill='currentColor' style='font-size:1em'>t</text>
<text text-anchor='middle' x='304' y='20' fill='currentColor' style='font-size:1em'>p</text>
<text text-anchor='middle' x='312' y='4' fill='currentColor' style='font-size:1em'>a</text>
<text text-anchor='middle' x='312' y='20' fill='currentColor' style='font-size:1em'>l</text>
<text text-anchor='middle' x='320' y='4' fill='currentColor' style='font-size:1em'>i</text>
<text text-anchor='middle' x='320' y='20' fill='currentColor' style='font-size:1em'>e</text>
<text text-anchor='middle' x='328' y='4' fill='currentColor' style='font-size:1em'>n</text>
<text text-anchor='middle' x='328' y='20' fill='currentColor' style='font-size:1em'>.</text>
<text text-anchor='middle' x='336' y='20' fill='currentColor' style='font-size:1em'>c</text>
<text text-anchor='middle' x='344' y='4' fill='currentColor' style='font-size:1em'>m</text>
<text text-anchor='middle' x='344' y='20' fill='currentColor' style='font-size:1em'>o</text>
<text text-anchor='middle' x='352' y='4' fill='currentColor' style='font-size:1em'>o</text>
<text text-anchor='middle' x='352' y='20' fill='currentColor' style='font-size:1em'>m</text>
<text text-anchor='middle' x='360' y='4' fill='currentColor' style='font-size:1em'>r</text>
<text text-anchor='middle' x='360' y='20' fill='currentColor' style='font-size:1em'>/</text>
<text text-anchor='middle' x='368' y='4' fill='currentColor' style='font-size:1em'>e</text>
<text text-anchor='middle' x='368' y='20' fill='currentColor' style='font-size:1em'>s</text>
<text text-anchor='middle' x='376' y='20' fill='currentColor' style='font-size:1em'>f</text>
<text text-anchor='middle' x='384' y='4' fill='currentColor' style='font-size:1em'>t</text>
<text text-anchor='middle' x='384' y='20' fill='currentColor' style='font-size:1em'>u</text>
<text text-anchor='middle' x='392' y='4' fill='currentColor' style='font-size:1em'>h</text>
<text text-anchor='middle' x='392' y='20' fill='currentColor' style='font-size:1em'>/</text>
<text text-anchor='middle' x='400' y='4' fill='currentColor' style='font-size:1em'>a</text>
<text text-anchor='middle' x='400' y='20' fill='currentColor' style='font-size:1em'>g</text>
<text text-anchor='middle' x='408' y='4' fill='currentColor' style='font-size:1em'>n</text>
<text text-anchor='middle' x='408' y='20' fill='currentColor' style='font-size:1em'>e</text>
<text text-anchor='middle' x='416' y='20' fill='currentColor' style='font-size:1em'>t</text>
<text text-anchor='middle' x='424' y='4' fill='currentColor' style='font-size:1em'>o</text>
<text text-anchor='middle' x='432' y='4' fill='currentColor' style='font-size:1em'>n</text>
<text text-anchor='middle' x='432' y='20' fill='currentColor' style='font-size:1em'>d</text>
<text text-anchor='middle' x='440' y='4' fill='currentColor' style='font-size:1em'>e</text>
<text text-anchor='middle' x='440' y='20' fill='currentColor' style='font-size:1em'>u</text>
<text text-anchor='middle' x='448' y='20' fill='currentColor' style='font-size:1em'>e</text>
<text text-anchor='middle' x='456' y='4' fill='currentColor' style='font-size:1em'>o</text>
<text text-anchor='middle' x='464' y='4' fill='currentColor' style='font-size:1em'>r</text>
<text text-anchor='middle' x='464' y='20' fill='currentColor' style='font-size:1em'>t</text>
<text text-anchor='middle' x='472' y='4' fill='currentColor' style='font-size:1em'>i</text>
<text text-anchor='middle' x='472' y='20' fill='currentColor' style='font-size:1em'>o</text>
<text text-anchor='middle' x='480' y='4' fill='currentColor' style='font-size:1em'>g</text>
<text text-anchor='middle' x='488' y='4' fill='currentColor' style='font-size:1em'>i</text>
<text text-anchor='middle' x='488' y='20' fill='currentColor' style='font-size:1em'>a</text>
<text text-anchor='middle' x='496' y='4' fill='currentColor' style='font-size:1em'>n</text>
<text text-anchor='middle' x='496' y='20' fill='currentColor' style='font-size:1em'>c</text>
<text text-anchor='middle' x='504' y='4' fill='currentColor' style='font-size:1em'>.</text>
<text text-anchor='middle' x='504' y='20' fill='currentColor' style='font-size:1em'>c</text>
<text text-anchor='middle' x='512' y='20' fill='currentColor' style='font-size:1em'>e</text>
<text text-anchor='middle' x='520' y='20' fill='currentColor' style='font-size:1em'>s</text>
<text text-anchor='middle' x='528' y='20' fill='currentColor' style='font-size:1em'>s</text>
<text text-anchor='middle' x='544' y='20' fill='currentColor' style='font-size:1em'>c</text>
<text text-anchor='middle' x='552' y='20' fill='currentColor' style='font-size:1em'>o</text>
<text text-anchor='middle' x='560' y='20' fill='currentColor' style='font-size:1em'>n</text>
<text text-anchor='middle' x='568' y='20' fill='currentColor' style='font-size:1em'>t</text>
<text text-anchor='middle' x='576' y='20' fill='currentColor' style='font-size:1em'>r</text>
<text text-anchor='middle' x='584' y='20' fill='currentColor' style='font-size:1em'>o</text>
<text text-anchor='middle' x='592' y='20' fill='currentColor' style='font-size:1em'>l</text>
<text text-anchor='middle' x='608' y='20' fill='currentColor' style='font-size:1em'>c</text>
<text text-anchor='middle' x='616' y='20' fill='currentColor' style='font-size:1em'>h</text>
<text text-anchor='middle' x='624' y='20' fill='currentColor' style='font-size:1em'>e</text>
<text text-anchor='middle' x='632' y='20' fill='currentColor' style='font-size:1em'>c</text>
<text text-anchor='middle' x='640' y='20' fill='currentColor' style='font-size:1em'>k</text>
<text text-anchor='middle' x='648' y='20' fill='currentColor' style='font-size:1em'>s</text>
<text text-anchor='middle' x='656' y='20' fill='currentColor' style='font-size:1em'>.</text>
</g>
</svg>
</div>
<p>The browser refused the JWT call due to duplicated <strong>ACAO</strong> headers, so no token → no WebSocket connect.</p>
<p><strong>Fix:</strong> in Nginx I added:</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">proxy_hide_header</span> <span style="color:#e6db74">Access-Control-Allow-Origin</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">add_header</span> <span style="color:#e6db74">Access-Control-Allow-Origin</span> $http_origin <span style="color:#e6db74">always</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">add_header</span> <span style="color:#e6db74">Vary</span> <span style="color:#e6db74">&#34;Origin&#34;</span> <span style="color:#e6db74">always</span>;
</span></span></code></pre></div><p>…and ensured no other <code>add_header</code> created duplicates. After that, <code>/sfu/get</code> succeeded and the WebSocket to <code>wss://rtc.example.com</code> immediately followed (I saw <strong>HTTP 101</strong> in the logs).</p>
<h3 id="step-3-confirm-livekit-side">Step 3: confirm LiveKit side</h3>
<p>Once the WS was up, LiveKit logs showed participants joining (not just <code>RoomService.CreateRoom</code>), and calls were established.</p>
<hr>
<h2 id="useful-verification-commands">Useful verification commands</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><span style="color:#75715e"># Synapse features (expect MSC4140 true)</span>
</span></span><span style="display:flex;"><span>curl -s https://matrix.example.com/_matrix/client/versions | jq <span style="color:#e6db74">&#39;.unstable_features.&#34;org.matrix.msc4140&#34;&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Well-known with RTC focus</span>
</span></span><span style="display:flex;"><span>curl -s https://matrix.example.com/.well-known/matrix/client | jq
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># TURN creds (with your access token)</span>
</span></span><span style="display:flex;"><span>curl -s -H <span style="color:#e6db74">&#34;Authorization: Bearer </span>$TOKEN<span style="color:#e6db74">&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span> https://matrix.example.com/_matrix/client/v3/voip/turnServer | jq
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># JWT health</span>
</span></span><span style="display:flex;"><span>curl -si -X POST https://rtc.example.com/sfu/get
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># LiveKit simple HTTP probe</span>
</span></span><span style="display:flex;"><span>curl -si https://rtc.example.com | head
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Nginx logs (look for 101 Switching Protocols when a call starts)</span>
</span></span><span style="display:flex;"><span>sudo tail -f /var/log/nginx/rtc.access.log | grep <span style="color:#e6db74">&#39; 101 &#39;</span>
</span></span></code></pre></div><hr>
<h2 id="common-pitfalls-i-hit-these-so-you-dont-have-to">Common pitfalls (I hit these so you dont have to)</h2>
<ul>
<li><strong>Host Nginx vs Docker Nginx:</strong> If you already run Nginx on the host, dont also bind 80/443/8448 in a Docker Nginx — youll get <code>bind() ... already in use</code> and restart loops. Use <strong>host</strong> Nginx to reverse proxy to containers.</li>
<li><strong>Nginx <code>http2</code> directive:</strong> Old Nginx may not support the <code>http2</code> directive on <code>listen</code>. Use <code>listen 443 ssl;</code> (and add <code>http2</code> if your version supports it).</li>
<li><strong>Certificate name mismatch:</strong> Make sure <code>rtc.example.com</code>s vhost uses a certificate <strong>for that exact hostname</strong> (initially I had the <code>matrix.*</code> cert on <code>rtc.*</code> and curl complained).</li>
<li><strong>Postgres collation:</strong> Either initialize the cluster with <code>C</code> or use <code>allow_unsafe_locale: true</code> in Synapse DB config to get running quickly.</li>
<li><strong>CORS duplication on <code>/sfu/get</code>:</strong> Only ONE <code>Access-Control-Allow-Origin</code> header. If the upstream adds it too, use <code>proxy_hide_header Access-Control-Allow-Origin;</code> on the Nginx location.</li>
<li><strong>Cloudflare:</strong> Use <strong>DNS-only</strong> for <code>rtc.*</code>. Proxies can interfere with WS and UDP paths.</li>
<li><strong>Firewall:</strong> Open the LiveKit UDP range and TURN ports on both v4 and v6.</li>
</ul>
<hr>
<h2 id="final-checklist-print-me">Final checklist (print me)</h2>
<ul>
<li><input disabled="" type="checkbox"> <code>https://matrix.example.com/.well-known/matrix/client</code> returns <strong>both</strong> <code>m.homeserver.base_url</code> and <code>org.matrix.msc4143.rtc_foci</code> pointing to <code>https://rtc.example.com</code>.</li>
<li><input disabled="" type="checkbox"> <code>/_matrix/client/versions</code> shows <code>&quot;org.matrix.msc4140&quot;: true</code>.</li>
<li><input disabled="" type="checkbox"> <code>/sfu/get</code> <strong>preflight</strong> returns <strong>one</strong> <code>Access-Control-Allow-Origin</code> and <strong>200/204</strong>.</li>
<li><input disabled="" type="checkbox"> Starting a call creates <strong>HTTP 101</strong> entries to <code>wss://rtc.example.com</code> in <code>rtc.access.log</code>.</li>
<li><input disabled="" type="checkbox"> LiveKit logs show <strong>participants joining</strong> (not just CreateRoom).</li>
<li><input disabled="" type="checkbox"> <code>/voip/turnServer</code> returns time-limited TURN credentials.</li>
<li><input disabled="" type="checkbox"> Cloudflare set to <strong>DNS-only</strong> for <code>rtc.*</code>. UFW allows 7881/tcp and your LiveKit UDP range.</li>
</ul>
<hr>
<h3 id="credits--tooling">Credits &amp; tooling</h3>
<ul>
<li>Matrix Synapse, coTURN, LiveKit, Element Call.</li>
<li><code>curl</code>, <code>jq</code>, <code>docker compose logs</code>, Nginx access logs. These are your best friends.</li>
<li>The debugging breakthrough was catching CORS errors in the browser console and looking for <strong>HTTP 101</strong> in Nginx logs.</li>
</ul>
<p>Happy calling! 🎉</p>
]]></content:encoded></item><item><title>Hello World</title><link>http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/posts/hello-world/</link><pubDate>Mon, 25 Aug 2025 08:53:38 +0000</pubDate><guid>http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/posts/hello-world/</guid><description>&lt;p>This is a test hello world post just to make sure everything works!&lt;/p></description><content:encoded>&lt;p>This is a test hello world post just to make sure everything works!&lt;/p>
</content:encoded></item></channel></rss>