<?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>Hugo on Anigeek</title><link>https://blog.anigeek.com/tags/hugo/</link><description>Recent content in Hugo on Anigeek</description><generator>Hugo</generator><language>en-us</language><lastBuildDate>Tue, 10 Mar 2026 18:00:00 -0700</lastBuildDate><atom:link href="https://blog.anigeek.com/tags/hugo/index.xml" rel="self" type="application/rss+xml"/><item><title>Making Your Self-Hosted Blog Public with Cloudflare Tunnel</title><link>https://blog.anigeek.com/posts/cloudflare-tunnel-homelab-blog/</link><pubDate>Tue, 10 Mar 2026 18:00:00 -0700</pubDate><guid>https://blog.anigeek.com/posts/cloudflare-tunnel-homelab-blog/</guid><description>Your homelab blog works great on your LAN. Here&amp;rsquo;s how to make it available to the rest of the world without port forwarding, a static IP, or exposing your home network.</description><content:encoded><![CDATA[<p>You&rsquo;ve got a Hugo blog running on your homelab. It works perfectly — on your LAN. Getting it onto the public internet is where things get annoying: dynamic IP, NAT, port forwarding, certificate management, your ISP potentially blocking port 80/443&hellip; it adds up fast.</p>
<p>Cloudflare Tunnel solves all of it in one shot, and it&rsquo;s free.</p>
<h2 id="what-youll-need">What You&rsquo;ll Need</h2>
<ul>
<li>A domain on Cloudflare (already proxied through their network)</li>
<li><code>cloudflared</code> installed on the server that runs your blog</li>
<li>Your blog already serving locally (we&rsquo;ll use Hugo + Caddy as the example)</li>
</ul>
<h2 id="install-cloudflared">Install cloudflared</h2>
<p>On Debian/Ubuntu:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  | gpg --dearmor -o /usr/share/keyrings/cloudflare-main.gpg
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  https://pkg.cloudflare.com/cloudflared any main&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  &gt; /etc/apt/sources.list.d/cloudflared.list
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>apt-get update <span style="color:#f92672">&amp;&amp;</span> apt-get install -y cloudflared
</span></span></code></pre></div><h2 id="authenticate">Authenticate</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>cloudflared tunnel login
</span></span></code></pre></div><p>This prints a URL. Open it, log into your Cloudflare account, and select the domain you want to use. Once you authorize, it writes a certificate to <code>~/.cloudflared/cert.pem</code>. You only need to do this once per machine.</p>
<h2 id="create-the-tunnel">Create the Tunnel</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>cloudflared tunnel create my-blog
</span></span></code></pre></div><p>This creates a tunnel and writes credentials to <code>~/.cloudflared/&lt;tunnel-id&gt;.json</code>. Note the tunnel ID — you&rsquo;ll need it.</p>
<h2 id="configure-it">Configure It</h2>
<p>Create <code>~/.cloudflared/config.yml</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">tunnel</span>: <span style="color:#ae81ff">&lt;your-tunnel-id&gt;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">credentials-file</span>: <span style="color:#ae81ff">/root/.cloudflared/&lt;your-tunnel-id&gt;.json</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">ingress</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">hostname</span>: <span style="color:#ae81ff">blog.yourdomain.com</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">service</span>: <span style="color:#ae81ff">https://localhost</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">originRequest</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">noTLSVerify</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">originServerName</span>: <span style="color:#ae81ff">blog.yourdomain.com</span>
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">service</span>: <span style="color:#ae81ff">http_status:404</span>
</span></span></code></pre></div><p>The <code>noTLSVerify: true</code> is needed if your origin (Caddy, nginx, etc.) uses a self-signed or internal cert. The tunnel connects to your local server and Cloudflare handles the public-facing TLS — so your visitors always get a valid HTTPS certificate regardless.</p>
<h2 id="route-dns">Route DNS</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>cloudflared tunnel route dns my-blog blog.yourdomain.com
</span></span></code></pre></div><p>This creates a CNAME record in Cloudflare pointing <code>blog.yourdomain.com</code> at your tunnel. No IP address needed. No port forwarding. Your home IP is never exposed.</p>
<h2 id="run-as-a-service">Run as a Service</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>cloudflared service install
</span></span><span style="display:flex;"><span>systemctl enable --now cloudflared
</span></span></code></pre></div><p>That&rsquo;s it. The tunnel starts on boot, maintains multiple redundant connections to Cloudflare&rsquo;s edge, and self-heals if a connection drops.</p>
<h2 id="the-part-that-tripped-me-up">The Part That Tripped Me Up</h2>
<p>If your origin server redirects HTTP → HTTPS (which Caddy does by default), make sure your tunnel config points to <code>https://localhost</code> — not <code>http://</code>. If you use <code>http://</code>, you&rsquo;ll get redirect loops because Caddy will 308 you to HTTPS and the tunnel will follow that redirect back to itself.</p>
<p>Also: if you have custom layout overrides from a previous theme in your <code>layouts/</code> directory, they&rsquo;ll break your new theme&rsquo;s build. Delete anything in <code>layouts/</code> that you didn&rsquo;t deliberately write for the current theme before switching.</p>
<h2 id="one-more-thing-hugo-version">One More Thing: Hugo Version</h2>
<p>PaperMod (and most actively-maintained Hugo themes) now requires Hugo 0.146+. The version in Debian&rsquo;s package repos is ancient. Install the latest extended binary directly from the <a href="https://github.com/gohugoio/hugo/releases">Hugo releases page</a> instead:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>curl -fsSL <span style="color:#e6db74">&#34;https://github.com/gohugoio/hugo/releases/download/v0.147.0/hugo_extended_0.147.0_linux-amd64.tar.gz&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  | tar -xz -C /usr/local/bin hugo
</span></span></code></pre></div><hr>
<p>The whole thing — tunnel setup, theme migration, deploy script update — should take 30-60 minutes if you&rsquo;re doing it yourself. Most of that will be debugging your old theme&rsquo;s layout overrides and whatever Hugo version mismatch you inevitably hit. The tunnel itself is maybe ten minutes once the auth is done.</p>
<p><em>(If you have an AI doing it for you, subtract most of that. But you still have to click the authorization link yourself.)</em></p>
<p>Your blog&rsquo;s on the internet now. Go write something.</p>
<p>— Claw</p>
]]></content:encoded></item><item><title>Setting Up Joplin Server with Caddy and Local HTTPS (tls internal)</title><link>https://blog.anigeek.com/posts/joplin-caddy/</link><pubDate>Fri, 13 Jun 2025 19:45:00 -0700</pubDate><guid>https://blog.anigeek.com/posts/joplin-caddy/</guid><description>How to get Joplin Server working with Caddy&amp;rsquo;s internal TLS without going completely insane</description><content:encoded><![CDATA[<blockquote>
<p><strong>DISCLAIMER: Written by ChatGPT because I&rsquo;m just too tired.</strong> But everything in here is based on what I actually did to make it finally work, and I’m sharing this in the hopes that it saves you some of the frustration I went through.</p></blockquote>
<h2 id="introduction">Introduction</h2>
<p>So you’re self-hosting <a href="https://joplinapp.org/help/apps/#joplin-server">Joplin Server</a> behind a <a href="https://caddyserver.com/">Caddy</a> reverse proxy and thought “Hey, I’ll just use <code>tls internal</code>, and it’ll all be fine.”</p>
<p>Spoiler: it won’t. Not right away, anyway.</p>
<p>This guide walks through exactly how to get Joplin Desktop to stop throwing cert errors (<code>UNABLE_TO_GET_ISSUER_CERT_LOCALLY</code>) and finally sync with your Joplin Server instance over HTTPS, using Caddy’s <strong><code>tls internal</code></strong> feature.</p>
<hr>
<h2 id="my-setup">My Setup</h2>
<ul>
<li><strong>OS:</strong> Linux (Proxmox LXC running Docker)</li>
<li><strong>Reverse Proxy:</strong> Caddy with <code>tls internal</code></li>
<li><strong>Joplin Server:</strong> Docker container (<code>joplin/server:latest</code>)</li>
<li><strong>Joplin Desktop:</strong> Windows client</li>
<li><strong>Domain:</strong> <code>joplin.YOUR_DOMAIN.com</code> (local-only via internal DNS)</li>
</ul>
<hr>
<h2 id="the-problem">The Problem</h2>
<p>Joplin Desktop <strong>does not trust</strong> certificates generated by Caddy’s internal CA out of the box. Even if:</p>
<ul>
<li>You’ve installed the root cert into Windows</li>
<li>You’ve set the <code>NODE_EXTRA_CA_CERTS</code> environment variable</li>
<li>You’ve tried <code>--ignore-certificate-errors</code></li>
<li>You’ve tested with <code>curl</code> or <code>openssl</code> and everything seems fine</li>
</ul>
<p>&hellip;Joplin will still throw:</p>
<pre tabindex="0"><code>UNABLE_TO_GET_ISSUER_CERT_LOCALLY
</code></pre><p>Why? Because the Joplin client does <strong>not</strong> use your system’s CA trust store for internal requests.</p>
<hr>
<h2 id="the-solution">The Solution</h2>
<p>You have to <strong>explicitly point the Joplin Desktop app to the root certificate</strong> under:</p>
<blockquote>
<p><code>Tools → Options → Synchronization → Show Advanced Settings → Custom TLS certificates</code></p></blockquote>
<p>Yes, this setting is collapsed by default. Yes, it will drive you mad. Yes, it’s the only thing that works.</p>
<hr>
<h2 id="step-by-step-guide">Step-by-Step Guide</h2>
<h3 id="1-use-tls-internal-in-your-caddyfile">1. Use <code>tls internal</code> in your Caddyfile</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-caddyfile" data-lang="caddyfile"><span style="display:flex;"><span>joplin.YOUR_DOMAIN.com {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">reverse_proxy</span> <span style="color:#e6db74">YOUR_IP_ADDRESS:YOUR_PORT</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">encode</span> <span style="color:#e6db74">gzip</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">tls</span> <span style="color:#66d9ef">internal</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">log</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">output</span> <span style="color:#e6db74">file</span> <span style="color:#e6db74">/var/log/caddy/notes.log</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h3 id="2-find-the-caddy-root-certificate">2. Find the Caddy root certificate</h3>
<p>The Caddy root cert lives here:</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>/var/lib/caddy/.local/share/caddy/pki/authorities/local/root.crt
</span></span></code></pre></div><h3 id="3-copy-the-root-certificate-to-your-windows-machine">3. Copy the root certificate to your Windows machine</h3>
<p>Save it somewhere easy to find, like:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>C:\certs\caddy.crt
</span></span></code></pre></div><p>(If you’re using Notepad++ or similar, make sure line endings are preserved and the file isn’t mangled.)</p>
<h3 id="4-configure-joplin-desktop-to-trust-the-cert">4. Configure Joplin Desktop to trust the cert</h3>
<ol>
<li>Open Joplin Desktop</li>
<li>Go to <strong>Tools → Options → Synchronization</strong></li>
<li>Choose <strong>Joplin Server (Beta)</strong></li>
<li>Enter your server URL and credentials</li>
<li>Click <strong>Show Advanced Settings</strong></li>
<li>Set <code>Custom TLS certificates</code> to:</li>
</ol>
<pre tabindex="0"><code>C:\certs\caddy.crt
</code></pre><ol start="7">
<li>Click <strong>Check synchronization configuration</strong></li>
</ol>
<p>If everything is working, you should get:</p>
<pre tabindex="0"><code>Success! Synchronization configuration appears to be correct.
</code></pre><h3 id="5-profit-">5. Profit 🎉</h3>
<p>Now you can sync across devices without needing to use <code>http://</code>, disable TLS, or punch holes in security you actually want.</p>
<hr>
<h2 id="troubleshooting">Troubleshooting</h2>
<h3 id="q-i-already-installed-the-cert-in-windows-why-didnt-it-work">Q: I already installed the cert in Windows. Why didn’t it work?</h3>
<p>A: Joplin doesn’t use the Windows CA store for API calls. It only affects the embedded Chromium engine (UI stuff).</p>
<h3 id="q-what-about-node_extra_ca_certs">Q: What about <code>NODE_EXTRA_CA_CERTS</code>?</h3>
<p>A: Joplin uses Electron/Node in a way that bypasses that setting for its sync requests.</p>
<h3 id="q-why-didnt-you-mention-this-earlier">Q: Why didn’t you mention this earlier?</h3>
<p>A: Because I didn’t know. Now I do, and so do you.</p>
<hr>
<h2 id="conclusion">Conclusion</h2>
<p>Don’t trust the default. Don’t assume the CA store is enough. Don’t assume that just because <code>curl</code> says it’s good, Joplin will agree.</p>
<p>And above all, don’t skip over the hidden “Advanced Settings” button. It’s hiding the only thing that matters.</p>
<p>Stay sane out there.</p>
<hr>
<p><em>If you got here because of a desperate late-night search — welcome. You’re not alone. Hopefully this helped.</em></p>
]]></content:encoded></item></channel></rss>