<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Containers | AkitaOnRails.com</title><link>https://akitaonrails.github.io/en/tags/containers/</link><description>Fabio Akita's blog — tech, career, and assorted geek topics. English edition.</description><generator>Hugo -- gohugo.io</generator><language>en</language><lastBuildDate>Mon, 19 Oct 2026 23:20:51 +0000</lastBuildDate><atom:link href="https://akitaonrails.github.io/en/tags/containers/index.xml" rel="self" type="application/rss+xml"/><item><title>AI-Jail: Security Update, Docker Goes Opt-In</title><link>https://akitaonrails.github.io/en/2026/07/25/ai-jail-security-update-docker-opt-in/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2026/07/25/ai-jail-security-update-docker-opt-in/</guid><pubDate>Sat, 25 Jul 2026 13:00:00 -0300</pubDate><description>&lt;p&gt;Someone opened an important issue on the &lt;a href="https://github.com/akitaonrails/ai-jail"target="_blank" rel="noopener"&gt;ai-jail&lt;/a&gt; repo this morning: &lt;a href="https://github.com/akitaonrails/ai-jail/issues/88"target="_blank" rel="noopener"&gt;issue #88&lt;/a&gt;, reported by &lt;a href="https://github.com/mdindoffer"target="_blank" rel="noopener"&gt;@mdindoffer&lt;/a&gt;, titled &amp;ldquo;Sandbox escape via a docker socket passthrough (effective host root)&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;The report checked out, and the fix is already available in &lt;strong&gt;v1.16.0&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;Update to v1.16.0&lt;span class="hx:absolute hx:-mt-20" id="update-to-v1160"&gt;&lt;/span&gt;
&lt;a href="#update-to-v1160" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;If you use ai-jail, the update is the usual drill:&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Arch Linux (AUR)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;yay -Syu ai-jail-bin
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Homebrew (macOS / Linux)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;brew update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; brew upgrade ai-jail
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# crates.io&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cargo install ai-jail --force
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# mise&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mise cache clear &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; mise upgrade github:akitaonrails/ai-jail&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2&gt;What changed in v1.16.0&lt;span class="hx:absolute hx:-mt-20" id="what-changed-in-v1160"&gt;&lt;/span&gt;
&lt;a href="#what-changed-in-v1160" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Up through v1.15.x, ai-jail mounted the Docker socket inside the jail automatically whenever &lt;code&gt;/var/run/docker.sock&lt;/code&gt; existed on the host. Read-write. No warning, no flag, no asking for your opinion. I documented this in the README as &amp;ldquo;favors usability,&amp;rdquo; and it was true: a coding agent often needs to run &lt;code&gt;docker compose&lt;/code&gt; to bring up a test database, and the automatic passthrough saved some configuration.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Someone opened an important issue on the <a href="https://github.com/akitaonrails/ai-jail"target="_blank" rel="noopener">ai-jail</a> repo this morning: <a href="https://github.com/akitaonrails/ai-jail/issues/88"target="_blank" rel="noopener">issue #88</a>, reported by <a href="https://github.com/mdindoffer"target="_blank" rel="noopener">@mdindoffer</a>, titled &ldquo;Sandbox escape via a docker socket passthrough (effective host root)&rdquo;.</p>
<p>The report checked out, and the fix is already available in <strong>v1.16.0</strong>.</p>
<h2>Update to v1.16.0<span class="hx:absolute hx:-mt-20" id="update-to-v1160"></span>
    <a href="#update-to-v1160" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>If you use ai-jail, the update is the usual drill:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Arch Linux (AUR)</span>
</span></span><span class="line"><span class="cl">yay -Syu ai-jail-bin
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Homebrew (macOS / Linux)</span>
</span></span><span class="line"><span class="cl">brew update <span class="o">&amp;&amp;</span> brew upgrade ai-jail
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># crates.io</span>
</span></span><span class="line"><span class="cl">cargo install ai-jail --force
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># mise</span>
</span></span><span class="line"><span class="cl">mise cache clear <span class="o">&amp;&amp;</span> mise upgrade github:akitaonrails/ai-jail</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h2>What changed in v1.16.0<span class="hx:absolute hx:-mt-20" id="what-changed-in-v1160"></span>
    <a href="#what-changed-in-v1160" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Up through v1.15.x, ai-jail mounted the Docker socket inside the jail automatically whenever <code>/var/run/docker.sock</code> existed on the host. Read-write. No warning, no flag, no asking for your opinion. I documented this in the README as &ldquo;favors usability,&rdquo; and it was true: a coding agent often needs to run <code>docker compose</code> to bring up a test database, and the automatic passthrough saved some configuration.</p>
<p>Starting with v1.16.0, the behavior flipped:</p>
<ul>
<li>Socket passthrough is now <strong>off by default</strong>. It only goes in if you explicitly ask for it with the <code>--docker</code> flag or <code>no_docker = false</code> in <code>.ai-jail</code>.</li>
<li>When you enable it and a host socket exists, ai-jail prints a launch warning spelling out that this amounts to giving host root to the process inside the jail.</li>
<li><code>ai-jail status</code> now shows Docker as <code>disabled (default)</code>, so nobody thinks it got turned on by accident.</li>
<li>In <code>--lockdown</code> and browser profile modes the socket never goes in, same as before.</li>
</ul>
<p>A behavior change, yes, and a deliberate one. If your workflow depends on Docker inside the jail (mine does, in a few projects), opting in is one line in the project&rsquo;s <code>.ai-jail</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="cl"><span class="nx">no_docker</span> <span class="p">=</span> <span class="kc">false</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>The field name is ugly (<code>no_docker = false</code> to turn it on, I know), but old configs keep parsing the same way, and <code>--no-docker</code> / <code>no_docker = true</code> work as before. Only the default changed. The details are in the <a href="https://github.com/akitaonrails/ai-jail/blob/master/releases/v1.16.0.md"target="_blank" rel="noopener">v1.16.0 release notes</a>.</p>
<h2>What issue #88 showed<span class="hx:absolute hx:-mt-20" id="what-issue-88-showed"></span>
    <a href="#what-issue-88-showed" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>mdindoffer&rsquo;s report is the kind every maintainer wants to get: precise summary, a repro in a few lines, a fix proposal. The gist:</p>
<p>ai-jail was mounting the <strong>raw</strong> Docker socket inside the sandbox, read-write. The Docker daemon runs as root on the host. So an agent inside the jail could run this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker run --rm -v /:/host alpine sh -c <span class="s1">&#39;cat /host/etc/shadow&#39;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>And that&rsquo;s the ballgame. Read any file on the host, as root. It defeats the tmpfs <code>$HOME</code>, <code>--mask</code>, <code>--deny-path</code>, and Landlock in one move, because the action no longer happens inside the sandbox: it happens in the daemon, which lives outside and above any namespace bwrap created. The agent doesn&rsquo;t even need to escape the jail when the jail has a door straight into the engine room.</p>
<p>In retrospect, shipping this on by default was a design mistake. I knew the passthrough was &ldquo;dangerous&rdquo; in the abstract, and I wrote as much in the README. What I hadn&rsquo;t internalized: dangerous like this, with this default, is a vulnerability.</p>
<p>mdindoffer also proposed the definitive hardening path: instead of mounting the raw socket, put a filtered proxy in front of it (in the style of <a href="https://github.com/wollomatic/socket-proxy"target="_blank" rel="noopener">wollomatic/socket-proxy</a>) that only accepts bind mounts from paths the agent can already write to inside the jail. It&rsquo;s on the radar for a future release. To close the hole now, opt-in with an explicit warning fixes the default, which is where the problem lived.</p>
<h2>The vulnerability class: docker.sock is root<span class="hx:absolute hx:-mt-20" id="the-vulnerability-class-dockersock-is-root"></span>
    <a href="#the-vulnerability-class-dockersock-is-root" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>This is not the first time I&rsquo;ve run into this story, and I&rsquo;d bet it isn&rsquo;t yours either. &ldquo;Whoever has access to the Docker socket has root on the host&rdquo; is one of the classics of container security, documented by Docker itself on the <a href="https://docs.docker.com/engine/security/"target="_blank" rel="noopener">daemon attack surface</a> page: only trusted users should control the daemon, because Docker lets you share any host directory with a container, with no access restriction whatsoever.</p>
<p>The technical reason is simple. <code>dockerd</code> is a daemon that runs as root and obeys commands arriving through the API on the <code>/var/run/docker.sock</code> Unix socket. The <code>docker</code> CLI is just a client of that API. When you ask for <code>docker run -v /:/host</code>, the daemon is the one creating the container and mounting the host&rsquo;s entire filesystem inside it, with full privilege. And a process inside a container runs as uid 0 by default, which the kernel sees as real uid 0 (barring user namespace remapping, which almost nobody turns on). The math checks out: write access to the socket equals root on the host. The <code>docker</code> group is passwordless sudo by another name.</p>
<h2>The demo, on your machine<span class="hx:absolute hx:-mt-20" id="the-demo-on-your-machine"></span>
    <a href="#the-demo-on-your-machine" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>If you have Docker installed and your user in the <code>docker</code> group, reproduce it right now. No sudo, no exploiting any bug:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># confirm you&#39;re a regular user</span>
</span></span><span class="line"><span class="cl">$ id
</span></span><span class="line"><span class="cl"><span class="nv">uid</span><span class="o">=</span>1000<span class="o">(</span>akitaonrails<span class="o">)</span> <span class="nv">gid</span><span class="o">=</span>1000<span class="o">(</span>akitaonrails<span class="o">)</span> <span class="nv">groups</span><span class="o">=</span>...,docker
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># try reading /etc/shadow directly: denied, as expected</span>
</span></span><span class="line"><span class="cl">$ cat /etc/shadow
</span></span><span class="line"><span class="cl">cat: /etc/shadow: Permission denied
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># now ask the daemon to do it for you</span>
</span></span><span class="line"><span class="cl">$ docker run --rm -v /:/host alpine sh -c <span class="s1">&#39;head -3 /host/etc/shadow&#39;</span>
</span></span><span class="line"><span class="cl">root:<span class="nv">$6</span>$...:...
</span></span><span class="line"><span class="cl">bin:!:...
</span></span><span class="line"><span class="cl">daemon:!:...</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>And if you want the whole nine yards, a root shell on your own host:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">$ docker run --rm -it -v /:/host alpine chroot /host /bin/bash
</span></span><span class="line"><span class="cl"><span class="c1"># id</span>
</span></span><span class="line"><span class="cl"><span class="nv">uid</span><span class="o">=</span>0<span class="o">(</span>root<span class="o">)</span> <span class="nv">gid</span><span class="o">=</span>0<span class="o">(</span>root<span class="o">)</span> <span class="nv">groups</span><span class="o">=</span>0<span class="o">(</span>root<span class="o">)</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>No exploit, no 0-day. You used the official API, the documented way, and went from regular user to root in one command. That&rsquo;s exactly what an agent inside ai-jail could do until today, even with every layer (bwrap, Landlock, seccomp, rlimits) turned on.</p>
<h2>Why does this still exist?<span class="hx:absolute hx:-mt-20" id="why-does-this-still-exist"></span>
    <a href="#why-does-this-still-exist" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>If everyone has known about this for over a decade, why hasn&rsquo;t anyone &ldquo;fixed&rdquo; it? Short answer: this is architecture, and architecture doesn&rsquo;t get patched.</p>
<ul>
<li>The root daemon with an all-powerful API was the design that made Docker simple to operate. Granular per-request authorization exists in the form of <a href="https://docs.docker.com/engine/extend/plugins_authorization/"target="_blank" rel="noopener">authorization plugins</a>, but it&rsquo;s opt-in, annoying to configure, and I can count on one hand the setups I&rsquo;ve seen using it.</li>
<li><a href="https://docs.docker.com/engine/security/userns-remap/"target="_blank" rel="noopener">userns-remap</a> has been around since Docker 1.10 and maps container root to an unprivileged user on the host. It ships turned off, because it breaks compatibility with images and volumes that assume uid 0.</li>
<li><a href="https://docs.docker.com/engine/security/rootless/"target="_blank" rel="noopener">Rootless mode</a> runs the whole daemon as your user, with the socket at <code>$XDG_RUNTIME_DIR/docker.sock</code>. It works, but it has network and storage limitations, and the entire internet of tutorials assumes the root daemon at the classic path.</li>
<li><a href="https://podman.io/"target="_blank" rel="noopener">Podman</a> was born rootless and daemonless precisely because of this criticism. There&rsquo;s a whole section about it below.</li>
</ul>
<p>Bottom line: this is here to stay. Every tool that mounts <code>/var/run/docker.sock</code> into an environment &ldquo;for convenience&rdquo; opens the same hole, knowingly or not. CI mounting the socket for image builds, remote IDEs, code-server, AI agent sandboxes (hi, me), web dashboard plugins. The fix always lives on the side of whoever builds the environment.</p>
<h2>Fun fact: this is anything but new<span class="hx:absolute hx:-mt-20" id="fun-fact-this-is-anything-but-new"></span>
    <a href="#fun-fact-this-is-anything-but-new" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>If you&rsquo;ve been following me for a while, this whole story should give you déjà vu. Back in 2023 I recorded <a href="/2023/03/02/akitando-139-entendendo-como-containers-funcionam/">[Akitando #139] - Understanding How Containers Work</a>, where I explain what a container actually is: an ordinary Linux process, throttled by cgroups, fooled by namespaces, with its capabilities trimmed. No magic and no virtual machine. And look at what was already sitting in that episode&rsquo;s link list: a walkthrough of <a href="https://flast101.github.io/docker-privesc/"target="_blank" rel="noopener">privilege escalation via Docker</a>, demonstrating the exact <code>docker run -v /:/host</code> trick. The hole issue #88 exploited inside ai-jail is the same one I was already pointing at in a 2023 video, and it was old news long before that.</p>
<p>Nobody knows this better than Red Hat. <a href="https://podman.io/"target="_blank" rel="noopener">Podman</a> was born there in 2018 as a direct answer to that architecture: no central daemon, rootless by default. Every container becomes a direct child of your user, via fork-exec, with no all-powerful process running as root brokering anything. The whole &ldquo;docker.sock is root&rdquo; problem doesn&rsquo;t exist in that model, because there&rsquo;s no docker.sock, no daemon, and no root.</p>
<p>And before you ask: yes, I run Podman for some things and I recommend it. But it&rsquo;s no perfect solution either, and it&rsquo;s worth understanding why.</p>
<p>What Podman does well:</p>
<ul>
<li><strong>The right architecture.</strong> Daemonless and rootless from day zero. The entire vulnerability class from this article stops making sense.</li>
<li><strong>CLI compatibility.</strong> <code>alias docker=podman</code> covers the overwhelming majority of day-to-day commands. Build, run, push, pull: same commands, same flags.</li>
<li><strong>systemd integration.</strong> <a href="https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html"target="_blank" rel="noopener">Quadlets</a> are, in my opinion, the cleanest way to run a container as a service on Linux. Docker never came close.</li>
</ul>
<p>Where it stumbles:</p>
<ul>
<li><strong>The compatibility socket isn&rsquo;t 100%.</strong> Podman offers a socket compatible with the Docker API (<code>podman.socket</code>), and plenty of tools work on top of it. But &ldquo;plenty&rdquo; isn&rsquo;t &ldquo;all&rdquo;: Testcontainers, some IDE plugins, more exotic CI tooling trip over behavioral differences. It works until the day it doesn&rsquo;t, and then you lose an afternoon debugging.</li>
<li><strong>Compose is a second-class citizen.</strong> The official <code>docker compose</code> does talk to the Podman socket, and <code>podman-compose</code> exists, but neither has the polish of the original pairing. Projects with a complicated compose file are where migrations usually get stuck.</li>
<li><strong>Rootless has a price.</strong> Rootless networking (slirp4netns, and pasta these days) has limits: no ping by default, weird source IPs, lower throughput. Images that assume uid 0 and volumes with wrong permissions need fiddling. Nothing fatal, but it&rsquo;s friction.</li>
<li><strong>Docker Desktop is a product.</strong> On macOS and Windows, Docker Desktop delivers a polished experience that Podman Desktop is still catching up to. For a lot of people, that&rsquo;s the only contact with containers they&rsquo;ll ever have.</li>
</ul>
<p>Add it all up and you get the answer to why the world stays on Docker: ecosystem inertia. Every tutorial, every CI pipeline, every example image, every <code>docker run</code> pasted from Stack Overflow assumes the root daemon at the classic path. Docker became the name of the category, the Kleenex of containers. Moving to Podman is technically easy and politically expensive: it&rsquo;s you against the accumulated knowledge of the entire internet.</p>
<p>One detail that matters for sandbox users: mounting the rootless Podman socket inside a jail is far less catastrophic than mounting the Docker one. The equivalent &ldquo;daemon&rdquo; runs as your user, so a malicious agent would gain your privileges, not root. Still bad (it could overwrite your <code>~/.ssh</code>, for one), but a whole different ballgame. Even so, ai-jail&rsquo;s default stands: don&rsquo;t mount any socket, from any runtime. Opt-in is opt-in.</p>
<h2>Best practices so this doesn&rsquo;t bite you<span class="hx:absolute hx:-mt-20" id="best-practices-so-this-doesnt-bite-you"></span>
    <a href="#best-practices-so-this-doesnt-bite-you" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The list I apply and recommend:</p>
<ol>
<li><strong>Treat the <code>docker</code> group as passwordless sudo.</strong> Before adding any user or service to it, ask whether you&rsquo;d give that thing unrestricted sudo. Same thing.</li>
<li><strong>Never mount <code>/var/run/docker.sock</code> into untrusted environments.</strong> AI agents, CI jobs running code from a stranger&rsquo;s pull request, third-party containers. Run <code>grep -r docker.sock</code> over your docker-compose files, CI manifests, and tool configs. It shows up in more places than you remember.</li>
<li><strong>Need to expose it to something semi-trusted? Use a socket proxy.</strong> <a href="https://github.com/wollomatic/socket-proxy"target="_blank" rel="noopener">wollomatic/socket-proxy</a> and <a href="https://github.com/Tecnativa/docker-socket-proxy"target="_blank" rel="noopener">Tecnativa&rsquo;s docker-socket-proxy</a> sit between the client and the daemon with an endpoint allowlist and blocking of arbitrary bind mounts. Read-only by default; you enable only what you need.</li>
<li><strong>Prefer rootless whenever possible.</strong> Rootless Podman on Linux is the cleanest path; Docker&rsquo;s own rootless mode is the second option. The daemon stops being root and this entire class of problem loses its bite.</li>
<li><strong>In CI, build images without a privileged daemon.</strong> <a href="https://github.com/GoogleContainerTools/kaniko"target="_blank" rel="noopener">Kaniko</a> and <a href="https://buildah.io/"target="_blank" rel="noopener">Buildah</a> build images rootless, with no socket mounted in the job at all.</li>
<li><strong>Can&rsquo;t go rootless? Turn on userns-remap.</strong> It costs an afternoon of testing with your volumes and buys real isolation between container root and host root.</li>
<li><strong>In ai-jail, let the default work for you.</strong> Docker off, and <code>--docker</code> only in projects where you trust the workload the way you&rsquo;d trust a sudo. Rule of thumb: if you wouldn&rsquo;t blindly hand <code>sudo</code> to the agent in that directory, don&rsquo;t hand it <code>--docker</code> either.</li>
</ol>
<h2>Conclusion<span class="hx:absolute hx:-mt-20" id="conclusion"></span>
    <a href="#conclusion" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>A well-built sandbox loses much of its value with a back door left open for convenience. bwrap, Landlock, seccomp, and rlimits in ai-jail keep doing their jobs, but none of them can see what happens when the process inside politely asks the host&rsquo;s root daemon to mount the whole filesystem into a container. A security layer you don&rsquo;t audit becomes decoration.</p>
<p>My public thanks to <a href="https://github.com/mdindoffer"target="_blank" rel="noopener">@mdindoffer</a>: clean report, minimal repro, correct severity, and a solution proposal on top. That&rsquo;s how you report a vulnerability to an open source project.</p>
<p>If you want the full context of how I use sandboxes day to day, I wrote about it in <a href="/en/2026/07/11/how-to-protect-yourself-from-agents-deleting-your-stuff/">How Do I Protect Myself From My Agents Deleting My Stuff?</a>. The ai-jail story is in <a href="/en/2026/01/10/ai-agents-locking-down-your-system/">AI Agents: Locking Down Your System</a> and in the <a href="/en/2026/03/01/ai-jail-sandbox-for-ai-agents-from-shell-script-to-real-tool/">Rust rewrite</a>.</p>
<p>Now go run the upgrade.</p>
]]></content:encoded><category>ai-jail</category><category>containers</category><category>security</category></item><item><title>An Emulation Distrobox with Claude Code</title><link>https://akitaonrails.github.io/en/2026/04/11/emulation-distrobox-with-claude-code/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2026/04/11/emulation-distrobox-with-claude-code/</guid><pubDate>Sat, 11 Apr 2026 18:00:00 -0300</pubDate><description>&lt;p&gt;I&amp;rsquo;ve loved old videogames since before many of you were born. My first contact was back in the Atari era, in the early 80s. Then came the 8-bit micros, the 90s arcades, the 16, 32 and 64-bit consoles, and I kept following all of it. For me, nostalgia is not a cute Instagram folder. It&amp;rsquo;s an archive. ROMs, BIOS files, dumps, patches, DLC, firmware, saves. Over the years I kept everything on my NAS. At this point I have terabytes of it under &lt;code&gt;/mnt/terachad/Emulators&lt;/code&gt;.&lt;/p&gt;</description><content:encoded><![CDATA[<p>I&rsquo;ve loved old videogames since before many of you were born. My first contact was back in the Atari era, in the early 80s. Then came the 8-bit micros, the 90s arcades, the 16, 32 and 64-bit consoles, and I kept following all of it. For me, nostalgia is not a cute Instagram folder. It&rsquo;s an archive. ROMs, BIOS files, dumps, patches, DLC, firmware, saves. Over the years I kept everything on my NAS. At this point I have terabytes of it under <code>/mnt/terachad/Emulators</code>.</p>
<p>On my YouTube channel I even used old games to teach computer science fundamentals. In <a href="https://www.youtube.com/watch?v=hYJ3dvHjeOE&amp;pp=ygUUYWtpdGFuZG8gc3VwZXIgbWFyaW8%3D"target="_blank" rel="noopener">this Akitando episode about Super Mario and old computers</a>, I explain the 6502, memory maps, the PPU, hardware constraints, and why those games were programmed the way they were. If you never watched it, start there:</p>


<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/hYJ3dvHjeOE"
    title="YouTube video player"
    frameborder="0"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
    referrerpolicy="strict-origin-when-cross-origin"
    allowfullscreen>
  </iframe>
</div>

<p>The problem is always the same: every few years I decide to set up a new emulation machine, download the main emulators again, and there I go repeating the same masochistic ritual. <code>PCSX2</code>, <code>RPCS3</code>, <code>Eden</code>, <code>Azahar</code>, <code>Dolphin</code>, <code>RetroArch</code>, <code>Flycast</code>, <code>shadPS4</code>, and so on. Each one with its quirks. Each one with its own particular way of wasting hours of your life.</p>
<p>In theory, setting all this up should be fun. In practice, it takes days. And that&rsquo;s not an exaggeration. <code>Dolphin</code> still manages to be one of the worst because GameCube and Wii controller setups have no standard. <code>RPCS3</code> requires per-game tuning. <code>Eden</code> needs DLC, updates and cheats in the right place. <code>shadPS4</code> is a festival of trial and error. When I&rsquo;d finally finish configuring everything, I didn&rsquo;t feel like playing anything anymore. I just wanted to close the menus and go do something else.</p>
<p>I&rsquo;ve been known to say that maybe the fun was the tuning itself. I don&rsquo;t think that anymore. After repeating this process too many times, I&rsquo;m done. I want to play the games, not fill out forms hidden inside emulator GUIs.</p>
<h2>The problem was never Linux<span class="hx:absolute hx:-mt-20" id="the-problem-was-never-linux"></span>
    <a href="#the-problem-was-never-linux" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>I&rsquo;ve solved this kind of thing in different ways in the past. Years ago I ran Linux on the host and a virtualized Windows with GPU passthrough to play inside the VM. At the time that made sense. I made a whole video about it:</p>


<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/IDnabc3DjYY"
    title="YouTube video player"
    frameborder="0"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
    referrerpolicy="strict-origin-when-cross-origin"
    allowfullscreen>
  </iframe>
</div>

<p>But that was before Valve, Proton, modern Wine and the whole open source community pushed the Linux desktop to a much better place. Today you can avoid Windows in most cases. In the other video below I explain the evolution of CPU, GPU, OpenGL, Vulkan, and why we ended up here:</p>


<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/JEp7ozWqIps"
    title="YouTube video player"
    frameborder="0"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
    referrerpolicy="strict-origin-when-cross-origin"
    allowfullscreen>
  </iframe>
</div>

<p>My gaming mini-PC with the RTX 4090 is still my main Steam machine, especially now that I&rsquo;ve built a <a href="/en/2026/04/01/my-sim-racing-cockpit-formula-fx1/">proper sim racing cockpit</a>. On it I use <a href="https://www.emudeck.com/"target="_blank" rel="noopener">EmuDeck</a>, which was born to automate installing and configuring emulators on the Steam Deck, then grew Windows support, and helps cut the complexity of setting up this kind of environment. But my main desktop is also way too powerful to leave underused. It has an RTX 5090, and today I use that GPU mostly to test LLMs and for benchmarking, as I wrote in <a href="/en/2026/04/05/testing-llms-open-source-and-commercial-can-anyone-beat-claude-opus/">Testing Open Source and Commercial LLMs</a>. Leaving it idle for gaming would be a waste.</p>
<p>Only on Linux I wanted something else. I didn&rsquo;t want a black box doing everything for me without me knowing exactly what was being changed, especially on my main PC, which is my work machine. I wanted my own setup, handmade in the right sense: not clicking GUI by GUI, but understanding what&rsquo;s being configured, keeping the files under my control, and being able to rebuild everything the way I like. I know the NixOS folks will jump in here to say &ldquo;just use Nix&rdquo; — I&rsquo;ll explain why I ruled that out below. I also didn&rsquo;t want to turn my work machine into a carnival of emulator packages, GTK themes, wrappers, launchers, weird runtimes and configs buried in <code>~/.config</code>. Nor did I want dual boot or a VM. In 2026, the best answer for this, in my opinion, is <a href="https://distrobox.it/"target="_blank" rel="noopener">Distrobox</a>.</p>
<p>From what the project itself explains, Distrobox is a wrapper on top of <code>podman</code>, <code>docker</code> or <code>lilipod</code> to create containers tightly integrated with the host, with access to <code>HOME</code>, Wayland/X11, audio, USB devices, external storage and GPU. In other words: exactly the kind of pragmatic isolation I wanted. It&rsquo;s not a security boundary, and the project site makes that clear. Don&rsquo;t use it thinking about high-security sandboxing. Use it thinking about separating environments without paying the price of a VM.</p>
<p>And before anyone repeats the usual confusion: a container is not a virtual machine. I explain this calmly in this other episode:</p>


<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/85k8se4Zo70"
    title="YouTube video player"
    frameborder="0"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
    referrerpolicy="strict-origin-when-cross-origin"
    allowfullscreen>
  </iframe>
</div>

<h2>The setup<span class="hx:absolute hx:-mt-20" id="the-setup"></span>
    <a href="#the-setup" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The idea is simple: a vanilla Arch Linux inside a Distrobox called <code>gaming</code>, with NAS paths mounted read-only, Steam library accessible where it makes sense, and all emulators installed inside. Since the container has access to the NVIDIA GPU, audio, USB and other peripherals, I can separate work and gaming on the same machine without practical performance penalty.</p>
<p>The initial bootstrap looks roughly like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">distrobox create --name gaming --image archlinux:latest --nvidia <span class="se">\
</span></span></span><span class="line"><span class="cl">  --home /mnt/data/distrobox/gaming <span class="se">\
</span></span></span><span class="line"><span class="cl">  --volume /mnt/data/steam:/mnt/data/steam <span class="se">\
</span></span></span><span class="line"><span class="cl">  --volume /mnt/terachad/Emulators:/mnt/terachad/Emulators:ro</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Even that first step has a catch. <code>archlinux:latest</code> inside Distrobox comes without <code>[multilib]</code> in <code>pacman.conf</code>, with a misconfigured <code>sudoers</code>, and with the old <code>--nvidia</code> problem: the host driver libs get bind-mounted read-only, so any install that depends on <code>nvidia-utils</code> breaks with file conflicts. Just that alone is the kind of headache that, a few years ago, would make me open half a dozen tabs, a bunch of terminals, and spend a whole night in trial and error.</p>
<p>This time I did it differently.</p>
<h2>Claude Code as an infrastructure assistant<span class="hx:absolute hx:-mt-20" id="claude-code-as-an-infrastructure-assistant"></span>
    <a href="#claude-code-as-an-infrastructure-assistant" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>I&rsquo;ve been getting more and more comfortable using Claude Code as my infrastructure assistant on my personal machines. I wouldn&rsquo;t do this blindly on a client server. But on my own machine, in an environment I can destroy and rebuild however many times I want, it makes all the sense in the world. I wrote about this in <a href="/en/2026/03/31/migrating-my-home-server-with-claude-code/">Migrating my Home Server with Claude Code</a>, when I used Claude to install and configure openSUSE MicroOS, Docker, NFS, firewall, hardening, and all the rest without me having to remember annoying shell commands.</p>
<p>So I thought: why not do the same thing here?</p>
<p>That&rsquo;s exactly what I did. I started with a sequence of objective prompts, always asking for two things at once: do the work, and document enough for me to rebuild everything later. The most important history ended up in <a href="https://github.com/akitaonrails/distrobox-gaming/blob/master/docs/distrobox-gaming-prompts.md"target="_blank" rel="noopener"><code>docs/distrobox-gaming-prompts.md</code></a>.</p>
<p>A clarification worth making: those prompts on GitHub aren&rsquo;t my raw messages, the way they came out in real time. After everything worked, I asked Claude itself to rewrite the prompts in a much more organized and detailed way, purely for documentation. The original prompts were much simpler and much less specific. I described the goal and let Claude figure out paths, config files, formats and commands on its own.</p>
<p>The first prompt created the box with <code>--nvidia</code>, a separate <code>--home</code>, and correct mounts. The second solved the three classic problems of Arch inside Distrobox: <code>sudo</code>, <code>multilib</code>, and the <code>nvidia-utils</code> dummy package. The third installed the gaming base, including a detail I would have easily forgotten if I was doing it by hand: <code>pipewire-pulse</code>, needed so several emulators don&rsquo;t end up mute or out of sync.</p>
<p>The point isn&rsquo;t the text itself. The point is the method. I don&rsquo;t need to remember the exact order, the precise options, or where the docs for each detail are. I describe the goal and let the agent carry the piano. I stay in the tech-lead role: watching, reviewing, course-correcting when needed, and telling it to keep going.</p>
<h2>The GitHub project<span class="hx:absolute hx:-mt-20" id="the-github-project"></span>
    <a href="#the-github-project" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The setup lives in <a href="https://github.com/akitaonrails/distrobox-gaming"target="_blank" rel="noopener">akitaonrails/distrobox-gaming</a>, structured as an Ansible project. 17 roles cover everything from box creation, package bootstrap, config seeding, DLC installation, controller setup, to post-setup verification. The main playbooks are:</p>
<ul>
<li><code>site.yml</code>: full setup from scratch</li>
<li><code>reset-configs.yml</code>: reset configs only (useful when you want to wipe tuning and redeploy)</li>
<li><code>backup.yml</code> / <code>restore.yml</code>: container snapshot and restore via <code>podman commit</code></li>
<li><code>refresh-shadps4.yml</code>: standalone shadPS4 update</li>
<li><code>install-xenia.yml</code>: optional Xenia Manager install (Wine prefix)</li>
</ul>
<p>Full walkthrough is in <a href="https://github.com/akitaonrails/distrobox-gaming/blob/master/docs/rebuild-runbook.md"target="_blank" rel="noopener"><code>docs/rebuild-runbook.md</code></a>. Package decisions (why AUR instead of Flatpak, for example) are in <a href="https://github.com/akitaonrails/distrobox-gaming/blob/master/docs/distrobox-gaming-packages.md"target="_blank" rel="noopener"><code>docs/distrobox-gaming-packages.md</code></a>.</p>
<h3>Why not Nix?<span class="hx:absolute hx:-mt-20" id="why-not-nix"></span>
    <a href="#why-not-nix" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Someone will ask. &ldquo;You&rsquo;re recreating a reproducible environment. Why not use NixOS, or at least <code>nix-shell</code>/flakes?&rdquo;</p>
<p>I considered and ruled it out. The emulator ecosystem I need lives on the AUR. AUR <code>-bin</code> packages are wrappers around AppImages that ship the official upstream binary without compiling anything. <code>rpcs3-bin</code>, <code>pcsx2-latest-bin</code>, <code>eden-bin</code>, <code>duckstation-qt-bin</code>, <code>shadps4-bin</code> — these binaries track upstream practically on release day. On Nixpkgs, many of these emulators are weeks or months behind release, and some don&rsquo;t exist. Creating a Nix overlay for every emulator I need, maintaining it, and dealing with compatibility patches when upstream changes is work I don&rsquo;t want to have.</p>
<p>There&rsquo;s also the mental model. I don&rsquo;t want to learn the Nix language, the derivation system and the evaluation model just to set up a personal gaming environment. I already know Ansible, roles are folders with YAML tasks, and debugging is <code>ansible-playbook -vvv</code>. When something goes wrong, I read the error, open the task that failed, and know exactly where to fix it. If I needed the same level of reproducibility at scale, across a cluster, with atomic rollback, then Nix would have an argument. For a distrobox with emulators on my personal machine, Ansible solves it with much less friction.</p>
<p>Flatpak was also tested and ruled out. Flatpak&rsquo;s <code>bwrap</code> (bubblewrap) doesn&rsquo;t nest properly inside Docker&rsquo;s mount namespaces, so Flatpak apps simply don&rsquo;t run inside distrobox. The alternative would be installing Flatpak on the host, but then you&rsquo;re back to polluting the work machine with gaming packages. Plus, Flatpak versions of emulators lag well behind AUR — the AUR&rsquo;s <code>pcsx2-latest-bin</code> tracks the 2.7.x AppImage, while Flathub still ships v2.6.3.</p>
<h2>Platforms covered<span class="hx:absolute hx:-mt-20" id="platforms-covered"></span>
    <a href="#platforms-covered" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The setup covers 12 platforms today:</p>
<table>
  <thead>
      <tr>
          <th>Platform</th>
          <th>Emulator</th>
          <th>Renderer</th>
          <th>Highlight</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>PS1</td>
          <td>DuckStation</td>
          <td>Vulkan, 8x upscale</td>
          <td>JINC2 filter, PGXP, widescreen hack</td>
      </tr>
      <tr>
          <td>PS2</td>
          <td>PCSX2 2.7.x</td>
          <td>Vulkan, 4x SSAA</td>
          <td>FXAA + PCRTC antiblur, Xbox bindings</td>
      </tr>
      <tr>
          <td>PS3</td>
          <td>RPCS3</td>
          <td>Vulkan</td>
          <td>Curated per-game configs via API</td>
      </tr>
      <tr>
          <td>PS4</td>
          <td>shadPS4</td>
          <td>Vulkan</td>
          <td>Driveclub-focused (experimental)</td>
      </tr>
      <tr>
          <td>PS Vita</td>
          <td>Vita3K</td>
          <td>—</td>
          <td>Default seeding</td>
      </tr>
      <tr>
          <td>GameCube</td>
          <td>Dolphin</td>
          <td>Vulkan</td>
          <td>8BitDo Ultimate 2 profiles</td>
      </tr>
      <tr>
          <td>Wii</td>
          <td>Dolphin</td>
          <td>Vulkan</td>
          <td>Classic Controller + Nunchuk</td>
      </tr>
      <tr>
          <td>Wii U</td>
          <td>Cemu</td>
          <td>Vulkan</td>
          <td>Baseline seed only-if-missing</td>
      </tr>
      <tr>
          <td>Switch</td>
          <td>Eden</td>
          <td>Vulkan</td>
          <td>Atmosphere DLC + cheats</td>
      </tr>
      <tr>
          <td>Dreamcast</td>
          <td>Flycast</td>
          <td>Vulkan, 2880p</td>
          <td>Hi-res wrapper, widescreen</td>
      </tr>
      <tr>
          <td>OG Xbox</td>
          <td>xemu</td>
          <td>—</td>
          <td>BIOS/HDD via symlink</td>
      </tr>
      <tr>
          <td>Xbox 360</td>
          <td>Xenia (Wine)</td>
          <td>Vulkan</td>
          <td>Project Forza Plus, PGR3/4</td>
      </tr>
  </tbody>
</table>
<p>RetroArch comes on top to cover the rest (NES, SNES, Genesis, N64, arcade, etc.), with 21 buildbot cores and 8 asset packs (databases, shaders, cheats, overlays, autoconfig).</p>
<p>Frontend is <a href="https://es-de.org/"target="_blank" rel="noopener">ES-DE</a>. Custom system definitions live in <code>ansible/group_vars/all/esde.yml</code>, and each emulator comes with a wrapper that sets the right GPU environment variables (more on that in a sec).</p>
<h2>GPU hardforce for NVIDIA<span class="hx:absolute hx:-mt-20" id="gpu-hardforce-for-nvidia"></span>
    <a href="#gpu-hardforce-for-nvidia" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>On a hybrid system with NVIDIA dGPU + AMD iGPU (my case on the desktop), Vulkan sometimes picks the iGPU on its own and you end up with an emulator running at 15 fps while the 5090 sits idle. The fix is to force NVIDIA&rsquo;s Vulkan ICD in every launcher.</p>
<p>Each desktop entry rendered by Ansible exports <code>VK_ICD_FILENAMES</code> pointing to NVIDIA&rsquo;s ICD, and the ES-DE wrapper does the same before launching any core. Combined with the <code>--nvidia</code> flag at box creation, which bind-mounts host drivers read-only, the result is predictable: Vulkan always on the NVIDIA dGPU, regardless of system state at runtime.</p>
<p>This mostly matters for hybrid setups. On a desktop with a single GPU, the ICD filtering is placebo, but it doesn&rsquo;t hurt.</p>
<h2>PS2: PCSX2 with per-game tuning<span class="hx:absolute hx:-mt-20" id="ps2-pcsx2-with-per-game-tuning"></span>
    <a href="#ps2-pcsx2-with-per-game-tuning" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The Gran Turismo line is my declared addiction. Each version needs its own specific tweak. The PCSX2 setup today has sane global configs (Vulkan, 4x SSAA, widescreen 16:9, Xbox-style bindings, FXAA + PCRTC antiblur, 16x anisotropic, Automatic deinterlace) and per-game INIs for the titles I actually play:</p>
<ul>
<li><strong>Gran Turismo 3 A-spec</strong> (SCUS-97102 + bundle PBPX-95503): widescreen pnach, optional retexture pack (disabled by default because NFS streaming causes flicker in car-showcase cutscenes)</li>
<li><strong>Gran Turismo 4 USA</strong> (SCUS-97328): Silentwarior112 HD HUD, full retexture pack, Silent&rsquo;s trigger/cam patches, widescreen pnach</li>
<li><strong>Gran Turismo 4 Spec II</strong> (SCUS-97436, CRC <code>4CE521F2</code>): special case — the vanilla GT4 HD packs are linked via symlink because Spec II has the same structure but a different CRC. Widescreen pnach renamed to match the CRC. Deinterlace mode 8 (Adaptive TFF), ShadeBoost tuning (+10 saturation, +3 brightness, +2 contrast) to fix pause-interlace ghosting and the anemic upscale saturation</li>
<li><strong>Enthusia Professional Racing</strong> (SLUS-20967): HD textures + widescreen</li>
<li><strong>Ridge Racer V</strong> (SLUS-20002): widescreen + no-interlace pnach</li>
</ul>
<p>Silent&rsquo;s and community pnach files are downloaded automatically by the <code>pcsx2_textures</code> role, which also deploys the cheats pack from the NAS and installs the Spec II HD packs via symlink. Widescreen is enabled per-CRC via <code>gamesettings</code> (PCSX2 identifies games by CRC before applying pnach).</p>
<p>On PCSX2 versioning: the AUR&rsquo;s <code>pcsx2-latest-bin</code> tracks the 2.7.x line, which is where all the modern features live (texture replacement, FXAA, PCRTC antiblur). The default <code>pcsx2</code> in Arch repos is still on 2.6.3. The gap is over 250 commits of fixes and new features.</p>
<h2>PS3: RPCS3 with curated per-game configs<span class="hx:absolute hx:-mt-20" id="ps3-rpcs3-with-curated-per-game-configs"></span>
    <a href="#ps3-rpcs3-with-curated-per-game-configs" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>RPCS3 has an annoying detail: per-game configs live in <code>~/.config/rpcs3/custom_configs/config_&lt;SERIAL&gt;.yml</code>. The file format has a version, and if you hand-write YAML with the wrong version, RPCS3 silently rejects it and falls back to defaults. That cost me time to figure out.</p>
<p>The <code>rpcs3_per_game_configs</code> role handles it like this:</p>
<ol>
<li>Queries the <a href="https://rpcs3.net/compatibility"target="_blank" rel="noopener">RPCS3 community compatibility DB</a> for recommended presets per title</li>
<li>Applies curated overrides on top for titles I know need specific tuning (typically GT5 and GT6)</li>
<li>Saves the YAML with the exact filename convention (<code>config_&lt;SERIAL&gt;.yml</code>, <code>config_</code> prefix mandatory)</li>
</ol>
<p>For the Gran Turismo line:</p>
<ul>
<li><strong>GT5</strong> (BCUS98114, BCES00569): Resolution Scale 300%, Shader Precision Ultra, Force High Precision Z, SPU XFloat Accurate, Multithreaded RSX. The combo kills the typical RSX dithering without breaking the GT-series safety config (WCB/RCB off).</li>
<li><strong>GT6</strong> (BCES01893 + regional variants): special case, <strong>version pinned to v1.05</strong>. Patches 1.06+ regress with black surfaces on cars in the garage menu. The <code>extract_ps3_dlc.py</code> script has a per-title PATCH ceiling specifically to pin GT6 at 1.05 even when PSN serves newer patches. Also, Force CPU Blit is mandatory (without it, full-screen flicker). The trade-off is that the rear-view mirror stays permanently black — turning WCB on would restore the mirror but downgrade to native 720p. I&rsquo;ll take no mirror.</li>
</ul>
<p>More details on those two in <a href="https://github.com/akitaonrails/distrobox-gaming/blob/master/docs/gt5-rpcs3.md"target="_blank" rel="noopener"><code>docs/gt5-rpcs3.md</code></a> and <a href="https://github.com/akitaonrails/distrobox-gaming/blob/master/docs/gt6-rpcs3.md"target="_blank" rel="noopener"><code>docs/gt6-rpcs3.md</code></a>.</p>
<h3>PS3 update checker<span class="hx:absolute hx:-mt-20" id="ps3-update-checker"></span>
    <a href="#ps3-update-checker" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>To avoid opening RPCS3&rsquo;s GUI and clicking <code>Download Game Updates</code> game by game, I wrote a Python script: <code>check_ps3_updates.py</code>. It walks the games installed in <code>dev_hdd0/game</code>, parses <code>PARAM.SFO</code> to get the current version per title, queries the PSN update server (<code>a0.ww.np.dl.playstation.net</code>), and compares against the local patch cache. <code>--list</code> shows the diff; <code>--download</code> fetches what&rsquo;s missing. <code>--max-version</code> respects per-title ceilings (e.g., 1.05 for GT6).</p>
<p>The first scan of my library found 51 of 72 games with updates available, 69 patches missing locally, ~24 GB in total. GT6 alone had 16 missing patches from 1.07 to 1.22, which I skipped for the reasons above.</p>
<p>Patches arrive as PSN packages type 0x0001 without encryption, so <code>extract_ps3_dlc.py</code> handles the install. Standard call:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">distrobox enter gaming -- python3 scripts/extract_ps3_dlc.py <span class="se">\
</span></span></span><span class="line"><span class="cl">  <span class="s2">&#34;/mnt/terachad/Emulators/EmuDeck/roms_heavy/ps3-DLC&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  --dest <span class="s2">&#34;/mnt/data/distrobox/gaming/.config/rpcs3/dev_hdd0/game&#34;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Tracking PS3 DLC versions manually, game by game through RPCS3, is impractical. It becomes an infinite to-do list, and you only find out there was a 1.22 patch when the game crashes.</p>
<h2>PS1: DuckStation per-game<span class="hx:absolute hx:-mt-20" id="ps1-duckstation-per-game"></span>
    <a href="#ps1-duckstation-per-game" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The Gran Turismo trilogy starts on PS1, and quality there only goes up if you enable the right things:</p>
<ul>
<li><strong>Gran Turismo</strong> (SCUS-94949): DuckStation&rsquo;s built-in WidescreenHack (no cheat exists for this game), PGXP enabled</li>
<li><strong>Gran Turismo 2 Simulation</strong> (SCUS-94455): widescreen cheat + 8 MB RAM cheat (fixes audio issues on crowded tracks), JINC2 filter</li>
<li><strong>Gran Turismo 2 Arcade</strong> (SCUS-94488): same treatment as Simulation</li>
</ul>
<p>Global defaults: Vulkan, 8x upscale, JINC2 as the default filter.</p>
<p>GT2 cheats come from a widescreen-fixes repo and are linked automatically into DuckStation&rsquo;s cheats folder.</p>
<h2>Switch: Eden with DLC, update and cheats<span class="hx:absolute hx:-mt-20" id="switch-eden-with-dlc-update-and-cheats"></span>
    <a href="#switch-eden-with-dlc-update-and-cheats" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>On Switch, <code>eden-bin</code> is the emulator. The integration automates three things I used to do by hand:</p>
<ol>
<li>
<p><strong>DLC and update install</strong>: the <code>install_dlcs</code> role points to the dumps directory on the NAS, identifies <code>.nsp</code>/<code>.nsz</code>/<code>.xci</code>/<code>.xcz</code> files, and installs everything into <code>~/.local/share/eden/nand/user/Contents/registered/</code>. If the dump is messy (patches and DLC mixed in a flat folder), the <code>reorganize_switch_nsps.py</code> script sorts everything by Title ID first.</p>
</li>
<li>
<p><strong>Atmosphere cheats</strong>: Atmosphere-format cheats get symlinked into Eden&rsquo;s load path (<code>~/.local/share/eden/load/&lt;TITLE_ID&gt;/cheats/</code>). The <code>switch_cheats</code> role handles this for every title covered by the NAS pack.</p>
</li>
<li>
<p><strong>Update checker</strong>: <code>check_switch_updates.py</code> lists titles with updates available against what&rsquo;s installed locally. Useful so I don&rsquo;t miss important patches for games still getting updates.</p>
</li>
</ol>
<p>A detail that burned me: <code>QT_STYLE_OVERRIDE</code> needs to be unset before launching <code>eden-bin</code>, otherwise it conflicts with Kvantum and breaks the UI. The wrapper does the <code>unset</code> before <code>exec</code>.</p>
<h2>Xbox 360: Xenia Manager via Wine<span class="hx:absolute hx:-mt-20" id="xbox-360-xenia-manager-via-wine"></span>
    <a href="#xbox-360-xenia-manager-via-wine" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Xbox 360 is still rough territory on Linux. There&rsquo;s no decent native emulator. The best route today is <a href="https://xenia.jp/"target="_blank" rel="noopener">Xenia</a> running through Wine, managed by <a href="https://github.com/xenia-manager/xenia-manager"target="_blank" rel="noopener">Xenia Manager</a>.</p>
<p>This is opt-in in my setup. The <code>install-xenia.yml</code> playbook creates a dedicated Wine prefix, downloads Xenia Manager, and registers launchers. It&rsquo;s not part of the main <code>site.yml</code> because not everyone wants Wine in the mix.</p>
<h3>Project Forza Plus (Forza Motorsport 2/3/4)<span class="hx:absolute hx:-mt-20" id="project-forza-plus-forza-motorsport-234"></span>
    <a href="#project-forza-plus-forza-motorsport-234" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Old Forza is still the king of previous-gen simcade. Running FM2/3/4 on Xenia requires the Project Forza Plus community mods: compatibility patches, performance mods, specific title-update installs. I documented the process in <a href="https://github.com/akitaonrails/distrobox-gaming/blob/master/docs/project-forza.md"target="_blank" rel="noopener"><code>docs/project-forza.md</code></a>.</p>
<h3>Project Gotham Racing 3 and 4<span class="hx:absolute hx:-mt-20" id="project-gotham-racing-3-and-4"></span>
    <a href="#project-gotham-racing-3-and-4" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>PGR3 runs reasonably well on Xenia Canary. PGR4 needs a specific tweak: <code>render_target_path_vulkan = &quot;fsi&quot;</code> in the config, otherwise some races break with visual artifacts. Setup documented in <a href="https://github.com/akitaonrails/distrobox-gaming/blob/master/docs/xbox360-pgr.md"target="_blank" rel="noopener"><code>docs/xbox360-pgr.md</code></a>. PGR4 audio on NVIDIA still has an XMA decoding issue that produces intermittent garbage. There&rsquo;s an open xenia-canary issue, no resolution so far.</p>
<h3>Batch Title Updates<span class="hx:absolute hx:-mt-20" id="batch-title-updates"></span>
    <a href="#batch-title-updates" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Title Updates for Xbox 360 games were distributed via Xbox Live, which has been shut down. The alternative is archive.org, which has the complete catalog preserved. I wrote <code>scripts/download-xbox360-tus.py</code> to automate:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">distrobox enter gaming -- python3 scripts/download-xbox360-tus.py <span class="se">\
</span></span></span><span class="line"><span class="cl">  --src /mnt/terachad/Emulators/EmuDeck/roms_heavy/xbox360 <span class="se">\
</span></span></span><span class="line"><span class="cl">  --dest /mnt/terachad/Emulators/EmuDeck/roms_heavy/xbox360-updates <span class="se">\
</span></span></span><span class="line"><span class="cl">  --dry-run</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>The script needs the <code>internetarchive</code> CLI authenticated (<code>ia configure</code> once, stores the token). It scans the games folder, matches against the archive.org manifest (cached locally), prioritizes by region (USA &gt; World &gt; Europe &gt; Japan), and downloads via <code>ia download --checksum</code> with automatic retry. The resulting .zip files go to Xenia Manager via <code>Manage → Install Content</code>, which extracts into the correct directory (<code>000B0000/</code>).</p>
<p>Full setup in <a href="https://github.com/akitaonrails/distrobox-gaming/blob/master/docs/xbox360-title-updates.md"target="_blank" rel="noopener"><code>docs/xbox360-title-updates.md</code></a>.</p>
<h2>Steam in the distrobox<span class="hx:absolute hx:-mt-20" id="steam-in-the-distrobox"></span>
    <a href="#steam-in-the-distrobox" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Since Proton became a first-class citizen on Linux, it makes sense to have Steam in the gaming box too. I added the <code>steam</code> package via Ansible (which pulls in multilib and 32-bit deps), mounted <code>/mnt/data/steam</code> read-write so the library persists outside the container, and created a host-side launcher.</p>
<p>In practice, this lets me run Steam games via Proton side-by-side with emulation, without switching environments. The mini-PC with the RTX 4090 is still the main Steam machine in the sim racing cockpit. The desktop is fallback and a test environment.</p>
<h2>Shell and controllers<span class="hx:absolute hx:-mt-20" id="shell-and-controllers"></span>
    <a href="#shell-and-controllers" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><h3>Minimal in-box shell<span class="hx:absolute hx:-mt-20" id="minimal-in-box-shell"></span>
    <a href="#minimal-in-box-shell" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>When I <code>distrobox enter gaming</code>, I want a decent prompt even for quick troubleshooting. I installed <code>zsh</code> + <code>starship</code> with a minimal config, zero exotic plugins. It&rsquo;s not my full dev setup, it&rsquo;s just &ldquo;a prompt that shows path and git branch without embarrassing me&rdquo;.</p>
<h3>Dolphin and the modern-controller hell<span class="hx:absolute hx:-mt-20" id="dolphin-and-the-modern-controller-hell"></span>
    <a href="#dolphin-and-the-modern-controller-hell" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p><code>Dolphin</code> has always been the king of manual friction. If you use a modern controller, like my 8BitDo Ultimate 2, you have to remember how I like the GameCube mapping, how I adapt the Wii Remote, when to use a Nunchuk profile, when to switch to Classic Controller. I don&rsquo;t have the patience to rebuild that by hand every time. Profiles are pre-built in <code>config/emulator-overrides/dolphin/Profiles/</code> and copied by the <code>seed_configs</code> role. Details in <a href="https://github.com/akitaonrails/distrobox-gaming/blob/master/docs/controller-hotkeys.md"target="_blank" rel="noopener"><code>docs/controller-hotkeys.md</code></a>.</p>
<h2>A few stumbles that stuck around<span class="hx:absolute hx:-mt-20" id="a-few-stumbles-that-stuck-around"></span>
    <a href="#a-few-stumbles-that-stuck-around" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Not everything became magic. Emulation always has banana peels.</p>
<p><code>Flycast</code>, for example, has an irritating trap. The <code>emu.cfg</code> file uses <code>rend.*</code> keys inside the <code>[config]</code> section. If you create a separate <code>[rend]</code> section, it looks right, but the emulator just ignores it and later rewrites everything with mediocre defaults. The fix became a dedicated wrapper at <code>$DG_BOX_HOME/bin/flycast-hires</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nv">$DG_BOX_HOME</span>/bin/flycast-hires <span class="se">\
</span></span></span><span class="line"><span class="cl">  -config config:pvr.rend<span class="o">=</span><span class="m">4</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  -config config:rend.Resolution<span class="o">=</span><span class="m">2880</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  -config config:rend.EmulateFramebuffer<span class="o">=</span>no <span class="se">\
</span></span></span><span class="line"><span class="cl">  -config config:rend.WideScreen<span class="o">=</span>yes</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Details in <a href="https://github.com/akitaonrails/distrobox-gaming/blob/master/docs/flycast-resolution.md"target="_blank" rel="noopener"><code>docs/flycast-resolution.md</code></a>.</p>
<p><code>shadPS4</code> is still far from a closed case. The current setup is focused on <code>Driveclub</code>, with a mirrored config, XML patch and sys_modules links from firmware 11.00. And that wasn&rsquo;t random: I really like <code>Driveclub</code>. It&rsquo;s practically the only game still stuck on PS4 that I really wanted to get running properly. I&rsquo;ve seen several YouTube videos showing the game working, but so far I haven&rsquo;t been able to get it running satisfactorily on Linux myself. Documented in <a href="https://github.com/akitaonrails/distrobox-gaming/blob/master/docs/driveclub-shadps4.md"target="_blank" rel="noopener"><code>docs/driveclub-shadps4.md</code></a>. The good part is that I no longer depend on muscle memory to remember how to launch it, with which patch, in which folder, with which modules. The bad part is that PS4 emulation is still PS4 emulation. No script makes upstream mature faster. If anyone knows how to close this setup on Linux, check the project on GitHub and send a PR.</p>
<p>Ridge Racer V has car-texture flickering documented in PCSX2 issues (#3639, #13729). The Hardware renderer is acceptable for a racing game where you&rsquo;re not standing still admiring the model. Software renderer fixes it but kills performance. I chose to live with the flicker.</p>
<h2>The real gain isn&rsquo;t just automation<span class="hx:absolute hx:-mt-20" id="the-real-gain-isnt-just-automation"></span>
    <a href="#the-real-gain-isnt-just-automation" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>It&rsquo;d be easy to sum this up as &ldquo;look how cool, I used AI to automate shell scripts&rdquo;. That&rsquo;s not it.</p>
<p>The real gain is more mundane and more important: I stopped spending mental energy on manual work. I didn&rsquo;t have to remember rare commands. I didn&rsquo;t have to keep dozens of tabs of wiki, issue, forum, README. I didn&rsquo;t have to leave half a dozen terminals scattered tailing logs while I try to remember which hidden GUI option the emulator insists on overwriting on the first boot. I started working in conversation.</p>
<p>I tell the agent the goal. It looks up the files, reads the logs, finds the right config format, compares defaults, proposes fixes, writes scripts, validates paths, checks UID/GID, verifies broken symlinks, generates wrappers, exports launchers. I&rsquo;m still responsible for decisions, of course. But I stopped being a typist of rare commands.</p>
<p>That&rsquo;s the point that interests me most about using coding agents on Linux. They dramatically reduce the entry friction. A lot of people bounce off the Linux desktop not because the system is incapable, but because the tuning curve has historically been too irritating. Having an assistant capable of reading docs, cross-referencing configs, proposing automation and executing under supervision changes that game.</p>
<h2>If you want to reproduce<span class="hx:absolute hx:-mt-20" id="if-you-want-to-reproduce"></span>
    <a href="#if-you-want-to-reproduce" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The repo was published for this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git clone https://github.com/akitaonrails/distrobox-gaming.git
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> distrobox-gaming/ansible
</span></span><span class="line"><span class="cl">ansible-galaxy collection install -r collections/requirements.yml
</span></span><span class="line"><span class="cl">cp host_vars/localhost.yml.example host_vars/localhost.yml
</span></span><span class="line"><span class="cl"><span class="nv">$EDITOR</span> host_vars/localhost.yml  <span class="c1"># adjust paths for your machine</span>
</span></span><span class="line"><span class="cl">ansible-playbook site.yml</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Xenia is opt-in:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ansible-playbook install-xenia.yml</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Read the <a href="https://github.com/akitaonrails/distrobox-gaming/blob/master/README.md"target="_blank" rel="noopener"><code>README.md</code></a> and <a href="https://github.com/akitaonrails/distrobox-gaming/blob/master/docs/rebuild-runbook.md"target="_blank" rel="noopener"><code>docs/rebuild-runbook.md</code></a> first. I don&rsquo;t distribute ROMs, BIOS, firmware or keys. The repo only detects, links and configures what you already have on your own machine.</p>
<h2>If you prefer the Claude Code route<span class="hx:absolute hx:-mt-20" id="if-you-prefer-the-claude-code-route"></span>
    <a href="#if-you-prefer-the-claude-code-route" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>My recommendation is simple:</p>
<ol>
<li>Start with the goal, not the command. &ldquo;I want an Arch distrobox with NVIDIA GPU, separate home, ROMs read-only and Steam rw&rdquo; beats dumping a half shell command without context.</li>
<li>Always ask it to document what it&rsquo;s doing. If it works, promote the result to a script or role. If you don&rsquo;t document, you just manufactured a throwaway hack.</li>
<li>Work in phases. Box creation. Bootstrap. Emulator config. Launcher export. Verification. That&rsquo;s exactly how I broke down the problem.</li>
<li>Ask for objective checks. <code>command -v</code>, file existence, broken symlinks, UID/GID, ROM paths, audio, GPU. The best automation is the one that fails early.</li>
<li>Don&rsquo;t use the agent as a command parrot. Use it as a technical assistant. You still review the decisions and tell it to adjust course.</li>
</ol>
<p>For me, that was the gain. I went from zero to a much more complete emulation machine without having to customize everything by hand, GUI by GUI, one window at a time. And this time, I finished with energy left to do what I wanted from the start.</p>
<p>Play.</p>
]]></content:encoded><category>emulation</category><category>containers</category><category>automation</category></item><item><title>Migrating my Home Server with Claude Code | openSUSE MicroOS</title><link>https://akitaonrails.github.io/en/2026/03/31/migrating-my-home-server-with-claude-code/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2026/03/31/migrating-my-home-server-with-claude-code/</guid><pubDate>Tue, 31 Mar 2026 16:00:00 -0300</pubDate><description>&lt;p&gt;My old home server was a mess. An Intel NUC with Ubuntu Server that I&amp;rsquo;d been patching together for two years. Containers with hardcoded paths, volumes mounted in random places (&lt;code&gt;/home/akitaonrails/docker/&lt;/code&gt;, &lt;code&gt;/home/akitaonrails/sonarr/&lt;/code&gt;, &lt;code&gt;/mnt/terachad/&lt;/code&gt;), docker-compose files scattered with no consistent layout. It worked, but if I lost the disk it would take days to rebuild everything from memory.&lt;/p&gt;
&lt;p&gt;With the &lt;a href="https://akitaonrails.github.io/en/2026/03/31/minisforum-ms-s1-max-amd-ai-max-395-review/"&gt;new Minisforum MS-S1 Max&lt;/a&gt; that I bought, I decided to do the migration properly. And I decided to use Claude Code from the start to speed up the process. It&amp;rsquo;s a home server, only I use it, so the risk of making a mistake is low. But if this were a real production server, I would never do this without rigorous human review at every step.&lt;/p&gt;</description><content:encoded><![CDATA[<p>My old home server was a mess. An Intel NUC with Ubuntu Server that I&rsquo;d been patching together for two years. Containers with hardcoded paths, volumes mounted in random places (<code>/home/akitaonrails/docker/</code>, <code>/home/akitaonrails/sonarr/</code>, <code>/mnt/terachad/</code>), docker-compose files scattered with no consistent layout. It worked, but if I lost the disk it would take days to rebuild everything from memory.</p>
<p>With the <a href="/en/2026/03/31/minisforum-ms-s1-max-amd-ai-max-395-review/">new Minisforum MS-S1 Max</a> that I bought, I decided to do the migration properly. And I decided to use Claude Code from the start to speed up the process. It&rsquo;s a home server, only I use it, so the risk of making a mistake is low. But if this were a real production server, I would never do this without rigorous human review at every step.</p>
<p>What follows is the migration writeup and a guide for anyone who wants to replicate it. If I ever have to rebuild from scratch, this post is the documentation.</p>
<h2>Choosing the operating system<span class="hx:absolute hx:-mt-20" id="choosing-the-operating-system"></span>
    <a href="#choosing-the-operating-system" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><h3>Why not Ubuntu Server again<span class="hx:absolute hx:-mt-20" id="why-not-ubuntu-server-again"></span>
    <a href="#why-not-ubuntu-server-again" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>I used Ubuntu Server on the NUC for convenience. But <code>do-release-upgrade</code> is Russian roulette. Every time Ubuntu releases a new version, the upgrade is a real risk of breaking things. Packages change, configs get overwritten, dependencies clash. For a server that has to be running constantly, that&rsquo;s unacceptable.</p>
<h3>Why not Arch Linux<span class="hx:absolute hx:-mt-20" id="why-not-arch-linux"></span>
    <a href="#why-not-arch-linux" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>I use Arch on the desktop and I like it. But Arch is a rolling release with no stability guarantees at all. For a desktop where I can stop and fix problems, fine. For a headless server running 49 Docker containers that has to work after every reboot, no.</p>
<h3>Fedora CoreOS vs openSUSE MicroOS<span class="hx:absolute hx:-mt-20" id="fedora-coreos-vs-opensuse-microos"></span>
    <a href="#fedora-coreos-vs-opensuse-microos" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The two modern options for a container server are Fedora CoreOS and openSUSE MicroOS. Both are immutable systems: the root filesystem is read-only, updates are atomic (they either apply in full or don&rsquo;t apply at all), and rollback is instant.</p>
<p>The difference: Fedora CoreOS uses Ignition (declarative configuration before the first boot) and is designed to be provisioned automatically. MicroOS uses <code>transactional-update</code> and allows normal interactive use. For a home server where I want SSH and the ability to poke around manually, MicroOS fits better.</p>
<h3>What makes MicroOS different<span class="hx:absolute hx:-mt-20" id="what-makes-microos-different"></span>
    <a href="#what-makes-microos-different" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The immutable system concept changes how you operate the server:</p>
<p>Every package install or <code>/etc</code> edit goes through <code>transactional-update</code>, which creates a new btrfs snapshot, applies the change in that snapshot, and on the next reboot the system boots into the updated snapshot. If the change breaks something, you do <code>transactional-update rollback</code> and you&rsquo;re back on the previous snapshot in seconds.</p>
<p>Updates are automatic and daily. The <code>transactional-update.timer</code> downloads patches, creates a snapshot, and <code>rebootmgr</code> reboots in a configured window (in my case, between 4am and 5:30am). If the update breaks the boot, GRUB automatically falls back to the previous snapshot.</p>
<p>SELinux is enforcing by default. That caused 90% of the problems during the migration, but it&rsquo;s the right setting for security.</p>
<h2>Initial setup<span class="hx:absolute hx:-mt-20" id="initial-setup"></span>
    <a href="#initial-setup" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><h3>Hardware<span class="hx:absolute hx:-mt-20" id="hardware"></span>
    <a href="#hardware" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><ul>
<li>AMD Ryzen AI Max+ 395, 128GB LPDDR5X</li>
<li>96GB allocated as VRAM via BIOS (UMA Frame Buffer Size)</li>
<li>2TB NVMe (system + Docker)</li>
<li>Wired 2.5Gbps network</li>
<li>Synology DS1821+ NAS at 192.168.0.21 (NFS)</li>
</ul>
<h3>First steps<span class="hx:absolute hx:-mt-20" id="first-steps"></span>
    <a href="#first-steps" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>MicroOS install is standard. Then:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Create user with a UID that matches the NAS (so NFS works without permission issues)</span>
</span></span><span class="line"><span class="cl">useradd -u <span class="m">1026</span> -m akitaonrails
</span></span><span class="line"><span class="cl">passwd akitaonrails
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Configure sudo (inside transactional-update shell)</span>
</span></span><span class="line"><span class="cl">sudo transactional-update shell
</span></span><span class="line"><span class="cl"><span class="c1"># inside: add akitaonrails to sudoers</span>
</span></span><span class="line"><span class="cl"><span class="nb">exit</span>
</span></span><span class="line"><span class="cl">sudo reboot</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h3>Synology NFS<span class="hx:absolute hx:-mt-20" id="synology-nfs"></span>
    <a href="#synology-nfs" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The Synology NAS exports <code>/volume1/TERACHAD</code> over NFS. The mount point on MicroOS is <code>/var/mnt/terachad</code> (not <code>/mnt/</code>, which lives on the immutable root).</p>
<p>In <code>/etc/fstab</code> (applied through transactional-update):</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>192.168.0.21:/volume1/TERACHAD /var/mnt/terachad nfs4 nfsvers=4.1,rsize=262144,wsize=262144,hard,_netdev 0 0</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Details that matter: <code>nfsvers=4.1</code> because 4.2 didn&rsquo;t work with the Synology. <code>rsize=262144,wsize=262144</code> (256KB buffers) was the biggest NFS performance improvement. <code>hard</code> instead of <code>nofail</code> so the mount keeps retrying indefinitely if the NAS disconnects temporarily.</p>
<h3>GPU / ROCm<span class="hx:absolute hx:-mt-20" id="gpu--rocm"></span>
    <a href="#gpu--rocm" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>This step was a pain. The Radeon 8060S in the AI Max+ 395 is gfx1151, which ROCm doesn&rsquo;t officially support. Three steps were needed, and all three are mandatory:</p>
<ol>
<li>BIOS: set UMA Frame Buffer Size to 96GB</li>
<li>Kernel: add <code>amdttm.pages_limit=25165824 amdttm.page_pool_size=25165824</code> to <code>/etc/kernel/cmdline</code></li>
<li>Docker: use <code>HSA_OVERRIDE_GFX_VERSION=11.5.1</code> in every ROCm container</li>
</ol>
<p>Without step 2, ROCm only sees 15.5GB even with the BIOS allocation in place. The numbers are 96GB / 4KB (page size) = 25,165,824 pages.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo transactional-update shell
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;amdttm.pages_limit=25165824 amdttm.page_pool_size=25165824&#34;</span> &gt;&gt; /etc/kernel/cmdline
</span></span><span class="line"><span class="cl"><span class="nb">exit</span>
</span></span><span class="line"><span class="cl">sudo sdbootutil update-all-entries  <span class="c1"># OUTSIDE the transactional shell</span>
</span></span><span class="line"><span class="cl">sudo reboot</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Verification:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">cat /sys/class/drm/card1/device/mem_info_vram_total
</span></span><span class="line"><span class="cl"><span class="c1"># 103079215104 (96 * 1024^3)</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h2>Docker on MicroOS<span class="hx:absolute hx:-mt-20" id="docker-on-microos"></span>
    <a href="#docker-on-microos" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo transactional-update --non-interactive pkg install docker
</span></span><span class="line"><span class="cl">sudo reboot
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">sudo systemctl <span class="nb">enable</span> --now docker
</span></span><span class="line"><span class="cl">sudo usermod -aG docker akitaonrails
</span></span><span class="line"><span class="cl"><span class="c1"># logout and login for the group to take effect</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Install standalone docker-compose (the openSUSE package doesn&#39;t include it)</span>
</span></span><span class="line"><span class="cl">sudo curl -L <span class="s2">&#34;https://github.com/docker/compose/releases/latest/download/docker-compose-linux-x86_64&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  -o /usr/local/bin/docker-compose
</span></span><span class="line"><span class="cl">sudo chmod +x /usr/local/bin/docker-compose
</span></span><span class="line"><span class="cl">mkdir -p ~/.docker/cli-plugins
</span></span><span class="line"><span class="cl">ln -s /usr/local/bin/docker-compose ~/.docker/cli-plugins/docker-compose</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h3>daemon.json<span class="hx:absolute hx:-mt-20" id="daemonjson"></span>
    <a href="#daemonjson" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;log-level&#34;</span><span class="p">:</span> <span class="s2">&#34;warn&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;log-driver&#34;</span><span class="p">:</span> <span class="s2">&#34;local&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;log-opts&#34;</span><span class="p">:</span> <span class="p">{</span><span class="nt">&#34;max-size&#34;</span><span class="p">:</span> <span class="s2">&#34;10m&#34;</span><span class="p">,</span> <span class="nt">&#34;max-file&#34;</span><span class="p">:</span> <span class="s2">&#34;5&#34;</span><span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;selinux-enabled&#34;</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;live-restore&#34;</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;userland-proxy&#34;</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;exec-opts&#34;</span><span class="p">:</span> <span class="p">[</span><span class="s2">&#34;native.cgroupdriver=systemd&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p><code>live-restore: true</code> keeps containers alive across a Docker daemon restart. <code>userland-proxy: false</code> uses iptables directly instead of proxy processes (less overhead). <code>selinux-enabled: true</code> is mandatory on MicroOS.</p>
<h2>SELinux and Docker: the biggest source of problems<span class="hx:absolute hx:-mt-20" id="selinux-and-docker-the-biggest-source-of-problems"></span>
    <a href="#selinux-and-docker-the-biggest-source-of-problems" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>This deserves an entire section because it was responsible for 90% of the bugs during the migration.</p>
<p>On MicroOS with SELinux enforcing, every container that writes to a host bind-mounted directory needs special handling. There are two approaches: the <code>:Z</code> suffix on volumes and the <code>security_opt: label:disable</code> option.</p>
<h3>NEVER use <code>:Z</code>. Use <code>security_opt: label:disable</code>.<span class="hx:absolute hx:-mt-20" id="never-use-z-use-security_opt-labeldisable"></span>
    <a href="#never-use-z-use-security_opt-labeldisable" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p><code>:Z</code> tells Docker to relabel the host directory with the container&rsquo;s SELinux context. Sounds like the right thing. In practice:</p>
<ul>
<li>SQLite databases break. The relabeling changes the file&rsquo;s context and SQLite may refuse to open the WAL journal.</li>
<li>NFS mounts silently ignore <code>:Z</code>. NFS doesn&rsquo;t support SELinux xattrs. The kernel ignores the flag without error, but the container still doesn&rsquo;t have permission.</li>
<li><code>:ro,Z</code> mounts try to relabel even when read-only, which fails on NFS and can corrupt the context on local paths.</li>
</ul>
<p>The right solution for every container on this system:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">myservice</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">security_opt</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">label:disable    </span><span class="w"> </span><span class="c"># disables SELinux enforcement for this container</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">./data:/data     </span><span class="w"> </span><span class="c"># NO :Z</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">./config.yml:/etc/config.yml:ro </span><span class="w"> </span><span class="c"># NO :Z even on :ro</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p><code>label:disable</code> disables SELinux label enforcement for that container only, not for the entire system. Combined with Docker&rsquo;s network and process isolation, it&rsquo;s safe for a home server.</p>
<h2>Migrating the stacks<span class="hx:absolute hx:-mt-20" id="migrating-the-stacks"></span>
    <a href="#migrating-the-stacks" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>All Docker stacks were reorganized into <code>/var/opt/docker/&lt;stack&gt;/docker-compose.yml</code>. On the old server, they were scattered across <code>/home/akitaonrails/docker/</code>, <code>/home/akitaonrails/&lt;service&gt;/</code>, with no pattern.</p>
<h3>Substitutions applied across every compose file<span class="hx:absolute hx:-mt-20" id="substitutions-applied-across-every-compose-file"></span>
    <a href="#substitutions-applied-across-every-compose-file" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><table>
  <thead>
      <tr>
          <th>Before</th>
          <th>After</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>/mnt/terachad/</code></td>
          <td><code>/var/mnt/terachad/</code></td>
      </tr>
      <tr>
          <td><code>192.168.0.145</code></td>
          <td><code>192.168.0.90</code></td>
      </tr>
      <tr>
          <td><code>/home/akitaonrails/&lt;service&gt;/</code></td>
          <td><code>/var/opt/docker/&lt;stack&gt;/&lt;service&gt;/</code></td>
      </tr>
      <tr>
          <td><code>OLLAMA_BASE_URL=http://192.168.0.14:11434</code></td>
          <td><code>OLLAMA_BASE_URL=http://192.168.0.90:11434</code></td>
      </tr>
  </tbody>
</table>
<h3>Media stack (Plex, Radarr, Sonarr, etc.)<span class="hx:absolute hx:-mt-20" id="media-stack-plex-radarr-sonarr-etc"></span>
    <a href="#media-stack-plex-radarr-sonarr-etc" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The media stack is the most complex. Plex needs its own LAN IP (macvlan) for direct streaming to work. The setup:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker network create -d macvlan <span class="se">\
</span></span></span><span class="line"><span class="cl">  --subnet<span class="o">=</span>192.168.0.0/24 <span class="se">\
</span></span></span><span class="line"><span class="cl">  --gateway<span class="o">=</span>192.168.0.1 <span class="se">\
</span></span></span><span class="line"><span class="cl">  -o <span class="nv">parent</span><span class="o">=</span>enp97s0 <span class="se">\
</span></span></span><span class="line"><span class="cl">  plex_macvlan</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>In compose, Plex needs to be on two networks: the macvlan (for the IP 192.168.0.6) and the default bridge (so other containers like Seerr can talk to it):</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">plex</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">networks</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">plex_macvlan</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">ipv4_address</span><span class="p">:</span><span class="w"> </span><span class="m">192.168.0.6</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">mac_address</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;02:42:c0:a8:00:06&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">default</span><span class="p">:</span><span class="w"> </span>{}<span class="w">    </span><span class="c"># mandatory — without this, Seerr can&#39;t see Plex</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>A detail that almost broke me: Plex stores absolute paths in its database. If the container&rsquo;s internal volume changed from <code>/media</code> to <code>/data</code>, Plex no longer finds anything. You have to use exactly the same mount target as the old compose.</p>
<h3>Ollama with ROCm<span class="hx:absolute hx:-mt-20" id="ollama-with-rocm"></span>
    <a href="#ollama-with-rocm" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>New stack, didn&rsquo;t exist on the previous server:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">ollama</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">ollama/ollama:rocm</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">container_name</span><span class="p">:</span><span class="w"> </span><span class="l">ollama</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">devices</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">/dev/kfd:/dev/kfd</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">/dev/dri:/dev/dri</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">security_opt</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">seccomp:unconfined</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">label:disable</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">group_add</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="s2">&#34;485&#34;</span><span class="w">   </span><span class="c"># render group GID</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="s2">&#34;488&#34;</span><span class="w">   </span><span class="c"># video group GID</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">HSA_OVERRIDE_GFX_VERSION=11.5.1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">PYTORCH_ROCM_ARCH=gfx1151</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">OLLAMA_KEEP_ALIVE=30m</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">OLLAMA_NUM_PARALLEL=4</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">OLLAMA_FLASH_ATTENTION=1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">OLLAMA_KV_CACHE_TYPE=q8_0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">/var/lib/ollama:/root/.ollama</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="s2">&#34;11434:11434&#34;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p><code>OLLAMA_FLASH_ATTENTION=1</code> activates flash attention. <code>OLLAMA_KV_CACHE_TYPE=q8_0</code> uses 8-bit KV cache, cutting the bandwidth needed per token in half. Free performance optimizations.</p>
<h3>Monitoring (Grafana + Prometheus)<span class="hx:absolute hx:-mt-20" id="monitoring-grafana--prometheus"></span>
    <a href="#monitoring-grafana--prometheus" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Grafana uses a named volume (<code>grafana_data</code>) which is NOT included in normal filesystem backups. That&rsquo;s the reason I lost all my dashboards on the first attempt. The fix is an explicit backup of the named volume:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># On the old server:</span>
</span></span><span class="line"><span class="cl">docker run --rm -v grafana_data:/data:ro -v /tmp:/backup alpine <span class="se">\
</span></span></span><span class="line"><span class="cl">  tar czf /backup/grafana_data.tar.gz -C /data .
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Transfer and restore on the new one:</span>
</span></span><span class="line"><span class="cl">docker run --rm -v grafana_data:/data -v /tmp:/backup alpine <span class="se">\
</span></span></span><span class="line"><span class="cl">  sh -c <span class="s2">&#34;cd /data &amp;&amp; tar xzf /backup/grafana_data.tar.gz&#34;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Same thing for Portainer (<code>portainer_data</code>). Any volume defined in the <code>volumes:</code> block of a compose without a host path needs this treatment.</p>
<h3>Cloudflare Tunnel<span class="hx:absolute hx:-mt-20" id="cloudflare-tunnel"></span>
    <a href="#cloudflare-tunnel" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>I use <a href="https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/"target="_blank" rel="noopener">Cloudflare Tunnel</a> to access all the services from outside the house without opening ports on the router. The migration was the easiest part: copy the tunnel&rsquo;s JSON credentials file and the <code>config.yml</code>, update the IPs from <code>.145</code> to <code>.90</code>, and bring the container up. The tunnel keeps the same ID, no need to recreate DNS.</p>
<p>The hostnames live in <code>config.yml</code>: portainer, grafana, plex, seerr, qbittorrent, syncthing, radarr, sonarr, bazarr, prowlarr, vault, gitea, kavita, and others. Everything accessible via <code>https://&lt;service&gt;.example.com</code> from anywhere.</p>
<h3>Gitea (image registry)<span class="hx:absolute hx:-mt-20" id="gitea-image-registry"></span>
    <a href="#gitea-image-registry" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p><a href="https://github.com/go-gitea/gitea"target="_blank" rel="noopener">Gitea</a> acts as a private Docker registry on port 3007. The Frank FBI, Frank Mega, Frank Yomik and Mila projects have Docker images that are built and pushed to Gitea. For it to work, Docker&rsquo;s <code>daemon.json</code> needs:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;insecure-registries&#34;</span><span class="p">:</span> <span class="p">[</span><span class="s2">&#34;192.168.0.90:3007&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Gitea SSH gave me a problem during the migration: the old app.ini had <code>SSH_LISTEN_PORT=22</code>, but the container&rsquo;s entrypoint also starts sshd on port 22. Conflict. Fix: <code>GITEA__server__SSH_LISTEN_PORT=2222</code> as an environment variable in compose.</p>
<h3>All 49 containers running<span class="hx:absolute hx:-mt-20" id="all-49-containers-running"></span>
    <a href="#all-49-containers-running" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The migrated server runs 49 containers across 15 stacks. The media stack alone has 10 containers (<a href="https://www.plex.tv/"target="_blank" rel="noopener">Plex</a>, <a href="https://github.com/Radarr/Radarr"target="_blank" rel="noopener">Radarr</a>, <a href="https://github.com/Sonarr/Sonarr"target="_blank" rel="noopener">Sonarr</a>, <a href="https://github.com/morpheus65535/bazarr"target="_blank" rel="noopener">Bazarr</a>, <a href="https://github.com/Prowlarr/Prowlarr"target="_blank" rel="noopener">Prowlarr</a>, <a href="https://github.com/qbittorrent/qBittorrent"target="_blank" rel="noopener">qBittorrent</a>, <a href="https://github.com/sabnzbd/sabnzbd"target="_blank" rel="noopener">SABnzbd</a>, <a href="https://github.com/Jackett/Jackett"target="_blank" rel="noopener">Jackett</a>, <a href="https://github.com/FlareSolverr/FlareSolverr"target="_blank" rel="noopener">FlareSolverr</a>, <a href="https://github.com/seerr/seerr"target="_blank" rel="noopener">Seerr</a>). The personal projects (<a href="https://github.com/akitaonrails/frank_fbi"target="_blank" rel="noopener">Frank FBI</a>, <a href="/en/2026/02/21/vibe-code-built-a-mega-clone-in-rails-in-1-day-frankmega/">Frank Mega</a>, <a href="https://github.com/akitaonrails/FrankYomik"target="_blank" rel="noopener">Frank Yomik</a>, Mila) add another 11. Monitoring with <a href="https://github.com/grafana/grafana"target="_blank" rel="noopener">Grafana</a>, <a href="https://github.com/prometheus/prometheus"target="_blank" rel="noopener">Prometheus</a>, node-exporter and <a href="https://github.com/google/cadvisor"target="_blank" rel="noopener">cAdvisor</a>. Utilities like <a href="https://github.com/portainer/portainer"target="_blank" rel="noopener">Portainer</a>, <a href="https://github.com/dani-garcia/vaultwarden"target="_blank" rel="noopener">Vaultwarden</a>, <a href="https://github.com/syncthing/syncthing"target="_blank" rel="noopener">Syncthing</a>, <a href="https://github.com/causefx/Organizr"target="_blank" rel="noopener">Organizr</a>, <a href="https://github.com/containrrr/watchtower"target="_blank" rel="noopener">Watchtower</a>. <a href="https://github.com/go-gitea/gitea"target="_blank" rel="noopener">Gitea</a> as private Docker registry. <a href="https://github.com/immich-app/immich"target="_blank" rel="noopener">Immich</a> as self-hosted Google Photos. <a href="https://github.com/oae/kaizoku"target="_blank" rel="noopener">Kaizoku</a> for manga with <a href="https://github.com/Kareadita/Kavita"target="_blank" rel="noopener">Kavita</a> as the reader. <a href="https://github.com/ollama/ollama"target="_blank" rel="noopener">Ollama</a> with ROCm. And <a href="https://github.com/bitcoin/bitcoin"target="_blank" rel="noopener">Bitcoin Core</a>/<a href="https://github.com/cculianu/Fulcrum"target="_blank" rel="noopener">Fulcrum</a> indexing the blockchain off the NAS.</p>
<h2>Backups: two layers<span class="hx:absolute hx:-mt-20" id="backups-two-layers"></span>
    <a href="#backups-two-layers" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><h3>Layer 1: local btrfs snapshots (snapper)<span class="hx:absolute hx:-mt-20" id="layer-1-local-btrfs-snapshots-snapper"></span>
    <a href="#layer-1-local-btrfs-snapshots-snapper" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p><code>/var</code> lives on a 3.7TB btrfs partition. snapper creates automatic snapshots: 7 daily + 1 weekly. They&rsquo;re crash-consistent, not application-consistent (postgres can be slightly inconsistent if there&rsquo;s heavy writing during the snapshot).</p>
<p>To recover an accidentally deleted file:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo snapper -c var list
</span></span><span class="line"><span class="cl">sudo cp /var/.snapshots/5/snapshot/opt/docker/media/radarr/appdata/config/radarr.db <span class="se">\
</span></span></span><span class="line"><span class="cl">        /var/opt/docker/media/radarr/appdata/config/radarr.db</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>For a full stack rollback:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo docker compose -p media down
</span></span><span class="line"><span class="cl">sudo snapper -c var undochange 7..0 /var/opt/docker/media
</span></span><span class="line"><span class="cl">sudo docker compose -p media up -d</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h3>Layer 2: restic to the NAS (off-machine)<span class="hx:absolute hx:-mt-20" id="layer-2-restic-to-the-nas-off-machine"></span>
    <a href="#layer-2-restic-to-the-nas-off-machine" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p><a href="https://github.com/restic/restic"target="_blank" rel="noopener">restic</a> runs every night at 3am, doing incremental backups to <code>/var/mnt/terachad/homelab-backups/</code>. Retention: 7 daily + 4 weekly. Content-based deduplication, so Plex config (19GB) and Gitea repos (12GB) only transfer deltas.</p>
<p>Before restic runs, a <code>pg_dump</code> exports the postgres databases (Immich, Kaizoku). The dumps go to <code>/tmp/homelab-db-dumps/</code> and are included in the backup.</p>
<p>What&rsquo;s NOT included in the backup (re-downloadable): Bitcoin blockchain (785GB on the NAS), Docker images (re-pullable), Ollama models (re-downloadable), HuggingFace/EasyOCR caches, Plex transcoding scratch.</p>
<p>Large re-downloadable directories were converted to btrfs subvolumes so snapper ignores them: <code>/var/lib/ollama</code> and <code>/var/opt/docker/bitcoin/fulcrum/fulc2_db</code>.</p>
<h2>Performance tuning<span class="hx:absolute hx:-mt-20" id="performance-tuning"></span>
    <a href="#performance-tuning" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><h3>btrfs with zstd compression<span class="hx:absolute hx:-mt-20" id="btrfs-with-zstd-compression"></span>
    <a href="#btrfs-with-zstd-compression" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Added <code>compress=zstd:1</code> to the fstab for the <code>/var</code> partition. zstd level 1 has near-zero CPU overhead on NVMe and compresses Docker metadata, JSON configs and logs nicely. Incompressible data (SQLite, postgres) is automatically skipped by btrfs.</p>
<h3>zram swap<span class="hx:absolute hx:-mt-20" id="zram-swap"></span>
    <a href="#zram-swap" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>With ~30GB of RAM available for the system (96GB go to VRAM), in-memory compressed swap helps. zram creates a ~15GB swap device (ram/2) with zstd compression, much faster than disk swap.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="c1"># /etc/systemd/zram-generator.conf</span>
</span></span><span class="line"><span class="cl"><span class="k">[zram0]</span>
</span></span><span class="line"><span class="cl"><span class="na">zram-size</span> <span class="o">=</span> <span class="s">ram / 2</span>
</span></span><span class="line"><span class="cl"><span class="na">compression-algorithm</span> <span class="o">=</span> <span class="s">zstd</span>
</span></span><span class="line"><span class="cl"><span class="na">swap-priority</span> <span class="o">=</span> <span class="s">100</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h3>btrfs nodatacow on database directories<span class="hx:absolute hx:-mt-20" id="btrfs-nodatacow-on-database-directories"></span>
    <a href="#btrfs-nodatacow-on-database-directories" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Copy-on-write + random database writes = write amplification. I disabled CoW on the directories that hold SQLite and postgres:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo chattr +C /var/opt/docker/gitea/data/gitea.db
</span></span><span class="line"><span class="cl">sudo chattr +C /var/opt/docker/immich/db/
</span></span><span class="line"><span class="cl">sudo chattr +C /var/opt/docker/media/radarr/appdata/config/</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h3>CPU in performance mode<span class="hx:absolute hx:-mt-20" id="cpu-in-performance-mode"></span>
    <a href="#cpu-in-performance-mode" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>On a headless server, there&rsquo;s no point saving energy:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">echo</span> performance <span class="p">|</span> sudo tee /sys/devices/system/cpu/cpu*/cpufreq/energy_performance_preference</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Persisted via systemd service <code>cpu-epp.service</code>.</p>
<h3>Docker shutdown fix<span class="hx:absolute hx:-mt-20" id="docker-shutdown-fix"></span>
    <a href="#docker-shutdown-fix" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>A problem I discovered: Docker ships with <code>KillMode=process</code>, which means at system shutdown, systemd kills only dockerd and leaves all the <code>containerd-shim</code> processes (one per container, ~49 in my case) orphaned. systemd-shutdown then has to hunt them down one by one after the journal has already stopped, causing a silent multi-minute hang.</p>
<p>Fix:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="c1"># /etc/systemd/system/docker.service.d/shutdown.conf</span>
</span></span><span class="line"><span class="cl"><span class="k">[Service]</span>
</span></span><span class="line"><span class="cl"><span class="na">KillMode</span><span class="o">=</span><span class="s">control-group</span>
</span></span><span class="line"><span class="cl"><span class="na">TimeoutStopSec</span><span class="o">=</span><span class="s">30</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h2>The problems we ran into<span class="hx:absolute hx:-mt-20" id="the-problems-we-ran-into"></span>
    <a href="#the-problems-we-ran-into" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>This is the table of actual problems we hit during the migration. If you&rsquo;re planning something similar, read it before starting:</p>
<table>
  <thead>
      <tr>
          <th>Problem</th>
          <th>Cause</th>
          <th>Fix</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>ROCm only sees 15.5GB of VRAM</td>
          <td>Kernel TTM caps pages even with BIOS at 96GB</td>
          <td>Add <code>amdttm.pages_limit=25165824</code> to kernel cmdline</td>
      </tr>
      <tr>
          <td>Every container: permission denied on volumes</td>
          <td>SELinux <code>container_t</code> can&rsquo;t write to unlabeled paths</td>
          <td><code>security_opt: label:disable</code> on every service</td>
      </tr>
      <tr>
          <td>NFS with <code>:Z</code> silently fails</td>
          <td>NFS doesn&rsquo;t support SELinux xattrs</td>
          <td>Never use <code>:Z</code> on NFS paths</td>
      </tr>
      <tr>
          <td>SQLite breaks with <code>:Z</code></td>
          <td>Relabeling changes the context, WAL mode fails</td>
          <td>Drop <code>:Z</code>, use <code>label:disable</code></td>
      </tr>
      <tr>
          <td>Radarr/Sonarr showed the setup screen</td>
          <td>Backup at <code>appdata/config/</code> but compose mounted <code>appdata/</code></td>
          <td>Fix to: <code>appdata/config:/config</code></td>
      </tr>
      <tr>
          <td>Grafana lost dashboards</td>
          <td>Named volume not included in filesystem backup</td>
          <td>Explicit named volume backup</td>
      </tr>
      <tr>
          <td>Plex can&rsquo;t find media</td>
          <td>Internal path changed from <code>/media</code> to <code>/data</code></td>
          <td>Restore the original path in compose</td>
      </tr>
      <tr>
          <td>Seerr can&rsquo;t connect to Plex</td>
          <td>macvlan isolated from the bridge network</td>
          <td>Add <code>default: {}</code> to Plex networks</td>
      </tr>
      <tr>
          <td>Fulcrum crash: &ldquo;option -b missing&rdquo;</td>
          <td>Env vars not supported by the image</td>
          <td>Use CLI flags in <code>command:</code></td>
      </tr>
      <tr>
          <td>bitcoind rejects RPC</td>
          <td>Binds on <code>::1</code> by default</td>
          <td>Add <code>-rpcbind=0.0.0.0 -rpcallowip=172.0.0.0/8</code></td>
      </tr>
      <tr>
          <td>sdbootutil warning in transactional shell</td>
          <td>Has to run outside the transaction</td>
          <td>Run <code>sdbootutil update-all-entries</code> in the normal shell</td>
      </tr>
      <tr>
          <td>Watchtower permission denied on docker.sock</td>
          <td>SELinux blocks socket access</td>
          <td><code>label:disable</code></td>
      </tr>
      <tr>
          <td>Gitea SSH crash</td>
          <td>Conflict: entrypoint sshd on port 22 + app on port 22</td>
          <td><code>GITEA__server__SSH_LISTEN_PORT=2222</code></td>
      </tr>
      <tr>
          <td>docker-compose not installed with Docker</td>
          <td>The openSUSE package only installs the daemon</td>
          <td>Install standalone binary manually</td>
      </tr>
  </tbody>
</table>
<h2>What to tell Claude Code before you start<span class="hx:absolute hx:-mt-20" id="what-to-tell-claude-code-before-you-start"></span>
    <a href="#what-to-tell-claude-code-before-you-start" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>If I were redoing the migration from scratch, I&rsquo;d give Claude Code these instructions on the very first message. In order of importance:</p>
<p>Tell it that SELinux is enforcing and that it should NEVER use <code>:Z</code> on any Docker volume, but rather <code>security_opt: label:disable</code> on every service. Tell it that <code>/var/mnt/terachad/</code> is an NFS mount and that <code>:Z</code> should never appear on NFS paths. Tell it to always look at the original compose before rewriting and only change IPs, paths and container names, without inventing new volume layouts. Warn that named volumes need explicit backup (Grafana, Portainer). Explain that Plex runs on macvlan and needs <code>default: {}</code> in the networks. Inform that the GPU is gfx1151, not officially supported, and that it needs UMA 96GB in the BIOS + kernel TTM params + <code>HSA_OVERRIDE_GFX_VERSION=11.5.1</code>. And tell it that Bitcoin/Fulcrum don&rsquo;t process environment variables, everything goes as an argument in <code>command:</code>.</p>
<p>Those instructions would have prevented 80% of the problems we hit.</p>
<h2>Final server layout<span class="hx:absolute hx:-mt-20" id="final-server-layout"></span>
    <a href="#final-server-layout" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>/var/opt/docker/
├── bitcoin/          (bitcoind &#43; fulcrum)
├── cloudflared/      (Cloudflare tunnel)
├── frank_fbi/        (email fraud analysis)
├── frank_mega/       (Mega clone)
├── frank_yomik/      (manga translation)
├── gitea/            (Docker registry)
├── immich/           (self-hosted Google Photos)
├── kaizoku/          (manga downloader &#43; reader)
├── media/            (Plex &#43; *arr stack)
├── mila/             (Discord bot)
├── monitor/          (Grafana &#43; Prometheus)
├── ollama/           (local LLM with ROCm)
├── rip/              (HandBrake)
└── utils/            (Portainer, Vaultwarden, Syncthing, etc.)

/var/mnt/terachad/    (Synology NFS)
├── Bitcoin/data/     (blockchain, 785GB)
├── Downloads/        (torrents &#43; nzbget)
├── Videos/           (Radarr movies &#43; Sonarr series)
└── Ollama/models/    (model overflow if local disk fills up)</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h2>A warning about using AI to administer servers<span class="hx:absolute hx:-mt-20" id="a-warning-about-using-ai-to-administer-servers"></span>
    <a href="#a-warning-about-using-ai-to-administer-servers" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>I used Claude Code to speed up the migration. It created compose files, wrote backup scripts, configured the firewall, diagnosed SELinux problems. It worked well for my case: home server, only I use it, and I was reviewing every step.</p>
<p>But there are traps. Claude doesn&rsquo;t know that <code>:Z</code> breaks SQLite unless you tell it. It doesn&rsquo;t know that Fulcrum doesn&rsquo;t accept env vars unless it&rsquo;s already seen the Dockerfile. It will invent &ldquo;better&rdquo; volume layouts that break Plex because Plex stores absolute paths in its database.</p>
<p>If it were real production: don&rsquo;t do this without review. Every compose file Claude generates, read it in full before applying. Every destructive command (rollback, delete, recreate), confirm manually. And have tested backups before you start. Claude is great for generating the first version and diagnosing errors, but the architecture decisions and the safety validations are yours.</p>
<p>The previous home server posts that may give additional context:</p>
<ul>
<li><a href="/en/2024/04/03/my-personal-netflix-with-docker-compose/">My &ldquo;Personal Netflix&rdquo; with Docker Compose</a></li>
<li><a href="/en/2025/09/09/accessing-my-home-server-with-a-real-domain/">Accessing my Home Server with a real domain</a></li>
<li><a href="/en/2025/09/10/protecting-your-home-server-with-cloudflare-zero-trust/">Protecting your Home Server with Cloudflare Zero Trust</a></li>
<li><a href="/en/2025/09/10/installing-grafana-on-my-home-server/">Installing Grafana on my Home Server</a></li>
<li><a href="/en/2025/09/10/omarchy-2-0-bitwarden-self-hosted-vaultwarden/">Self-hosted Vaultwarden</a></li>
</ul>
]]></content:encoded><category>homelab</category><category>containers</category><category>devops</category><category>security</category></item><item><title>Omarchy 2.0 - Bitwarden Self-Hosted / VaultWarden</title><link>https://akitaonrails.github.io/en/2025/09/10/omarchy-2-0-bitwarden-self-hosted-vaultwarden/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2025/09/10/omarchy-2-0-bitwarden-self-hosted-vaultwarden/</guid><pubDate>Wed, 10 Sep 2025 20:00:00 -0300</pubDate><description>&lt;p&gt;Continuing the &lt;a href="https://akitaonrails.github.io/en/tags/omarchy/"&gt;Omarchy series&lt;/a&gt; and doing a cross-over with the &lt;a href="https://akitaonrails.github.io/en/tags/homeserver/"&gt;home server series&lt;/a&gt;, let&amp;rsquo;s talk about Password Managers and 2FA. Anyone who follows me knows I talk often about personal digital security, the importance of paying attention to links so you don&amp;rsquo;t fall for scams, phishing, or accidentally install malware.&lt;/p&gt;
&lt;p&gt;More important: having a strong, random, exclusive and different password for &lt;strong&gt;EACH&lt;/strong&gt; site, app or service you use. Never, ever, reuse a password. Why? Because every day one of these sites gets breached and passwords get leaked. If you search for yourself on &lt;a href="https://haveibeenpwned.com/"target="_blank" rel="noopener"&gt;Have I Been Pwned&lt;/a&gt; you&amp;rsquo;ll surely find accounts of yours that have already leaked. And if you share that same password across 10 other sites, someone has probably already gotten into your account by now.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Continuing the <a href="/en/tags/omarchy/">Omarchy series</a> and doing a cross-over with the <a href="/en/tags/homeserver/">home server series</a>, let&rsquo;s talk about Password Managers and 2FA. Anyone who follows me knows I talk often about personal digital security, the importance of paying attention to links so you don&rsquo;t fall for scams, phishing, or accidentally install malware.</p>
<p>More important: having a strong, random, exclusive and different password for <strong>EACH</strong> site, app or service you use. Never, ever, reuse a password. Why? Because every day one of these sites gets breached and passwords get leaked. If you search for yourself on <a href="https://haveibeenpwned.com/"target="_blank" rel="noopener">Have I Been Pwned</a> you&rsquo;ll surely find accounts of yours that have already leaked. And if you share that same password across 10 other sites, someone has probably already gotten into your account by now.</p>
<p>Watch my video about security:</p>


<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/s7ldn31OEFc"
    title="YouTube video player"
    frameborder="0"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
    referrerpolicy="strict-origin-when-cross-origin"
    allowfullscreen>
  </iframe>
</div>

<p>Being too lazy to use a password manager is just a dumb excuse from lazy people. And a programmer has zero right to use that excuse.</p>
<h3>Omarchy 1Password<span class="hx:absolute hx:-mt-20" id="omarchy-1password"></span>
    <a href="#omarchy-1password" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Anyone who has followed DHH since the early days when Macs were the best platform for developers remembers he was always a fan of 1Password, one of the apps that worked best on iPhones.</p>
<p>I migrated back to Android and Linux many years ago, in 2015. I used <strong>1Password</strong>, but on Android the app was horrendous, ugly, and painful to use. That&rsquo;s why I gave up on it and kept bouncing between several other apps. Google Authenticator, Lastpass (which I don&rsquo;t recommend).</p>
<p>I know there are several other open source ones that people prefer, but I ended up settling on Bitwarden, which is <strong>BEAUTIFUL</strong> both on Android and as a browser extension. On top of that, it has open components and allows <strong>self-hosting</strong>.</p>
<p>More than that, it doesn&rsquo;t make it hard to export all the data from your account and import it somewhere else. Unlike apps like Twilio Authy, which locks you out of everything and is why I stopped using it.</p>
<p>Bitwarden has a secure random password generator, supports <strong>2FA TOTP</strong>, lets you store secure notes (mini texts) and a lot more. The apps integrate with phone biometrics, so it&rsquo;s very convenient to use.</p>
<p>I still keep my Bitwarden inside Samsung&rsquo;s <a href="https://www.samsung.com/global/sustainability/popup/popup_doc/AYUqoB6qDDMAIx_C/"target="_blank" rel="noopener">Secure Vault</a>, the <strong>Samsung Knox Vault</strong>. So there are 2 layers of security. And when I need to walk around somewhere suspicious, I don&rsquo;t take my main phone, only a burner phone with nothing important logged in.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20250910193713_bitwarden-generator.png" alt="Bitwarden Generator"  loading="lazy" /></p>
<p>Look how beautiful it is. I like it and I&rsquo;ve been using it for a few years now. My passwords range from 21 to 42 random characters, a different one for each site. I&rsquo;ve updated all the passwords for everything more than once in the past few years, and all accounts have 2FA/TOTP registered inside Bitwarden too, which is how I managed to drop Authy.</p>
<h3>Installing VaultWarden Self-Hosted<span class="hx:absolute hx:-mt-20" id="installing-vaultwarden-self-hosted"></span>
    <a href="#installing-vaultwarden-self-hosted" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>More important, the time has come to host my own Bitwarden server, <a href="https://github.com/dani-garcia/vaultwarden"target="_blank" rel="noopener"><strong>vaultwarden</strong></a>, which is an open source alternative, code open on GitHub, so you can poke around yourself to learn or even contribute to it. Again, I bring everything up with Docker Compose on my Home Server, so I just needed to add this snippet to my file:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="w">  </span><span class="nt">vaultwarden</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">vaultwarden/server:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">container_name</span><span class="p">:</span><span class="w"> </span><span class="l">vaultwarden</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">DOMAIN</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;https://bitwarden.fabioakita.dev&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">SIGNUPS_ALLOWED</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;false&#34;</span><span class="w">          </span><span class="c"># set to true only to create first user</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">WEBSOCKET_ENABLED</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;true&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">ADMIN_TOKEN</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;iA...Q9&#34;</span><span class="w">  </span><span class="c"># for /admin</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="c"># SMTP (recommended)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">SMTP_HOST</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;smtp.gmail.com&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">SMTP_PORT</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;587&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">SMTP_SECURITY</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;starttls&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">SMTP_USERNAME</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;boss@akitaonrails.com&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">SMTP_PASSWORD</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;xxx&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">SMTP_FROM</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;boss@akitaonrails.com&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/home/akitaonrails/vaultwarden/data:/data</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;9999:80&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">healthcheck</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">test</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;CMD&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;wget&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;-q&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;-O&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;-&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;http://localhost/health&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">interval</span><span class="p">:</span><span class="w"> </span><span class="l">30s</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">timeout</span><span class="p">:</span><span class="w"> </span><span class="l">5s</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">retries</span><span class="p">:</span><span class="w"> </span><span class="m">5</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Important:</p>
<ul>
<li>Create a long, random <code>ADMIN_TOKEN</code>. Use Bitwarden itself for that, keep it secret.</li>
<li>Set <code>SIGNUPS_ALLOWED</code> to <code>true</code> only once, log into the local site and create your new account. Then change it back to <code>false</code> in the file and restart the container. That way nobody else will be able to create any new accounts.</li>
<li>It&rsquo;s recommended to configure a GMAIL SMTP to send notification emails. For that, you need to create an app password in Google.</li>
</ul>
<p>To create a password exclusively for vaultwarden, go to <a href="https://myaccount.google.com/apppasswords"target="_blank" rel="noopener">Google App Passwords</a> and generate a new exclusive secret password:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20250910194453_screenshot-2025-09-10_16-34-48.png" alt="App Password"  loading="lazy" /></p>
<p>It will only show you that password once. Copy and paste it straight into the docker compose yaml. With email properly configured, every time someone logs in, it sends an email notification. That&rsquo;s good to make sure nobody shady is messing with your server:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20250910201642_screenshot-2025-09-10_20-16-25.png" alt="notification"  loading="lazy" /></p>
<p>Now I just need to bring up the container:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker compose -f utils-docker-compose.yml up -d</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h3>Routing the Cloudflared tunnel<span class="hx:absolute hx:-mt-20" id="routing-the-cloudflared-tunnel"></span>
    <a href="#routing-the-cloudflared-tunnel" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>As I already showed in the <a href="https://akitaonrails.com/en/2025/09/09/accessing-my-home-server-with-a-real-domain/"target="_blank" rel="noopener">Cloudflared post</a> now I want to be able to access it remotely, via secure tunnel, using a public URL like <code>https://bitwarden.fabioakita.dev</code>. To do that we have to edit that <code>/etc/cloudflared/config.yml</code> file and add this to the end of it:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">hostname</span><span class="p">:</span><span class="w"> </span><span class="l">bitwarden.fabioakita.dev</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">service</span><span class="p">:</span><span class="w"> </span><span class="l">http://192.168.0.200:9999</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">originRequest</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">httpHostHeader</span><span class="p">:</span><span class="w"> </span><span class="l">bitwarden.fabioakita.dev</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">http2Origin</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">service</span><span class="p">:</span><span class="w"> </span><span class="l">http_status:404</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Then we have to restart the service and also add the route manually:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo cloudflared tunnel route dns homelab bitwarden.fabioakita.dev
</span></span><span class="line"><span class="cl">sudo systemctl restart cloudflared</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>And done! It might take up to 5 minutes for DNS to sync and all that, but then just hop into your browser and you&rsquo;ll see this:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20250910194927_screenshot-2025-09-10_19-49-14.png" alt="VaultWarden Login"  loading="lazy" /></p>
<p>If that <code>SIGNUPS_ALLOWED</code> is <code>true</code> there will also be a link to &ldquo;Create new account&rdquo;. In my case I already did that and turned it off, so there&rsquo;s only login.</p>
<h3>Importing your Data<span class="hx:absolute hx:-mt-20" id="importing-your-data"></span>
    <a href="#importing-your-data" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Before moving on, let&rsquo;s open the Bitwarden extension - which is still logged into the cloud version at bitwarden.com - and export all my data:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20250910195209_bitwarden-export.png" alt="Export Vault"  loading="lazy" /></p>
<p>Just authenticate, then on the bottom icon bar you&rsquo;ll find &ldquo;Settings&rdquo;, then &ldquo;Vault Options&rdquo; and finally &ldquo;Export vault&rdquo;. Leave the default JSON format (it will be an <strong>encrypted JSON</strong>) and download that file somewhere safe.</p>
<p>Now we can log back into our self-hosted version at <code>https://bitwarden.fabioakita.dev</code> and go through the same menu, but choose &ldquo;Import&rdquo;:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20250910195410_screenshot-2025-09-10_19-53-56.png" alt="Import"  loading="lazy" /></p>
<p>And done! Now my self-hosted version is <strong>identical</strong> to the cloud version I had been using until now.</p>
<h3>Logging into the new self-hosted server<span class="hx:absolute hx:-mt-20" id="logging-into-the-new-self-hosted-server"></span>
    <a href="#logging-into-the-new-self-hosted-server" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Now just log out of both the mobile App and the browser extension. Then on the login screen you&rsquo;ll see the following option:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20250910195632_bitwarden-login.png" alt="Login self-hosted"  loading="lazy" /></p>
<p>On the web extension the window doesn&rsquo;t let you see that option, use the mouse scroll and scroll to the bottom of the window and you&rsquo;ll see that small option. Choose &ldquo;self-hosted&rdquo; and a window will open to type the URL <code>https://bitwarden.fabioakita.dev</code> and done!</p>
<p>From here on the usage is exactly the same as before. The interface is the same, the options are the same. Configure <strong>biometrics</strong>, PIN, lock timeout and everything else the way you like it.</p>
<p>Do the same on the mobile app and done!</p>
<p>On this new account we created on the self-hosted server, don&rsquo;t forget to enable 2FA too, which is the &ldquo;Authenticator App&rdquo; option under &ldquo;Settings/Security&rdquo;:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20250910200009_screenshot-2025-09-10_19-59-55.png" alt="2FA"  loading="lazy" /></p>
<p>For all other accounts, we can register the 2FA inside Bitwarden. But for the master account of Bitwarden/Vaultwarden itself, we need to use a second app just for that.</p>
<p>I don&rsquo;t know if it&rsquo;s the best, but I&rsquo;ve been enjoying using <a href="https://getaegis.app/"target="_blank" rel="noopener">Aegis Authenticator</a> on my Android:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20250910203243_hero.7C782sEM.png" alt="Aegis"  loading="lazy" /></p>
<p>It&rsquo;s open source, the code is on <a href="https://github.com/beemdevelopment/aegis"target="_blank" rel="noopener">GitHub</a>, so it seems minimally trustworthy. I also keep it inside the Samsung Knox Vault. I only need it when I have to re-login into Bitwarden/Vaultwarden. To log into Google, Steam or any other account, the 2FA for each one lives inside Vaultwarden itself:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20250910200449_2fa.png" alt="2FA"  loading="lazy" /></p>
<h3>Conclusion<span class="hx:absolute hx:-mt-20" id="conclusion"></span>
    <a href="#conclusion" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Now all that&rsquo;s left is to log in with my old account at <strong>bitwarden.com</strong>, which is the &ldquo;official&rdquo; cloud version, and delete my account, to make sure nobody else has access.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20250910200615_screenshot-2025-09-10_20-06-04.png" alt="Deletar"  loading="lazy" /></p>
<p>And there, <strong>PERMANENTLY DELETED</strong>! My accounts no longer exist on some random server I don&rsquo;t control, even if they were encrypted.</p>
<p>I&rsquo;m pretty happy with this solution. Now my Vaultwarden is in a container, the home server data gets automatic backups to my NAS, everything is encrypted and access only happens via Cloudflare&rsquo;s secure tunnel. The risk is very low and nobody has a chance of getting access to my data, because it&rsquo;s no longer in the &ldquo;cloud&rdquo;.</p>
<p>My data is mine alone.</p>
]]></content:encoded><category>omarchy</category><category>homelab</category><category>containers</category><category>security</category></item><item><title>Installing Grafana on My Home Server</title><link>https://akitaonrails.github.io/en/2025/09/10/installing-grafana-on-my-home-server/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2025/09/10/installing-grafana-on-my-home-server/</guid><pubDate>Wed, 10 Sep 2025 16:00:00 -0300</pubDate><description>&lt;p&gt;This post continues my &lt;a href="https://akitaonrails.github.io/en/tags/homeserver/"&gt;home server series&lt;/a&gt;, once again serving as notes for my future self. As I explained before, my home server is an Intel NUC mini-PC that I only access via SSH. So I wanted a way to monitor system resource usage without having to manually SSH in and open BTOP or NTOP.&lt;/p&gt;
&lt;p&gt;I had been too lazy to install Grafana back then, but I finally got around to it, and it was much easier than I expected, which is why I decided to share how I did it.&lt;/p&gt;</description><content:encoded><![CDATA[<p>This post continues my <a href="/en/tags/homeserver/">home server series</a>, once again serving as notes for my future self. As I explained before, my home server is an Intel NUC mini-PC that I only access via SSH. So I wanted a way to monitor system resource usage without having to manually SSH in and open BTOP or NTOP.</p>
<p>I had been too lazy to install Grafana back then, but I finally got around to it, and it was much easier than I expected, which is why I decided to share how I did it.</p>
<p>The idea is to spin up <a href="https://github.com/google/cadvisor"target="_blank" rel="noopener">cAdvisor</a>, which monitors container resources, and <a href="https://github.com/prometheus/prometheus"target="_blank" rel="noopener">Prometheus</a>, which scrapes system resource data, then consolidate the data from both into <a href="https://grafana.com/"target="_blank" rel="noopener">Grafana</a>, a web application that lets you build graphical dashboards to monitor your systems. If you do it right, in the end you&rsquo;ll have a screen like this:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20250910155108_screenshot-2025-09-10_15-50-49.png" alt="Grafana Dashboard"  loading="lazy" /></p>
<h3>Installation<span class="hx:absolute hx:-mt-20" id="installation"></span>
    <a href="#installation" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Spinning this up on my home server is very simple. I do everything with Docker Compose, so:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="c"># monitor-docker-compose.yml</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">networks</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">monitoring</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">driver</span><span class="p">:</span><span class="w"> </span><span class="l">bridge</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">prometheus_data</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">grafana_data</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">node-exporter</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">quay.io/prometheus/node-exporter:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">command</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- --<span class="l">path.rootfs=/host</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">pid</span><span class="p">:</span><span class="w"> </span><span class="l">host</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">networks</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">monitoring]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/:/host:ro,rslave</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">security_opt</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;no-new-privileges:true&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">read_only</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">cadvisor</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">gcr.io/cadvisor/cadvisor:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">networks</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">monitoring]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;8080:8080&#34;</span><span class="nt">]   # optional</span><span class="p">:</span><span class="w"> </span><span class="l">expose UI locally</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/:/rootfs:ro</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/var/run:/var/run:ro</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/sys:/sys:ro</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/var/lib/docker/:/var/lib/docker:ro</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/dev/disk/:/dev/disk:ro</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">security_opt</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;no-new-privileges:true&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">read_only</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">prometheus</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">prom/prometheus:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">networks</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">monitoring]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">prometheus_data:/prometheus</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">./prometheus.yml:/etc/prometheus/prometheus.yml:ro</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">command</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- --<span class="l">config.file=/etc/prometheus/prometheus.yml</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- --<span class="l">storage.tsdb.path=/prometheus</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- --<span class="l">storage.tsdb.retention.time=30d</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- --<span class="l">web.enable-lifecycle</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;9090:9090&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">security_opt</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;no-new-privileges:true&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">read_only</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">grafana</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">grafana/grafana:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">networks</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">monitoring]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">depends_on</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">prometheus]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;3001:3000&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">GF_SECURITY_ADMIN_USER</span><span class="p">:</span><span class="w"> </span><span class="l">admin</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">GF_SECURITY_ADMIN_PASSWORD</span><span class="p">:</span><span class="w"> </span><span class="l">change-me</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">GF_USERS_ALLOW_SIGN_UP</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;false&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">grafana_data:/var/lib/grafana</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">./provisioning/datasources:/etc/grafana/provisioning/datasources:ro</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">security_opt</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;no-new-privileges:true&#34;</span><span class="p">]</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Pay attention to the local directory mappings. If you put <code>/etc/grafana/prometheus.yml</code>, for example, you have to change this line:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nn">...</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">prometheus_data:/prometheus</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/etc/grafana/prometheus.yml:/etc/prometheus/prometheus.yml:ro</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nn">...</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Speaking of which, here&rsquo;s the <code>prometheus.yml</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">global</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">scrape_interval</span><span class="p">:</span><span class="w"> </span><span class="l">15s</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">scrape_configs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">job_name</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;prometheus&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">static_configs</span><span class="p">:</span><span class="w"> </span><span class="p">[</span>{<span class="w"> </span><span class="nt">targets</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s1">&#39;prometheus:9090&#39;</span><span class="p">]</span><span class="w"> </span>}<span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">job_name</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;node&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">static_configs</span><span class="p">:</span><span class="w"> </span><span class="p">[</span>{<span class="w"> </span><span class="nt">targets</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s1">&#39;node-exporter:9100&#39;</span><span class="p">]</span><span class="w"> </span>}<span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">job_name</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;cadvisor&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">static_configs</span><span class="p">:</span><span class="w"> </span><span class="p">[</span>{<span class="w"> </span><span class="nt">targets</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s1">&#39;cadvisor:8080&#39;</span><span class="p">]</span><span class="w"> </span>}<span class="p">]</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>And finally <code>/etc/grafana/provisioning/datasources/datasource.yaml</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>apiVersion: 1
datasources:
  - name: Prometheus
    type: prometheus
    access: proxy
    url: http://prometheus:9090
    isDefault: true</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Be careful with port mappings too. The recommended approach is this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">ports</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;127.0.0.1:8080:8080&#34;</span><span class="p">]</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>That&rsquo;s because cAdvisor and Prometheus only need to be accessible to Grafana, inside the home server, and there&rsquo;s no reason to bind on <code>0.0.0.0</code> and have them reachable from outside the server. That&rsquo;s your call. Since I&rsquo;m on a home LAN and there are no tunnels pointing to these services, it makes no difference to me.</p>
<p>If, like me, you already have a service sitting on port 8080, cAdvisor will fail. In that case, remap the ports and reconfigure Prometheus, or just remove the port line. You don&rsquo;t need cAdvisor exposed externally.</p>
<p>Another detail: by default, Grafana comes up on port 3000, but I changed it to 3001 because I already had another service on that port. So once it&rsquo;s up, I can access it at <code>http://192.168.0.200:3001</code>.</p>
<p>As I explained in the <a href="/en/2025/09/09/accessing-my-home-server-with-a-real-domain/">Cloudflared article</a>, I route this address through Cloudflare to get a nice domain like <code>https://grafana.fabioakita.dev</code> to access it both locally and remotely, if I want. Go read those other articles afterward to understand how.</p>
<p>And that&rsquo;s it, now just spin up the containers:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker compose -f monitor-docker-compose.yml up -d</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Done, super simple. I should have done this sooner.</p>
<p>As you can see in the Docker Compose file, when you open the site for the first time, the login is <code>admin</code> with password <code>change-me</code>. Obviously, the first thing to do is change the password:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20250910160042_screenshot-2025-09-10_16-00-31.png" alt="Admin Password"  loading="lazy" /></p>
<h3>Dashboards<span class="hx:absolute hx:-mt-20" id="dashboards"></span>
    <a href="#dashboards" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Now we&rsquo;re practically done. We can check whether Prometheus is working and integrated. Just go to the &ldquo;Connections&rdquo; menu and &ldquo;Data Sources&rdquo;:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20250910160151_screenshot-2025-09-10_16-01-40.png" alt="Data Sources"  loading="lazy" /></p>
<p>Grafana can consolidate data from dozens of different sources, and Prometheus is only one of them. In a larger, more complex infrastructure, this list would be huge.</p>
<p>To build dashboards, you can start from scratch and assemble them widget by widget, but it&rsquo;s easier to import dashboards that already exist. Just click the magnifying glass at the top, in the search box, and choose &ldquo;Import dashboards&rdquo;:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20250910160509_screenshot-2025-09-10_16-04-59.png" alt="import dashboards menu"  loading="lazy" /></p>
<p>On that page you can either import a JSON file, copy and paste the JSON, or search by an ID already registered on grafana.com:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20250910160603_screenshot-2025-09-10_16-05-46.png" alt="import by ID"  loading="lazy" /></p>
<p>Right off the bat, I learned that there are 3 IDs that are useful for me:</p>
<ul>
<li><strong>1860 - Node Exporter Full</strong> - the screenshot at the beginning of this article, with several system metrics like CPU, memory, disk, network, etc. The most useful one for me.</li>
<li><strong>13484 - Docker/cAdvisor Exporter</strong> - CPU and memory metrics, but broken down per container running on the server.</li>
<li><strong>3662 - Prometheus 2.0 overview</strong> - a general health summary.</li>
</ul>
<p>If you like these or other dashboards, just favorite them with a &ldquo;pin&rdquo; and you&rsquo;re set.</p>
<p>For example, here&rsquo;s the cAdvisor dashboard:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20250910160927_screenshot-2025-09-10_16-09-13.png" alt="cAdvisor dashboard"  loading="lazy" /></p>
<p>I haven&rsquo;t explored other dashboards yet. If you have ID recommendations to import, drop them in the comments below!</p>
<p>With this, I have a quick and good-looking view of my home server&rsquo;s resource usage, and I can spot anything weird happening right away. These services don&rsquo;t seem to consume many resources, so I can leave them running alongside the other containers.</p>
]]></content:encoded><category>homelab</category><category>containers</category><category>devops</category></item><item><title>Accessing My Home Server With a Real Domain</title><link>https://akitaonrails.github.io/en/2025/09/09/accessing-my-home-server-with-a-real-domain/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2025/09/09/accessing-my-home-server-with-a-real-domain/</guid><pubDate>Tue, 09 Sep 2025 17:00:00 -0300</pubDate><description>&lt;p&gt;Now an advanced tip that complements my article about my &lt;a href="https://akitaonrails.com/2024/04/03/meu-netflix-pessoal-com-docker-compose/"target="_blank" rel="noopener"&gt;Home Server&lt;/a&gt;. In short, besides my desktop PC, which now runs Omarchy, I have a mini-PC (a simple Intel NUC, it doesn&amp;rsquo;t need to be very powerful), exclusively to run Docker containers.&lt;/p&gt;
&lt;p&gt;I have a nice Synology DS1821+ that also supports running Docker, but I prefer to keep the NAS doing only NAS duties and avoid risking my data by running other stuff on it. Having a single server that does everything isn&amp;rsquo;t a &amp;ldquo;problem&amp;rdquo; per se, but since I can, I prefer to separate things.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Now an advanced tip that complements my article about my <a href="https://akitaonrails.com/2024/04/03/meu-netflix-pessoal-com-docker-compose/"target="_blank" rel="noopener">Home Server</a>. In short, besides my desktop PC, which now runs Omarchy, I have a mini-PC (a simple Intel NUC, it doesn&rsquo;t need to be very powerful), exclusively to run Docker containers.</p>
<p>I have a nice Synology DS1821+ that also supports running Docker, but I prefer to keep the NAS doing only NAS duties and avoid risking my data by running other stuff on it. Having a single server that does everything isn&rsquo;t a &ldquo;problem&rdquo; per se, but since I can, I prefer to separate things.</p>
<p>This Intel NUC runs a headless Ubuntu Server LTS (no desktop manager), which I access only via SSH (as I explained in the <a href="https://akitaonrails.com/en/2025/09/09/omarchy-2-0-understanding-ssh-and-yubikeys/"target="_blank" rel="noopener">previous post about SSH</a>). On it I run things like Portainer, Plex, Sonarr, Bazarr, qBitTorrent and much more.</p>
<p>The way it&rsquo;s set up, I just added an entry to my desktop&rsquo;s <code>/etc/hosts</code> like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>192.168.0.100 synology.lan
192.168.0.200 plex.lan</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p><strong>From now on all my domains and IP addresses will be fake, obviously.</strong></p>
<p>Anyway, this way I can open my browser and go to <code>http://plex.lan:32400</code> to access my Plex, for example. But this has three disadvantages:</p>
<ol>
<li>I can only use these hostnames on my desktop; from my laptop I&rsquo;d need to edit the hosts file too. On top of that, I need to know the ports for each container.</li>
<li>I have no SSL for anything, so all authentication goes in plain text. It&rsquo;s not a huge problem because the traffic is only internal on my local LAN. I haven&rsquo;t been accessing it from outside the house.</li>
<li>If I ever want to access it from outside, none of this works.</li>
</ol>
<p>There are dozens of ways to solve this problem, solutions involving WireGuard, or commercial solutions like TailScale and so on. But there&rsquo;s a reasonably simple solution that solves all the problems at once: <strong>Cloudflare Tunnels</strong>.</p>
<p>The idea is to create secure tunnels via Cloudflare to access my services on my internal LAN <strong>WITHOUT</strong> having to expose ports outside my router or anything like that. My network stays isolated and unchanged.</p>
<h3>1. Registering a Domain<span class="hx:absolute hx:-mt-20" id="1-registering-a-domain"></span>
    <a href="#1-registering-a-domain" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The first recommendation is to register a real domain. Doing this lets you have real SSL certificates that Cloudflare itself manages and renews automatically.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/screenshot-2025-09-09_16-52-14.png" alt="Cloudflare Domain"  loading="lazy" /></p>
<p>I have a few domains registered and I&rsquo;m not going to reveal which one I use for my Home Server. I&rsquo;ll use this empty <strong>fabioakita.dev</strong> as an example just for this post.</p>
<p>Domains are cheap, around USD 12 per year, depending on the name you choose. Pick one that&rsquo;s short and easy to type.</p>
<h3>2. Installing Cloudflared<span class="hx:absolute hx:-mt-20" id="2-installing-cloudflared"></span>
    <a href="#2-installing-cloudflared" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>With the domain created, now we need to install the <a href="https://pkg.cloudflare.com/index.html"target="_blank" rel="noopener"><strong>cloudflared</strong></a> service on the Home Server. It&rsquo;s the one that connects to Cloudflare&rsquo;s servers and opens the tunnel into my local server. Click the link for installation instructions, but on an Ubuntu Server 22.04 it goes like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Add cloudflare gpg key</span>
</span></span><span class="line"><span class="cl">sudo mkdir -p --mode<span class="o">=</span><span class="m">0755</span> /usr/share/keyrings
</span></span><span class="line"><span class="cl">curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg <span class="p">|</span> sudo tee /usr/share/keyrings/cloudflare-main.gpg &gt;/dev/null
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Add this repo to your apt repositories</span>
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s1">&#39;deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared jammy main&#39;</span> <span class="p">|</span> sudo tee /etc/apt/sources.list.d/cloudflared.list
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># install cloudflared</span>
</span></span><span class="line"><span class="cl">sudo apt-get update <span class="o">&amp;&amp;</span> sudo apt-get install cloudflared</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Now we need to log in:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># login</span>
</span></span><span class="line"><span class="cl">sudo cloudflared tunnel login
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># add the homelab tunnel</span>
</span></span><span class="line"><span class="cl">sudo cloudflared tunnel create homelab
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># create the config directory</span>
</span></span><span class="line"><span class="cl">sudo mkdir -p /etc/cloudflared</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Use the same credentials you use to log in to the Cloudflare website. If everything goes well, at the end it will create a file at <code>/root/.cloudflared/xxxxx-yyyy-zzzz...json</code>, which holds your credentials, just like an SSH private key, so don&rsquo;t throw it on a USB stick or some cloud drive. Keep it very safe. Let&rsquo;s move it:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>sudo mv /root/.cloudflared/*.json /etc/cloudflared
sudo chmod 600 /etc/cloudflared/*.json</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Finally, we need to create a configuration file <code>/etc/cloudflared/config.yml</code> with all the hostnames we want for each of the containers, pointing to the correct ports. Here&rsquo;s a snippet of my config, modified with fake IPs and the fake domain:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="c"># /etc/cloudflared/config.yml</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">tunnel</span><span class="p">:</span><span class="w"> </span><span class="l">homelab</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">credentials-file</span><span class="p">:</span><span class="w"> </span><span class="l">/etc/cloudflared/xxxx-yyyy-....json</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">ingress</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="l">...</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">hostname</span><span class="p">:</span><span class="w"> </span><span class="l">plex.fabioakita.dev</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">service</span><span class="p">:</span><span class="w"> </span><span class="l">http://192.168.0.254:32400</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">originRequest</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">httpHostHeader</span><span class="p">:</span><span class="w"> </span><span class="l">plex.fabioakita.dev</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">hostname</span><span class="p">:</span><span class="w"> </span><span class="l">immich.fabioakita.dev</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">service</span><span class="p">:</span><span class="w"> </span><span class="l">http://192.168.0.200:2283</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">originRequest</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">httpHostHeader</span><span class="p">:</span><span class="w"> </span><span class="l">immich.fabioakita.dev</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">hostname</span><span class="p">:</span><span class="w"> </span><span class="l">overseer.fabioakita.dev</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">service</span><span class="p">:</span><span class="w"> </span><span class="l">http://192.168.0.200:5055</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">originRequest</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">httpHostHeader</span><span class="p">:</span><span class="w"> </span><span class="l">overseer.fabioakita.dev</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">hostname</span><span class="p">:</span><span class="w"> </span><span class="l">syncthing.fabioakita.dev</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">service</span><span class="p">:</span><span class="w"> </span><span class="l">http://192.168.0.200:8384</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">originRequest</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">httpHostHeader</span><span class="p">:</span><span class="w"> </span><span class="l">qbittorrent.fabioakita.dev</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">hostname</span><span class="p">:</span><span class="w"> </span><span class="l">radarr.fabioakita.dev</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">service</span><span class="p">:</span><span class="w"> </span><span class="l">http://192.168.0.200:7878</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">originRequest</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">httpHostHeader</span><span class="p">:</span><span class="w"> </span><span class="l">radarr.fabioakita.dev</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">hostname</span><span class="p">:</span><span class="w"> </span><span class="l">sonarr.fabioakita.dev</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">service</span><span class="p">:</span><span class="w"> </span><span class="l">http://192.168.0.200:8989</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">originRequest</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">httpHostHeader</span><span class="p">:</span><span class="w"> </span><span class="l">sonarr.fabioakita.dev</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">hostname</span><span class="p">:</span><span class="w"> </span><span class="l">bazarr.fabioakita.dev</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">service</span><span class="p">:</span><span class="w"> </span><span class="l">http://192.168.0.200:6767</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">originRequest</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">httpHostHeader</span><span class="p">:</span><span class="w"> </span><span class="l">bazarr.fabioakita.dev</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">hostname</span><span class="p">:</span><span class="w"> </span><span class="l">prowlarr.fabioakita.dev</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">service</span><span class="p">:</span><span class="w"> </span><span class="l">http://192.168.0.200:9696</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">originRequest</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">httpHostHeader</span><span class="p">:</span><span class="w"> </span><span class="l">prowlarr.fabioakita.dev</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">hostname</span><span class="p">:</span><span class="w"> </span><span class="l">portainer.fabioakita.dev</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">service</span><span class="p">:</span><span class="w"> </span><span class="l">http://192.168.0.200:9000</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">originRequest</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">httpHostHeader</span><span class="p">:</span><span class="w"> </span><span class="l">portainer.fabioakita.dev</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">hostname</span><span class="p">:</span><span class="w"> </span><span class="l">grafana.fabioakita.dev</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">service</span><span class="p">:</span><span class="w"> </span><span class="l">http://192.168.0.200:3001</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">originRequest</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">httpHostHeader</span><span class="p">:</span><span class="w"> </span><span class="l">grafana.fabioakita.dev</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">service</span><span class="p">:</span><span class="w"> </span><span class="l">http_status:404</span><span class="w"> </span><span class="c"># this has to be the LAST ONE</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>As you can see, it&rsquo;s an extremely simple file. I think anyone can understand what it does. Modify it for your own needs.</p>
<p>After saving the file and going back to the command line, you have to manually run this command for each hostname:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo cloudflared tunnel route dns homelab plex.fabioakita.dev</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>But that&rsquo;s a lot of work with this many hostnames, so I found it easier to create a script <code>/etc/cloudflared/add.sh</code> like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># run only once</span>
</span></span><span class="line"><span class="cl"><span class="nv">TUN</span><span class="o">=</span>homelab
</span></span><span class="line"><span class="cl"><span class="nv">CONF</span><span class="o">=</span>/etc/cloudflared/config.yml
</span></span><span class="line"><span class="cl">grep -E <span class="s1">&#39;^[[:space:]]*-[[:space:]]*hostname:&#39;</span> <span class="s2">&#34;</span><span class="nv">$CONF</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="p">|</span> awk <span class="s1">&#39;{print $3}&#39;</span> <span class="p">|</span> tr -d <span class="s1">&#39;&#34;&#39;</span> <span class="p">|</span> tr -d <span class="s2">&#34;&#39;&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="p">|</span> <span class="k">while</span> <span class="nb">read</span> -r h<span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span> <span class="s2">&#34;adding </span><span class="nv">$h</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    sudo cloudflared tunnel route dns <span class="s2">&#34;</span><span class="nv">$TUN</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="nv">$h</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="k">done</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Now just run: <code>sudo sh /etc/cloudflared/add.sh</code>, <strong>only once!</strong> So make sure your config file is correct first!</p>
<p>Finally, we can configure the service to restart by itself on every boot:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo cloudflared --config /etc/cloudflared/config.yml service install
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">sudo systemctl <span class="nb">enable</span> --now cloudflared</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h3>3. Accessing your services via the tunnel<span class="hx:absolute hx:-mt-20" id="3-accessing-your-services-via-the-tunnel"></span>
    <a href="#3-accessing-your-services-via-the-tunnel" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>If nothing went wrong, you&rsquo;re done! You can go straight to your browser and test it:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/screenshot-2025-09-09_17-06-40.png" alt="Immich"  loading="lazy" /></p>
<p>How cool is that. Notice in the URL bar I typed <code>https://immich.fabioakita.dev</code> and it just worked. By the way, if you didn&rsquo;t know it, <a href="https://immich.app/"target="_blank" rel="noopener">Immich</a> works identically to Google Photos, except entirely under your control! It has a smartphone app and syncs nicely.</p>
<p>More importantly, notice it&rsquo;s on <strong>HTTPS</strong>, which was provided by Cloudflare for my entire domain. When I log in, it will go through an <strong>encrypted tunnel</strong>. And more than that: even if I&rsquo;m on 5G, away from home, on my phone, I&rsquo;ll be able to access my photos using this address!</p>
<p>From my phone it opens a secure tunnel to Cloudflare, and that <code>cloudflared</code> service on my home server also has a secure tunnel open to Cloudflare, so Cloudflare bridges the two tunnels and done! I have remote access without exposing my server directly to the internet! The best of both worlds!</p>
<p>Don&rsquo;t forget to configure all your services with strong passwords (I generate them via Bitwarden, but any service works, 1Password or whatever):</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/screenshot-2025-09-09_17-10-15.png" alt="Radarr General Auth Settings"  loading="lazy" /></p>
<h3>4. Bridge to vlans<span class="hx:absolute hx:-mt-20" id="4-bridge-to-vlans"></span>
    <a href="#4-bridge-to-vlans" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>I don&rsquo;t remember exactly why, but my Plex container alone gave me more trouble. That&rsquo;s because it uses a different VLAN than the others. Again, I don&rsquo;t remember why I did this at the time, but it looks like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="w">  </span><span class="nt">plex</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c">#image: plexinc/pms-docker:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">linuxserver/plex:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">TZ=America/Sao_Paulo</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">PUID=1026</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">PGID=1026</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">VA_DRIVER=IHD</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">PLEX_HW_TRANS_MAX=16</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">PLEX_MEDIA_SERVER_OPTS=--network-host</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/config:/config</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/data:/data</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/transcode:/transcode</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/Videos:/media</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">devices</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/dev/dri:/dev/dri</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/dev/bus/usb:/dev/bus/usb</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">healthcheck</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">test</span><span class="p">:</span><span class="w"> </span><span class="l">curl --connect-timeout 15 --silent --show-error --fail http://localhost:32400/identity</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">interval</span><span class="p">:</span><span class="w"> </span><span class="l">1m00s</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">timeout</span><span class="p">:</span><span class="w"> </span><span class="l">15s</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">retries</span><span class="p">:</span><span class="w"> </span><span class="m">3</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">start_period</span><span class="p">:</span><span class="w"> </span><span class="l">1m00s</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">mem_limit</span><span class="p">:</span><span class="w"> </span><span class="l">4g</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">cpus</span><span class="p">:</span><span class="w"> </span><span class="m">5.0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">networks</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">plex_macvlan</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">ipv4_address</span><span class="p">:</span><span class="w"> </span><span class="m">192.168.0.254</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">mac_address</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;xx:xx:xx:xx:xx:xx&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">networks</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">plex_macvlan</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">external</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">plex_macvlan</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Because of this, <code>cloudflared</code> can&rsquo;t route to that container. The way to fix it is by creating a bridge from that vlan to my ethernet.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo ip link add macvlan0 link enp3s0 <span class="nb">type</span> macvlan mode bridge
</span></span><span class="line"><span class="cl">sudo ip addr add 192.168.0.200/24 dev macvlan0   <span class="c1"># free IP in your LAN</span>
</span></span><span class="line"><span class="cl">sudo ip link <span class="nb">set</span> macvlan0 up</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Obviously, replace <code>enp3s0</code> with the correct name of your ethernet interface. Check with <code>ip addr</code> or <code>ip link</code>, for example. Just by running the commands above, if I go to the browser and type <code>https://plex.fabioakita.dev</code> it should already work. To make this permanent, we need systemd:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="cl"><span class="c"># /etc/systemd/network/80-macvlan0.netdev</span>
</span></span><span class="line"><span class="cl"><span class="p">[</span><span class="nx">NetDev</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="nx">Name</span><span class="p">=</span><span class="nx">macvlan0</span>
</span></span><span class="line"><span class="cl"><span class="nx">Kind</span><span class="p">=</span><span class="nx">macvlan</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">[</span><span class="nx">MACVLAN</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="nx">Mode</span><span class="p">=</span><span class="nx">bridge</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="cl"><span class="c"># /etc/systemd/network/80-macvlan0.network</span>
</span></span><span class="line"><span class="cl"><span class="p">[</span><span class="nx">Match</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="nx">Name</span><span class="p">=</span><span class="nx">macvlan0</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">[</span><span class="nx">Network</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="nx">Address</span><span class="p">=</span><span class="mf">192.168</span><span class="p">.</span><span class="mf">0.200</span><span class="err">/</span><span class="mi">24</span>
</span></span><span class="line"><span class="cl"><span class="nx">Gateway</span><span class="p">=</span><span class="mf">192.168</span><span class="p">.</span><span class="mf">0.1</span>
</span></span><span class="line"><span class="cl"><span class="nx">DNS</span><span class="p">=</span><span class="mf">192.168</span><span class="p">.</span><span class="mf">0.1</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>And now, just restart the services:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo systemctl restart systemd-networkd
</span></span><span class="line"><span class="cl">ip addr show macvlan0</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>The &ldquo;correct&rdquo; thing would be to have my Plex configured with <code>network_mode: host</code>, but, again, I don&rsquo;t remember why it wasn&rsquo;t, and I wasn&rsquo;t in the mood to unconfigure everything just to find out. In case anyone has similar setups, this is how you bypass it.</p>
<p>Speaking of Plex, I had heard that you have to be careful with streaming via cloudflared, I don&rsquo;t know if there are limits or extra costs. I found <a href="https://mythofechelon.co.uk/blog/2024/1/7/how-to-set-up-free-secure-high-quality-remote-access-for-plex"target="_blank" rel="noopener">this article</a> that talks more about it but I didn&rsquo;t stop to look into it deeply. The only extra thing I did, on the Cloudflare dashboard, was add a rule to not do <strong>CACHING</strong> on the <code>plex</code> subdomain, to avoid additional costs.</p>
<h3>Conclusion<span class="hx:absolute hx:-mt-20" id="conclusion"></span>
    <a href="#conclusion" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>As you can see, it&rsquo;s extremely easy to have a home server, isolate everything with Docker, and then expose your containers via a secure tunnel for remote access. None of this means instant security. Make sure your containers are always up to date using services like <a href="https://github.com/containrrr/watchtower"target="_blank" rel="noopener">Watchtower</a>, make sure each service has strong, random, unique passwords - don&rsquo;t use the same password for everything. Make sure your firewall is well configured and active, even on a LAN.</p>
<p>Many modern Wi-Fi routers let you create a separate VLAN for guest Wi-Fi. That way guests can use your Wi-Fi but can&rsquo;t see your computers, which will be logically isolated on another VLAN. Check your router&rsquo;s documentation. I use a <strong>TP-Link BE1900 Tri-Band Wi-Fi 7</strong>, but for most people it&rsquo;s too expensive and overkill. Do your research. And don&rsquo;t bother asking me about other models: I haven&rsquo;t tested all of them to know the differences.</p>
<p>If you saw names like Radarr, Sonarr and don&rsquo;t know what they mean, read the <a href="https://wiki.servarr.com/"target="_blank" rel="noopener">Servarr Wiki</a>, which consolidates all the documentation for these services in one place. It&rsquo;s the infrastructure that lets you have your own &ldquo;Personal Netflix&rdquo;.</p>
<p>Also, don&rsquo;t ask me why I use Plex and not Jellyfin. It&rsquo;s a personal choice, pick what&rsquo;s best for you. I like Plex, I&rsquo;ve used Jellyfin, and I don&rsquo;t plan to switch.</p>
<p>I hate the idea of paying subscriptions for everything and owning nothing. I think it&rsquo;s money thrown away. I prefer to <strong>OWN</strong> my own things under <strong>MY CONTROL</strong>. It&rsquo;s more expensive, it&rsquo;s more work. Too much convenience means you have control over nothing and always depend on others.</p>
<p>I hate depending on others.</p>
]]></content:encoded><category>homelab</category><category>networking</category><category>containers</category></item><item><title>Omarchy 2.0 - Mise for Organizing Development Environments</title><link>https://akitaonrails.github.io/en/2025/09/07/omarchy-2-0-mise-for-organizing-dev-environments/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2025/09/07/omarchy-2-0-mise-for-organizing-dev-environments/</guid><pubDate>Sun, 07 Sep 2025 16:40:00 -0300</pubDate><description>&lt;p&gt;Continuing my posts about &lt;a href="https://www.akitaonrails.com/en/tags/omarchy/"target="_blank" rel="noopener"&gt;Omarchy&lt;/a&gt;, here&amp;rsquo;s another introductory post, this time about &lt;a href="https://mise.jdx.dev/getting-started.html"target="_blank" rel="noopener"&gt;Mise-en-place&lt;/a&gt;, which already comes pre-installed. On Omarchy, if you press &amp;ldquo;Super+Alt+Space&amp;rdquo; the main menu will open. Pick &amp;ldquo;Install&amp;rdquo; and then &amp;ldquo;Development&amp;rdquo; and you&amp;rsquo;ll see several languages/frameworks you can install, like Ruby on Rails or Go.&lt;/p&gt;
&lt;p&gt;But you don&amp;rsquo;t need to use these menus. Let me explain.&lt;/p&gt;
&lt;p&gt;On a regular Arch Linux or Ubuntu, an amateur would install ruby or python doing something like this:&lt;/p&gt;</description><content:encoded><![CDATA[<p>Continuing my posts about <a href="https://www.akitaonrails.com/en/tags/omarchy/"target="_blank" rel="noopener">Omarchy</a>, here&rsquo;s another introductory post, this time about <a href="https://mise.jdx.dev/getting-started.html"target="_blank" rel="noopener">Mise-en-place</a>, which already comes pre-installed. On Omarchy, if you press &ldquo;Super+Alt+Space&rdquo; the main menu will open. Pick &ldquo;Install&rdquo; and then &ldquo;Development&rdquo; and you&rsquo;ll see several languages/frameworks you can install, like Ruby on Rails or Go.</p>
<p>But you don&rsquo;t need to use these menus. Let me explain.</p>
<p>On a regular Arch Linux or Ubuntu, an amateur would install ruby or python doing something like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code># arch
yay -S ruby python3
# ubuntu
apt install ruby python3</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p><strong>This is the WRONG way.</strong></p>
<p>These commands will install the latest version of each language, and every time you update the system with <code>yay -Syu</code> or <code>apt upgrade</code>, they&rsquo;ll pull in even newer versions.</p>
<p>The thing is, if you develop real projects, <strong>you don&rsquo;t want the versions to change</strong>.</p>
<p>The version deployed on the cloud server and the version on your local machine <strong>MUST BE THE SAME</strong>.</p>
<p>If you&rsquo;re using Ruby v3.0.7 and Node.js 22.19.0, you can&rsquo;t be held hostage to the fact that when you update your system, out of nowhere Ruby 3.4.5 and Node 24.7.0 show up. Everything will start breaking and you&rsquo;ll be super confused without knowing why. This is the most basic mistake a junior developer makes.</p>
<p>To avoid this, every project you&rsquo;re working on MUST BE LOCKED TO FIXED VERSIONS.</p>
<p>The easiest way to do this in 2025 is using <strong>MISE</strong>. As I said, it already comes installed and activated on Omarchy, but if you&rsquo;re using another distro just install Mise manually:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>yay -S mise

# .bashrc
eval $(mise activate bash)</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Adapt this for ZSH, Fish or whatever shell you&rsquo;re using, read the <a href="https://mise.jdx.dev/installing-mise.html"target="_blank" rel="noopener">documentation</a></p>
<p>Now navigate to your project directory, for example:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>cd ~/Projects/AkitaOnRails.com</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>And lock the versions:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>mise use ruby@3.2.3
mise use node@14.21.3</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>This will create a <code>.tool-versions</code> file that you should add to your repository&rsquo;s git:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>❯ cat .tool-versions
ruby 3.2.3
nodejs 14.21.3

❯ git add .tool-versions</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>And done, every time you <code>cd</code> into the directory, Mise will activate exactly the right version for that project. Look:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>AkitaOnRails.com [ master][$!?⇡][ v24.7.0][💎 v3.2.3]
❯ ruby -v
ruby 3.2.3 (2024-01-18 revision 52bb2ac0a6) [x86_64-linux]

AkitaOnRails.com [ master][$!?⇡][ v24.7.0][💎 v3.2.3]
❯ which ruby
/home/akitaonrails/.local/share/mise/installs/ruby/3.2.3/bin/ruby</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Notice that, if you&rsquo;re using the <a href="https://akitaonrails.com/en/2025/09/07/omarchy-2-0-zsh-configs/"target="_blank" rel="noopener">Starship</a> prompt I explained in the previous post, with a preset like Pure Prompt, it shows the version of each language in the prompt so it&rsquo;s easy to see if you&rsquo;re on the wrong version.</p>
<p>In this case, if you read carefully, you&rsquo;ve probably already noticed that <strong>YES, I&rsquo;M ON THE WRONG VERSION</strong> of Node.js. Look how the prompt says I&rsquo;m using Node 24.7.0 but the <code>.tool-versions</code> file asks for 14.21.3.</p>
<p>The reason it&rsquo;s &ldquo;missing&rdquo; is because this machine was recently installed, but the project is old.</p>
<p>So let&rsquo;s check:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>AkitaOnRails.com [ master][$!?⇡][ v24.7.0][💎 v3.2.3]
❯ mise list
Tool    Version            Source                                              Requested
go      1.25.0             ~/.config/mise/config.toml                          latest
node    14.21.3 (missing)  /mnt/data/Projects/AkitaOnRails.com/.tool-versions  14.21.3
node    22.18.0
python  3.13.7
ruby    3.2.3              /mnt/data/Projects/AkitaOnRails.com/.tool-versions  3.2.3
ruby    3.4.5</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>See how in the <code>mise list</code> output it says 14.21.3 is &ldquo;missing&rdquo;. So let&rsquo;s install it:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>AkitaOnRails.com [ master][$!?⇡][ v24.7.0][💎 v3.2.3]
❯ mise use node@14.21.3
mise /mnt/data/Projects/AkitaOnRails.com/.tool-versions tools: node@14.21.3

AkitaOnRails.com [ master][$!?⇡][ v14.21.3][💎 v3.2.3]
❯</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>With the <code>mise use</code> command we can ask it to install and use a particular version. If it&rsquo;s not already installed, it will install it. Notice how after that the Starship prompt changed to reflect that we&rsquo;re now using 14.21.3. That&rsquo;s the right way.</p>
<p>If you&rsquo;re not necessarily in a project directory with locked versions, you can still run commands using a specific version, like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>mise exec ruby@3.2.1 -- ruby ...</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Say you want to generate a Rails project on a specific Ruby version:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>mise exec ruby@3.2.3 -- rails new todo-exercise</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>The same goes for generators in Node, like Next.js or any other framework.</p>
<p>I haven&rsquo;t checked whether LazyVim on Omarchy comes configured this way, but in case it doesn&rsquo;t, for NeoVim to support Mise you need to add this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-lua" data-lang="lua"><span class="line"><span class="cl"><span class="c1">-- Prepend mise shims to PATH</span>
</span></span><span class="line"><span class="cl"><span class="n">vim.env</span><span class="p">.</span><span class="n">PATH</span> <span class="o">=</span> <span class="n">vim.env</span><span class="p">.</span><span class="n">HOME</span> <span class="o">..</span> <span class="s2">&#34;/.local/share/mise/shims:&#34;</span> <span class="o">..</span> <span class="n">vim.env</span><span class="p">.</span><span class="n">PATH</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Just put this in <code>~/.config/nvim/lua/config/mise.lua</code> and it should load the correct path. To integrate with other editors, check <a href="https://mise.jdx.dev/ide-integration.html"target="_blank" rel="noopener">this documentation</a>. There are TONS of advanced options and it&rsquo;s worth reading about them in the <a href="https://mise.jdx.dev/dev-tools/"target="_blank" rel="noopener">Dev-Tools section</a>, but the basics are what I listed here.</p>
<h2>And databases??<span class="hx:absolute hx:-mt-20" id="and-databases"></span>
    <a href="#and-databases" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The same goes for databases. Every project should be locked to a specific version identical to what&rsquo;s running on the production server and should never depend on operating system packages. You can use mise for this but the most recommended option is to use Docker. Every project <strong>MUST</strong> have a <code>docker-compose.yml</code> to spin up only the databases. You don&rsquo;t need to spin up containers for languages, for that it&rsquo;s better to use Mise.</p>
<p>If you don&rsquo;t understand Docker, I made videos teaching it:</p>


<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/85k8se4Zo70"
    title="YouTube video player"
    frameborder="0"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
    referrerpolicy="strict-origin-when-cross-origin"
    allowfullscreen>
  </iframe>
</div>

</br>


<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/-yGHG3pnHLg"
    title="YouTube video player"
    frameborder="0"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
    referrerpolicy="strict-origin-when-cross-origin"
    allowfullscreen>
  </iframe>
</div>

<p>Adding up Mise + Docker Compose + LazyVim, that&rsquo;s everything you need to be a productive web developer, while also using your machine&rsquo;s resources efficiently. Every developer NEEDS to learn this combo of options. Omarchy already brings it all pre-installed, which is why it&rsquo;s been my recommendation for every beginner.</p>
<p>Don&rsquo;t worry: Arch Linux (with archinstall) is as easy to install as an Ubuntu or Linux Mint, and Omarchy&rsquo;s Hyprland is much prettier and smoother than MacOS. The best of both worlds. Learn it today!</p>
]]></content:encoded><category>omarchy</category><category>developer-tools</category><category>containers</category></item><item><title>How to Contribute to the AkitaOnRails Blog Using Docker</title><link>https://akitaonrails.github.io/en/2025/08/29/how-to-contribute-to-the-akitaonrails-blog-using-docker/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2025/08/29/how-to-contribute-to-the-akitaonrails-blog-using-docker/</guid><pubDate>Fri, 29 Aug 2025 22:37:16 -0300</pubDate><description>&lt;p&gt;Contributing to open source projects can be a pain when you have to set up a complex development environment. On the AkitaOnRails blog, that used to mean installing Hugo Extended, Go, Ruby, and all their dependencies. Now we have a much simpler solution: &lt;strong&gt;Docker&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;The Problem&lt;span class="hx:absolute hx:-mt-20" id="the-problem"&gt;&lt;/span&gt;
&lt;a href="#the-problem" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Before, to contribute to the blog you had to:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Install Hugo Extended&lt;/li&gt;
&lt;li&gt;Install Go&lt;/li&gt;
&lt;li&gt;Install Ruby&lt;/li&gt;
&lt;li&gt;Set up all the dependencies&lt;/li&gt;
&lt;li&gt;Generate the post index manually&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This created barriers for new contributors and could cause conflicts with other installations on the system. A nightmare.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Contributing to open source projects can be a pain when you have to set up a complex development environment. On the AkitaOnRails blog, that used to mean installing Hugo Extended, Go, Ruby, and all their dependencies. Now we have a much simpler solution: <strong>Docker</strong>.</p>
<h2>The Problem<span class="hx:absolute hx:-mt-20" id="the-problem"></span>
    <a href="#the-problem" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Before, to contribute to the blog you had to:</p>
<ol>
<li>Install Hugo Extended</li>
<li>Install Go</li>
<li>Install Ruby</li>
<li>Set up all the dependencies</li>
<li>Generate the post index manually</li>
</ol>
<p>This created barriers for new contributors and could cause conflicts with other installations on the system. A nightmare.</p>
<h2>The Solution: Docker<span class="hx:absolute hx:-mt-20" id="the-solution-docker"></span>
    <a href="#the-solution-docker" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Now all you need is Docker and Docker Compose. Everything else runs inside isolated containers.</p>
<h3>Quick Setup<span class="hx:absolute hx:-mt-20" id="quick-setup"></span>
    <a href="#quick-setup" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Clone the repository</span>
</span></span><span class="line"><span class="cl">git clone https://github.com/akitaonrails/akitaonrails.github.io.git
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> akitaonrails.github.io
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Start the environment</span>
</span></span><span class="line"><span class="cl">./scripts/dev.sh start
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Open the blog</span>
</span></span><span class="line"><span class="cl">open http://localhost:1313</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Done! The blog is running and you can start contributing.</p>
<h2>Useful Commands<span class="hx:absolute hx:-mt-20" id="useful-commands"></span>
    <a href="#useful-commands" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The project includes scripts that make development easier:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># See all available commands</span>
</span></span><span class="line"><span class="cl">./scripts/dev.sh <span class="nb">help</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Create a new post</span>
</span></span><span class="line"><span class="cl">./scripts/dev.sh new-post <span class="s2">&#34;Título do Meu Post&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># View server logs</span>
</span></span><span class="line"><span class="cl">./scripts/dev.sh logs
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Generate the post index</span>
</span></span><span class="line"><span class="cl">./scripts/dev.sh generate-index
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Stop the environment</span>
</span></span><span class="line"><span class="cl">./scripts/dev.sh stop</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h2>Creating a Post<span class="hx:absolute hx:-mt-20" id="creating-a-post"></span>
    <a href="#creating-a-post" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Creating a new post is dead simple:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">./scripts/dev.sh new-post <span class="s2">&#34;Como Contribuir no Blog do AkitaOnRails&#34;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>The script automatically:</p>
<ul>
<li>Creates the directory structure with the current date</li>
<li>Generates an <code>index.md</code> file with basic front matter</li>
<li>The Hugo server reloads automatically</li>
</ul>
<h2>Structure of a Post<span class="hx:absolute hx:-mt-20" id="structure-of-a-post"></span>
    <a href="#structure-of-a-post" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The created file will have this structure:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="cl">---
</span></span><span class="line"><span class="cl">title: &#34;Título do Post&#34;
</span></span><span class="line"><span class="cl">date: &#34;2025-08-29&#34;
</span></span><span class="line"><span class="cl">draft: false
</span></span><span class="line"><span class="cl">description: &#34;Descrição do post&#34;
</span></span><span class="line"><span class="cl">tags: [tag1, tag2]
</span></span><span class="line"><span class="cl">categories: [categoria]
</span></span><span class="line"><span class="cl">---
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Conteúdo do post aqui...</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h2>Advantages of Docker<span class="hx:absolute hx:-mt-20" id="advantages-of-docker"></span>
    <a href="#advantages-of-docker" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><h3>For Contributors<span class="hx:absolute hx:-mt-20" id="for-contributors"></span>
    <a href="#for-contributors" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><ul>
<li><strong>Fast setup</strong>: Docker is the only requirement</li>
<li><strong>Isolated environment</strong>: Stays out of the way of your local system</li>
<li><strong>Hot reload</strong>: Changes show up automatically</li>
<li><strong>Reproducible</strong>: Works the same way for everyone</li>
</ul>
<h3>For the Project<span class="hx:absolute hx:-mt-20" id="for-the-project"></span>
    <a href="#for-the-project" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><ul>
<li><strong>Fewer issues</strong>: Consistent environment</li>
<li><strong>More contributors</strong>: Lower barrier to entry</li>
<li><strong>Maintenance</strong>: Easy to update dependencies</li>
</ul>
<h2>How It Works<span class="hx:absolute hx:-mt-20" id="how-it-works"></span>
    <a href="#how-it-works" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The Docker environment includes:</p>
<ul>
<li><strong>Hugo Extended</strong>: Static site generator</li>
<li><strong>Go</strong>: For Hugo modules</li>
<li><strong>Ruby</strong>: For the index generation script</li>
<li><strong>Volumes</strong>: For hot reload of files</li>
</ul>
<p>The <code>docker-compose.yml</code> maps the local directories into the container, so when you edit a file, Hugo detects the change and reloads automatically.</p>
<h2>Local Development (Alternative)<span class="hx:absolute hx:-mt-20" id="local-development-alternative"></span>
    <a href="#local-development-alternative" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>If you prefer to skip Docker, you can still install everything locally:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Generate the index</span>
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> content <span class="o">&amp;&amp;</span> ruby generate_index.rb
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Run the server</span>
</span></span><span class="line"><span class="cl">hugo server --logLevel debug --disableFastRender -p <span class="m">1313</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>But Docker is way more practical!</p>
<h2>Next Steps<span class="hx:absolute hx:-mt-20" id="next-steps"></span>
    <a href="#next-steps" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><ol>
<li><strong>Test the environment</strong>: Run <code>./scripts/dev.sh start</code></li>
<li><strong>Create a post</strong>: Use <code>./scripts/dev.sh new-post</code></li>
<li><strong>Explore the code</strong>: See how to customize layouts</li>
<li><strong>Contribute</strong>: Open a Pull Request</li>
</ol>
<hr>
<p><strong>Tip</strong>: The Docker environment automatically saves your changes and reloads the server. Just edit the files and watch the changes in real time! 🚀</p>
]]></content:encoded><category>open-source</category><category>containers</category><category>tutorials</category></item><item><title>Generating AI Images - even Ghibli style 😂 - with Docker and CUDA</title><link>https://akitaonrails.github.io/en/2025/04/20/generating-ai-images-even-ghibli-style-with-docker-and-cuda/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2025/04/20/generating-ai-images-even-ghibli-style-with-docker-and-cuda/</guid><pubDate>Sun, 20 Apr 2025 17:30:00 -0300</pubDate><description>&lt;p&gt;This topic will be split into two posts. This one is purely technical, about how to get it running. In the other one I&amp;rsquo;ll explain what the heck ComfyUI is and roughly how to use it.&lt;/p&gt;
&lt;p&gt;More than a year ago, when I made my last videos about A.I. and on the podcasts, I always said I ran everything on my own machine. But I never detailed how. So today I fixed that with several blog posts and little projects on GitHub with Dockerfiles so you can run it on your own machine, without configuring anything difficult.&lt;/p&gt;</description><content:encoded><![CDATA[<p>This topic will be split into two posts. This one is purely technical, about how to get it running. In the other one I&rsquo;ll explain what the heck ComfyUI is and roughly how to use it.</p>
<p>More than a year ago, when I made my last videos about A.I. and on the podcasts, I always said I ran everything on my own machine. But I never detailed how. So today I fixed that with several blog posts and little projects on GitHub with Dockerfiles so you can run it on your own machine, without configuring anything difficult.</p>
<p>I finally made a <a href="https://github.com/akitaonrails/ComfyUI-Docker-CUDA-preloaded"target="_blank" rel="noopener">Dockerfile</a> for ComfyUI, which is the best graphical interface for editing image generation workflows. If you&rsquo;ve ever worked with Davinci Resolve Fusion or Blender Geometry Nodes and the like, workflows are basically node configurations to process your images, audio, video and pipe them through processing steps, not just A.I. ones. It&rsquo;s like programming visually. That&rsquo;s why it&rsquo;s a much more complicated tool than the FramePack I showed in the previous post. You don&rsquo;t just click and go.</p>
<p>Recently everyone went hysterical with the ChatGPT updates and how it <em>&ldquo;OH MY GOD&rdquo;</em> can generate Ghibli anime style images, and they kept spamming the world with idiotic anime-style memes. Online hysteria is really annoying. But as I said, as impressive as it is, that&rsquo;s all it is: a meme generator. You can&rsquo;t use it professionally.</p>
<p>I&rsquo;m tired of trying to upload my own photos, or even generic photos, fictional characters, sometimes drawings of mine, and randomly ChatGPT just refuses to generate the image. Claiming privacy, security, copyright and whatever else it can invent as an excuse. You can&rsquo;t rely on this for work. Anyone who&rsquo;s a professional should have full control over the process, and that&rsquo;s what I&rsquo;m going to teach today.</p>
<p>To start, clone my <a href="https://github.com/akitaonrails/ComfyUI-Docker-CUDA-preloaded"target="_blank" rel="noopener">GitHub repository</a> and follow the README instructions or do this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>git clone https://github.com/akitaonrails/ComfyUI-Docker-CUDA-preloaded.git
cd ComfyUI-Docker-CUDA-preloaded
docker compose build</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Since it&rsquo;s more complex, this time I had to put together a Docker Compose. So to bring it up, after the build finishes, just do this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>docker compose up</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Now it&rsquo;s going to take a while, because by default it will download almost <strong>500GB</strong> of pre-trained models so it can do almost anything image-related. Yes, to play with A.I. locally you need infrastructure. Otherwise, rent a beefy machine with lots of storage on a RunPod or similar and run it remotely. You can download fewer models, I&rsquo;ll explain that next.</p>
<p>On Docker Compose. I recommend avoiding the &ldquo;-d&rdquo; option to bring it up as a daemon, because it&rsquo;s good to reserve a terminal to keep watching the outputs (you can run <code>docker compose logs --follow</code> if you want, but it&rsquo;s easier directly).</p>
<p>ComfyUI is a more complex program than average, much more so than something like FramePack. It supports installing &ldquo;extensions&rdquo; (plugins, addons, call them what you want). And honestly on its own it doesn&rsquo;t do much, you need to install several extensions for it to start being really useful. Every now and then one of these extensions doesn&rsquo;t handle errors properly and blows up in the console, something like this:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/n7w5oxun51elrsv2fra46f684xre" alt="Console Error"  loading="lazy" /></p>
<p>It&rsquo;s always useful to keep an eye on the console to find out if it tried to load a file that doesn&rsquo;t exist. Then you can grab the file name and search Google, download it and put it in the right place. Or if it&rsquo;s a really buggy extension that&rsquo;s better off removed from the &ldquo;custom_nodes&rdquo; directory, and so on. Without watching the console, it doesn&rsquo;t show up in the web interface and you&rsquo;ll be left in the dark.</p>
<p>A lot of the models and extensions I based on the <strong>Ultimate ComfyUI version 3</strong> configuration recommended by <strong>Aitrepreneur</strong>, again. Watch <a href="https://youtu.be/q5kpr84uyzc"target="_blank" rel="noopener">this video of his</a> to get all the details and everything that&rsquo;s possible to do. He has the scripts for Linux, Windows, and the workflows in his paid Patreon version and, of course, I won&rsquo;t share the workflows because they&rsquo;re not mine. In theory, my config should be able to do what he teaches.</p>
<p>Aitrepreneur&rsquo;s V3 workflow is really badass. But I can&rsquo;t share it because it&rsquo;s closed material on his Patreon, but here&rsquo;s a screenshot of my ComfyUI with all of his stuff loaded:</p>
<p><a href="https://www.patreon.com/posts/ultimate-all-in-121355089"target="_blank" rel="noopener"><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/1p3xfs0ecfimbw4yg55qz17buti6" alt="Ultimate V3"  loading="lazy" /></a></p>
<p>But the config itself, I redid it entirely to work in this Docker. You can load any other open workflow you find on a Reddit or wherever.</p>
<p>Here&rsquo;s what I did:</p>
<h2>Models<span class="hx:absolute hx:-mt-20" id="models"></span>
    <a href="#models" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>In the project there has to be a &ldquo;models&rdquo; directory, if there isn&rsquo;t one, create it, because docker compose will map it inside the container. When it starts, it&rsquo;ll run the file <a href="https://github.com/akitaonrails/ComfyUI-Docker-CUDA-preloaded/blob/master/init_models.sh"target="_blank" rel="noopener">init_models.sh</a>. This script will load a configuration file you can edit later: <a href="https://github.com/akitaonrails/ComfyUI-Docker-CUDA-preloaded/blob/master/models.conf"target="_blank" rel="noopener">models.conf</a>. It&rsquo;s huge and a piece of it looks like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>...
[CONTROLNET]
https://huggingface.co/XLabs-AI/flux-controlnet-collections/resolve/main/flux-canny-controlnet-v3.safetensors
https://huggingface.co/XLabs-AI/flux-controlnet-collections/resolve/main/flux-depth-controlnet-v3.safetensors
https://huggingface.co/brad-twinkl/controlnet-union-sdxl-1.0-promax/resolve/main/diffusion_pytorch_model.safetensors|controlnet-union-sdxl-1.0-promax.safetensors
https://huggingface.co/lllyasviel/ControlNet-v1-1/resolve/main/control_v11p_sd15_canny.pth
https://huggingface.co/lllyasviel/ControlNet-v1-1/resolve/main/control_v11f1p_sd15_depth.pth
https://huggingface.co/lllyasviel/ControlNet-v1-1/resolve/main/control_v11p_sd15_openpose.pth
https://huggingface.co/Aitrepreneur/FLX/resolve/main/Shakker-LabsFLUX1-dev-ControlNet-Union-Pro.safetensors

[STYLE_MODELS]
https://huggingface.co/Aitrepreneur/FLX/resolve/main/flux1-redux-dev.safetensors

[SAMS]
https://huggingface.co/Aitrepreneur/FLX/resolve/main/sam_vit_b_01ec64.pth</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Each section maps to a sub-directory, so &ldquo;[SAMS]&rdquo; will map to &ldquo;models/sams&rdquo; and download the binaries to the right places. There are some special ones in the &ldquo;[GIT_REPOS]&rdquo; section, for those that can be cloned via git, or &ldquo;[CUSTOM]&rdquo; for files you need to download later inside those cloned projects, for example. I don&rsquo;t yet know if it can be simplified, but I picked this up from Aitrepreneur&rsquo;s config.</p>
<p>The ideal is to always keep this list organized with models you find later. If you want to contribute, that would also be cool. The reason is that everything is scattered across all kinds of sites out there, you download manually, then move to the directory inside &ldquo;models&rdquo;. Tomorrow, if you reinstall your machine, how are you supposed to remember all the sites? There are dozens of available models.</p>
<p>In the next post I&rsquo;ll try to summarize what the hell &ldquo;checkpoints&rdquo;, &ldquo;diffusion models&rdquo;, &ldquo;loras&rdquo;, &ldquo;controlnet&rdquo; and all the rest are. But for now, the file extension doesn&rsquo;t say much. &ldquo;.pt&rdquo; for example only means it&rsquo;s a file PyTorch can open, but it doesn&rsquo;t tell you whether it&rsquo;s a clip or a text encoder, for example. Same thing with &ldquo;.safetensors&rdquo;, it can be anything, and ComfyUI needs each file to be in the correct sub-directory in order to use it, you can&rsquo;t just go around moving files randomly.</p>
<p>The &ldquo;init_models.sh&rdquo; script runs every time Docker is restarted and is smart enough to only download URLs that aren&rsquo;t already downloaded in the &ldquo;models&rdquo; directory. That&rsquo;s why it always runs when the container comes up, in the entrypoint. That&rsquo;s one of the advantages of my config: it has automatic maintenance.</p>
<h3>Extensions and Dependencies<span class="hx:absolute hx:-mt-20" id="extensions-and-dependencies"></span>
    <a href="#extensions-and-dependencies" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>I made a second script called <a href="https://github.com/akitaonrails/ComfyUI-Docker-CUDA-preloaded/blob/master/init_extensions.sh"target="_blank" rel="noopener">init_extensions.sh</a>, which also runs at the container&rsquo;s entrypoint and which you shouldn&rsquo;t touch, but it loads another configuration file called <a href="https://github.com/akitaonrails/ComfyUI-Docker-CUDA-preloaded/blob/master/extensions.conf"target="_blank" rel="noopener">extensions.conf</a> and that one, yes, you can mess with and add new extensions later. A piece of it looks like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>[EXTENSIONS]
https://github.com/ltdrdata/ComfyUI-Manager.git
https://codeberg.org/Gourieff/comfyui-reactor-node.git
https://github.com/ssitu/ComfyUI_UltimateSDUpscale.git
https://github.com/kijai/ComfyUI-LivePortraitKJ.git
https://github.com/PowerHouseMan/ComfyUI-AdvancedLivePortrait.git
...</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Here I declare all the extensions that add new functionality on top of ComfyUI, in particular new <strong>NODES</strong>, which is why they live in the &ldquo;custom_nodes&rdquo; directory, which is a volume external to Docker and therefore persistent, meaning every new extension survives container restarts.</p>
<p>But there&rsquo;s still a BIG catch: <strong>Python Dependencies</strong>. A nightmare.</p>
<p>The Docker image build does a bunch of <code>pip install</code> and since I&rsquo;m using Virtual ENV, everything gets installed in <code>/venv</code> inside the container, as it should.</p>
<p>However, the extensions that get installed outside the build - every time the container restarts - tend to be Python code and also have more dependencies. Extensions are GitHub projects and may be changing right this minute. If they change and you do a <code>git pull</code> to update, you need to run <code>pip install -r requirements.txt</code>. But if you have dozens of extensions, running <code>pip install</code> for each one on every Docker restart takes a long time to do almost nothing. As I said, a nightmare.</p>
<p>My solution - to brag a bit - fixes a good chunk of this maintenance. When the <code>init_extensions.sh</code> script runs, if an extension from the list is already installed, it&rsquo;ll skip it. Then it&rsquo;ll write to the <code>./custom_nodes/.last_commit/*.commit</code> directory the latest commit of each extension. When Docker restarts, it&rsquo;ll do a <code>git fetch</code> and check whether the latest commit hash has changed.</p>
<p>Only if it has changed will it run <code>pip install</code> for that extension that got an update, and skip all the others that haven&rsquo;t changed since the last restart, saving A LOT of time and avoiding having to manually update things. Since I&rsquo;m using VENV, everything will stay persisted in the &ldquo;/venv&rdquo; volume. Yes, it&rsquo;s a VOLUME, because it changes after the build with each extension update.</p>
<p>This way, everything in the image gets copied into that volume, and everything the extensions install on the side also goes into that volume. Every time you restart, it all comes back the way it was. That was the solution I found and so far it seems to work just fine.</p>
<p>But it&rsquo;s only a matter of time until some poorly made extension installs dependencies that break other dependencies, or an actual case I went through: there was an extension that crashes with no error message if you&rsquo;re using Python 3.11, the Zoe Depth Map from the <code>comfyui_controlnet_aux</code> extension, one of the packages that seems to be popular.</p>
<p>I hadn&rsquo;t realized I&rsquo;d started using a Docker base image of ubuntu22 instead of ubuntu24, I was afraid of using something too new and breaking things, but I ended up using something too old and it broke just the same. Managing versioning is a pain. Anyway, I changed the Dockerfile to ubuntu24.</p>
<p>But in the external VENV volume now there were a bunch of dependencies pinned to Python 3.11. No problem, just delete the volume:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>docker compose down # make sure it&#39;s stopped
docker volume rm comfyui-venv
rm ./custom_nodes/.last_commits/*.commit</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>And done, the VENV will start over empty, fill up with the correct Python 3.12 dependencies from the image, and when the container starts my <code>init_extensions.sh</code> script won&rsquo;t find &ldquo;.last_commits&rdquo; and will force a <code>pip install -r requirements.txt</code> of all the extensions, now with Python 3.12. Super clean and reliable.</p>
<p>If even so you get an error, then it might be a bug in a specific extension, just go to its directory in &ldquo;./custom_nodes&rdquo; and delete it manually before restarting the container.</p>
<p>I think that&rsquo;s the least complicated thing you can do in this case. This docker wasn&rsquo;t built to run on its own on a server, it&rsquo;s meant to run on your own local machine, where you&rsquo;re monitoring the terminal output anyway.</p>
<p>Oh yes, if a new version of ComfyUI comes out then you have to rebuild the image and delete the VENV volume. The image is static, don&rsquo;t forget. Inside, the Dockerfile does a new git clone and grabs the newest commit. Then it&rsquo;ll install newer python packages, so you need to wipe the VENV volume and have the extensions reinstall their dependencies afterwards too. It&rsquo;s a huge pain, but that&rsquo;s just how it works.</p>
<h3>Conclusion<span class="hx:absolute hx:-mt-20" id="conclusion"></span>
    <a href="#conclusion" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>With all that done, with a simple <code>docker compose up</code> I can have a stable ComfyUI environment isolated from the rest of my system, without dirtying anything with thousands of dubious dependencies. When I&rsquo;m done I just do <code>docker compose down</code> or &ldquo;ctrl-c&rdquo; in its console and that&rsquo;s it, my system stays clean.</p>
<p>On top of that, directories like &ldquo;models&rdquo; and their 500GB of models stay separate, you can back them up elsewhere if you need to. And if you want to bring it up on another machine, just take only those directories and bring up Docker Compose there and it has to come up exactly the same way, zero-setup. That was my goal in Dockerizing ComfyUI, FramePack, Video2K: to have everything portable, isolated and clean to bring up on any machine later without having to run sketchy installation scripts directly on my machine.</p>
<p><a href="https://x.com/AkitaOnRails/status/1914030815962411367"target="_blank" rel="noopener">On my X</a> I already showed my ComfyUI working. I loaded some random workflow I found on the Web and look at this:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/jhx6v5va7qac4vocp17ku5o6ha51" alt="ComfyUI"  loading="lazy" /></p>
<p>In this workflow I upload a reference image and I can write a prompt for how I want to transform it:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20xq72nojtvb887mebeksr6q32cw" alt="Prompt"  loading="lazy" /></p>
<p>With ComfyUI I have control over every step of the process, for example, this workflow can separate things like Depth Map and Edge Contour from the original image, to use as context before making the final image, ensuring a much more precise result:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/4r1en3a557znzmy0zojk8ei9tflu" alt="Depth Map"  loading="lazy" /></p>
<p>And here&rsquo;s a result I already got using the &ldquo;waiNSFIllustrious&rdquo; model that this workflow suggests, which is a model trained to generate anime-style images (there are dozens, for various different styles).</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/m7amy8gmp5y4snztatjjlwlr0wye" alt="anime"  loading="lazy" /></p>
<p>Switching the model to <code>Mistoon</code>, based on Stable Diffusion (SDXL), here&rsquo;s another result, with the same prompt, in the same workflow:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/lkj86wdlvh5fy8h78a8ml5io8t6g" alt="anime 2"  loading="lazy" /></p>
<p>And I can keep tuning dozens of parameters inside this workflow, swap vaes, text encoders, controlnets and much more until I get exactly the result I want. That&rsquo;s the power of ComfyUI and its Custom Nodes and Workflows. That&rsquo;s what I&rsquo;ll try to explain the basics of in the next post.</p>
]]></content:encoded><category>generative-media</category><category>containers</category><category>tutorials</category></item><item><title>Generating Up to 2-Minute Videos from a Photo with A.I.</title><link>https://akitaonrails.github.io/en/2025/04/19/generating-up-to-2-minute-videos-from-a-photo-with-ai/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2025/04/19/generating-up-to-2-minute-videos-from-a-photo-with-ai/</guid><pubDate>Sat, 19 Apr 2025 20:00:00 -0300</pubDate><description>&lt;p&gt;By far the best channel for tutorials on open source A.I. tools is &lt;a href="https://www.youtube.com/watch?v=GywyMij88rY"target="_blank" rel="noopener"&gt;Aitrepreneur&lt;/a&gt;. And in this link he introduces a new tool called &lt;a href="https://github.com/lllyasviel/FramePack"target="_blank" rel="noopener"&gt;FramePack&lt;/a&gt;, which uses Tencent&amp;rsquo;s famous &lt;a href="https://github.com/Tencent/HunyuanVideo"target="_blank" rel="noopener"&gt;HunyuanVideo&lt;/a&gt; model to take a single image, a photo, and generate excellent quality videos up to 2 minutes long. It&amp;rsquo;s truly impressive! Watch the video, but right below is a 10-second clip generated from a photo I took of one of the action figures in my collection.&lt;/p&gt;</description><content:encoded><![CDATA[<p>By far the best channel for tutorials on open source A.I. tools is <a href="https://www.youtube.com/watch?v=GywyMij88rY"target="_blank" rel="noopener">Aitrepreneur</a>. And in this link he introduces a new tool called <a href="https://github.com/lllyasviel/FramePack"target="_blank" rel="noopener">FramePack</a>, which uses Tencent&rsquo;s famous <a href="https://github.com/Tencent/HunyuanVideo"target="_blank" rel="noopener">HunyuanVideo</a> model to take a single image, a photo, and generate excellent quality videos up to 2 minutes long. It&rsquo;s truly impressive! Watch the video, but right below is a 10-second clip generated from a photo I took of one of the action figures in my collection.</p>
<div class="video-container">
    <video controls>
        <source src="https://akitaonrails-videos.s3.us-east-2.amazonaws.com/link-animado.mp4">
        Your browser does not support the video tag. [Direct Link](https://akitaonrails-videos.s3.us-east-2.amazonaws.com/link-animado.mp4)
    </video>
</div>
<p>Since I was already on a roll packaging this kind of tool in Docker, I decided to make another little project with a Dockerfile to run on my Manjaro Linux with an RTX 4090. The good thing about Aitrepreneur is that he has a Patreon you can subscribe to in order to contribute (it&rsquo;s cheap) and he always uploads scripts to run on <a href="https://www.runpod.io/"target="_blank" rel="noopener">RunPod</a> in case you don&rsquo;t have a beefy machine or GPU, or .BAT scripts to run on your Windows.</p>
<p>My little project is <a href="https://github.com/akitaonrails/FramePack-Docker-CUDA"target="_blank" rel="noopener">on this GitHub</a> and using it is very simple:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>
git clone https://github.com/akitaonrails/FramePack-Docker-CUDA.git
cd FramePack-Docker-CUDA
mkdir outputs
mkdir hf_download

# Build the image
docker build -t framepack-torch26-cu124:latest .

# Run mapping the directories outside:
docker run -it --rm --gpus all -p 7860:7860 \
  -v ./outputs:/app/outputs \
  -v ./hf_download:/app/hf_download \
  framepack-torch26-cu124:latest</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>This will build the Docker image and run it. And the first time you can see it will download a TON of models, HunyuanVideo, Flux and more. Get ready for over 30GB. But it&rsquo;s only the first time because I map the download directory outside, so if you restart the container it will already have everything next time.</p>
<p>When it&rsquo;s done, just go to <strong>http://localhost:7860</strong> and you&rsquo;ll see this super simple interface:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/x5nwxu25vlb9ox5kda5xiz6b4fwf" alt="FramePack Web UI"  loading="lazy" /></p>
<p>The controls are very simple:</p>
<ul>
<li>Disable the &ldquo;Use TeaCache&rdquo; option which makes it faster but then you risk getting those A.I. defects like hands with extra fingers and stuff like that.</li>
<li>Total Video length can go up to 2 minutes, but only do that if you really have patience because it takes A LOT of time! I think it&rsquo;s better to start by testing with short 5 to 10 second videos first.</li>
<li>If your GPU has little VRAM you&rsquo;ll be forced to mess with &ldquo;GPU Inference Preserved Memory (GB) (larger means slower)&rdquo;. In my case with 24GB of VRAM I left it as is, but look how it consumes resources while running:</li>
</ul>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/5purckhhytuqd7bwp2wjqhyxq66p" alt="btop"  loading="lazy" /></p>
<p>Yes! 100% of the GPU and almost 100% of the VRAM, pulling more than 350W from the wall! (screw the environment! lol) The CPU stays chill, because this process is made to really MASSACRE the GPU. If you don&rsquo;t have a good GPU, rent a machine on RunPod like I said before.</p>
<p>The 10-second video at the start of the post is a photo of one of my favorite action figures in my collection, Link from Breath of the Wild. And this is something that got me excited. This is the original photo that served as the basis for FramePack to make the video:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/g3dm9u1ij8r6gl0dk90qool0htyw" alt="Link Foto"  loading="lazy" /></p>
<p>Making videos of photos of people is dull and tedious, but now that I&rsquo;m learning to model 3D to print on my Bambulab, imagine then being able to make it animated too?</p>
<p>To animate in Blender and render also takes a lot of processing and time. But with this I can quickly prototype an animation and see how it looks, to decide if it&rsquo;s worth making a better version in Blender afterwards. It opens up many possibilities for experimentation!</p>
<p>As impressive as this seems, it doesn&rsquo;t do <strong>JUST ANYTHING</strong>, I had several experiments that went wrong and the movement it makes is quite limited, after all it only has a single photo to use as a reference and you can&rsquo;t make movements that are too abrupt, there are limits. Even so it&rsquo;s very fun to play with. The fact that it can identify more than just human beings is a big &ldquo;plus&rdquo; in my opinion, because that&rsquo;s what got me more interested.</p>
<p><strong>OH YES!</strong> Remember the <a href="https://www.akitaonrails.com/en/2025/04/19/aumentando-resolucao-de-anime-velho-pra-4k-com-i-a"target="_blank" rel="noopener">other blog post</a> I published today about doing <strong>UPSCALE TO 4K</strong>?? You can use it here too, take a video that FramePack generated that you really liked and upscale it to 4K to have even more definition! Then it&rsquo;s at quality you can even use in video editing.</p>
<p>2-minute videos must take hours to make. 30 seconds took almost 1 hour. The best thing is to stick with 5-second videos to play and experiment, just that already takes nearly half an hour. Here are some more examples taken from photos of items in my collection (the ones you used to see in the background of my videos). Have fun!</p>
<div class="video-container">
    <video controls>
        <source src="https://akitaonrails-videos.s3.us-east-2.amazonaws.com/ultraseven-animado.mp4">
        Your browser does not support the video tag. [Direct Link](https://akitaonrails-videos.s3.us-east-2.amazonaws.com/ultraseven-animado.mp4)
    </video>
</div>
<p>This is an Ultraseven over 30cm tall that I found and brought directly from a used goods shop in Tokyo last year, one of my favorites especially because I watched it a lot in the early 80s when I was a kid.</p>
<div class="video-container">
    <video controls>
        <source src="https://akitaonrails-videos.s3.us-east-2.amazonaws.com/jiraya-animado.mp4">
        Your browser does not support the video tag. [Direct Link](https://akitaonrails-videos.s3.us-east-2.amazonaws.com/jiraya-animado.mp4)
    </video>
</div>
<p>Speaking of nostalgia, this is another awesome item in my collection, a super detailed Jiraya made by Iron Studios. And animated it actually turned out pretty cool. What did you think?</p>
<p>But now my <strong>FAVORITE</strong>, those who follow me on <a href="https://www.instagram.com/p/DIAe3pNOFgffFZ5J32fWlc-8Kmy3Lhap5Vm58U0/?img_index=1"target="_blank" rel="noopener">Instagram</a> have seen that for the past few weeks I&rsquo;ve been dedicating myself to improving my 3D techniques and also drawing and I made a concept of Mandalorian + Judge Dredd:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/01qz4daelmm671m5ytgua036373h" alt="Boba Dredd"  loading="lazy" />.png?disposition=attachment&amp;locale=en)</p>
<p>Check out my other drawings on my Instagram afterwards, but anyway, I needed to know if FramePack was capable of animating drawings too and YES, IT CAN!! LOOK HOW B4D4SS!!! This one really impressed me, it gives a new dimension to my stuff!</p>
<div class="video-container">
    <video controls>
        <source src="https://akitaonrails-videos.s3.us-east-2.amazonaws.com/boba-dredd-animado_upscaled.mp4">
        Your browser does not support the video tag. [Direct Link](https://akitaonrails-videos.s3.us-east-2.amazonaws.com/boba-dredd-animado_upscaled.mp4)
    </video>
</div>
<p>Yes, all of this is quite heavy, but the advantage is that I can experiment INFINITELY, because everything runs locally. On someone&rsquo;s commercial product in the &ldquo;cloud&rdquo;, I&rsquo;d have to pay some subscription and they&rsquo;d limit me, it would be impossible to keep experimenting whenever I wanted. Now I can do everything I want, whenever I want, however I want, without anyone or anything bothering me.</p>
<p>By the way, if you can, watch that &ldquo;Boba Dredd&rdquo; clip in full screen on a big monitor. Did you notice how sharp it is? That&rsquo;s because I ran it through Video2K and upscaled to 4x the resolution that FramePack gave me. Combining these tools you can do REALLY interesting things, let me know later if you managed to use it and what cool stuff you made!</p>
]]></content:encoded><category>generative-media</category><category>containers</category></item><item><title>Upscaling Old Anime to 4K with AI</title><link>https://akitaonrails.github.io/en/2025/04/19/upscaling-old-anime-to-4k-with-ai/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2025/04/19/upscaling-old-anime-to-4k-with-ai/</guid><pubDate>Sat, 19 Apr 2025 15:30:00 -0300</pubDate><description>&lt;p&gt;More than a year ago I was playing around with ErsGAN, generative adversarial networks for the task of &amp;ldquo;upscaling&amp;rdquo; (raising resolution) old anime files I have.&lt;/p&gt;
&lt;p&gt;For collectors, the problem is that a lot of anime from the 90s and earlier never came out, and never will, on Blu-Ray (1080p) or UHD (4K). Only the most famous ones get the &amp;ldquo;remaster&amp;rdquo; treatment (taking the original master tapes and recapturing them at higher resolution). So a ton of old anime is stuck in the DVD era (480p) or VHS (480i).&lt;/p&gt;</description><content:encoded><![CDATA[<p>More than a year ago I was playing around with ErsGAN, generative adversarial networks for the task of &ldquo;upscaling&rdquo; (raising resolution) old anime files I have.</p>
<p>For collectors, the problem is that a lot of anime from the 90s and earlier never came out, and never will, on Blu-Ray (1080p) or UHD (4K). Only the most famous ones get the &ldquo;remaster&rdquo; treatment (taking the original master tapes and recapturing them at higher resolution). So a ton of old anime is stuck in the DVD era (480p) or VHS (480i).</p>
<p>Not every old anime can be upscaled, it can end up looking too &ldquo;washed out&rdquo;. Anyone who downloads torrents has probably seen badly done upscales. But I wanted to be able to test it myself, on my local machine, with my old files. Here&rsquo;s a small example clip:</p>
<div class="video-container">
    <video controls>
        <source src="https://akitaonrails-videos.s3.us-east-2.amazonaws.com/kenshin_old_clip1.mp4">
        Your browser does not support the video tag. [Direct Link](https://akitaonrails-videos.s3.us-east-2.amazonaws.com/kenshin_old_clip1.mp4)
    </video>
</div>
<p>It&rsquo;s a piece of the 1st episode of the legendary Rurouni Kenshin from 1996. Yes, today we have a remake, which they&rsquo;re redoing from scratch. But for those of us who feel nostalgic about the original, there&rsquo;s no replacement. The DVD quality isn&rsquo;t horrendous, you can still watch it just fine today as is. But I&rsquo;m curious to see how it looks in 4K.</p>
<p>I didn&rsquo;t do any long research, but a year ago I had stumbled onto <a href="https://github.com/xinntao/Real-ESRGAN"target="_blank" rel="noopener"><strong>Real ERSGAN</strong></a>. That project has been discontinued for about 4 years, so I don&rsquo;t know what the most modern option is today. But their pretrained models are still useful.</p>
<p>There are still projects that use this model, the one I know of is <a href="https://github.com/k4yt3x/video2x"target="_blank" rel="noopener"><strong>Video2K</strong></a>. On Windows it apparently even has a nice little graphical interface, but the advantage for Linux is that it runs in Docker. An example would be like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>docker run --rm --gpus all \
  -v &#34;$PWD/videos_in&#34;:/input \
  -v &#34;$PWD/videos_out&#34;:/output \
  ghcr.io/k4yt3x/video2x:6.4.0 \
  -i /input/old_anime.mp4 -o /output/old_anime_4K.mkv \
  -p realesrgan -s 4 --realesrgan-model realesr-animevideov3</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>If you have your NVIDIA set up with the Container Toolkit, CUDA and all that (ask ChatGPT), Docker can map the device straight into the container. This makes it much easier to keep my Linux from getting dirty with dependencies and loose configs scattered around. I don&rsquo;t do anything of this kind outside of containers anymore.</p>
<p>But I ran into a problem. If you&rsquo;re using an AMD GPU, just doing this should already work, because this Docker image actually uses Vulkan and not CUDA. In my case I have a primary integrated AMD GPU and a secondary NVIDIA RTX 4090. I tried passing it directly:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>❯ docker run --gpus all --privileged \
  --device=/dev/nvidia0 \
  --device=/dev/nvidiactl \
  --device=/dev/nvidia-uvm \
  --device=/dev/nvidia-modeset \
  --runtime=nvidia \
  -e VK_ICD_FILENAMES=/usr/share/vulkan/icd.d/nvidia_icd.json \
  -e VK_LAYER_PATH=/usr/share/vulkan/explicit_layer.d \
  -v &#34;$HOME/Downloads/Video2K/videos_in&#34;:/input \
  -v &#34;$HOME/Downloads/Video2K/videos_out&#34;:/output \
  --rm -it \
  ghcr.io/k4yt3x/video2x:6.4.0 \
  -i &#34;/input/(B-A)Rurouni_Kenshin_-_01_(A5D3CB57).mkv&#34; \
  -o &#34;/output/(B-A)Rurouni_Kenshin_-_01_(A5D3CB57)_upscaled.mkv&#34; \
  -p realesrgan -s 4 --realesrgan-model realesr-animevideov3</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>But it just won&rsquo;t work. If I only say &ldquo;&ndash;gpus all&rdquo; it grabs the iGPU by default. Upscaling a video ends up consuming 100% of my CPU, all 32 threads, but my NVIDIA GPU just sits there watching, doing nothing. If I try to pass &ldquo;/dev/dri/card0&rdquo; or &ldquo;/dev/nvidia0&rdquo;, it errors out and can&rsquo;t even start.</p>
<p>So from here on the solution is only for those who have NVIDIA:</p>
<h3>CUDA Dockerfile<span class="hx:absolute hx:-mt-20" id="cuda-dockerfile"></span>
    <a href="#cuda-dockerfile" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>This time I decided not to be lazy and organize everything into a little project on Github. Clone it directly from there <a href="https://github.com/akitaonrails/Batch-Anime-Upscaler-Video2K-Docker-CUDA"target="_blank" rel="noopener">Batch Anime Upscaler Video2K Docker CUDA</a>. Just do this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>git clone git@github.com:akitaonrails/Batch-Anime-Upscaler-Video2K-Docker-CUDA.git
cd Batch-Anime-Upscaler-Video2K-Docker-CUDA
mkdir input # put your old videos here
mkdir output

docker build --build-arg HOST_UID=$(id -u) --build-arg HOST_GID=$(id -g) -t anime-upscaler:latest .</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>If nothing goes wrong, this will build a Docker image with everything you need, then it&rsquo;s just a matter of placing your videos in the subdirectory I mentioned, or any other you want, and configuring the run command:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code> docker run --gpus all --rm \
  -v &#34;./input&#34;:/input \
  -v &#34;./output&#34;:/output \
  -e HOST_UID=$(id -u) -e HOST_GID=$(id -g) \
  anime-upscaler:latest</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>And that&rsquo;s it. It takes a while. With my Ryzen 7950X3D plus RTX 4090, it processes at a rate of around 13 frames per second. So less than half the playback speed. If a video is 20 minutes long, it&rsquo;ll take more than 40 minutes to process each one. It&rsquo;s a good idea to set aside plenty of time, and that&rsquo;s also why this is best reserved for videos you really want to watch.</p>
<p>The original Video2K script supports several parameters, which I left exposed as environment variables in the Dockerfile:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>MODEL=&#34;${MODEL:-realesr-animevideov3}&#34;
SCALE=&#34;${SCALE:-4}&#34;
TILE=&#34;${TILE:-0}&#34;
DENOISE=&#34;${DENOISE:-1.0}&#34;
NUM_PROC=&#34;${NUM_PROC:-1}&#34;</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Then you can change them like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>docker run --gpus all --rm \
  -e DENOISE=0.5 \
  -v ... \
  anime-upscaler:latest</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>As you can see, by default we use the pretrained model &ldquo;realesr-animevideov3&rdquo;, which is less detailed than the 6 billion parameter model but is much more stable between frames, which matters more for video. Most of the other models it supports, like &ldquo;RealESRGAN_x4plus&rdquo; or &ldquo;ESRGAN_x4&rdquo;, are meant for photos and images and not for video. Video demands temporal coherence, which is quite hard.</p>
<p>At most it&rsquo;s worth trying the &ldquo;realesr-general-x4v3&rdquo; model, which they say is a bit lighter, in case your machine is struggling too much to run the default. But then you&rsquo;ll have to modify the Dockerfile to download that model and rebuild:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code># === OPTIONAL: Preload additional Real-ESRGAN models ===
# Uncomment any models you want available in the container at build time

# Anime 6-block model (better for anime stills, line art)
# RUN wget -q https://github.com/xinntao/Real-ESRGAN/releases/download/v0.2.5.0/RealESRGAN_x4plus_anime_6B.pth \
#     -O /opt/Real-ESRGAN/weights/RealESRGAN_x4plus_anime_6B.pth

# Original RealESRGAN 4x general-purpose model
# RUN wget -q https://github.com/xinntao/Real-ESRGAN/releases/download/v0.2.5.0/RealESRGAN_x4plus.pth \
#     -O /opt/Real-ESRGAN/weights/RealESRGAN_x4plus.pth

# RealESRGAN 2x model (lighter upscale)
# RUN wget -q https://github.com/xinntao/Real-ESRGAN/releases/download/v0.2.5.0/RealESRGAN_x2plus.pth \
#     -O /opt/Real-ESRGAN/weights/RealESRGAN_x2plus.pth

# ESRNet (non-GAN model, ultra-smooth)
# RUN wget -q https://github.com/xinntao/Real-ESRGAN/releases/download/v0.2.5.0/RealESRNet_x4plus.pth \
#     -O /opt/Real-ESRGAN/weights/RealESRNet_x4plus.pth

# General-purpose lightweight video-friendly model (with denoise control)
# RUN wget -q https://github.com/xinntao/Real-ESRGAN/releases/download/v0.3.0/realesr-general-x4v3.pth \
#     -O /opt/Real-ESRGAN/weights/realesr-general-x4v3.pth

# Anime video model (default in your setup)
# RUN wget -q https://github.com/xinntao/Real-ESRGAN/releases/download/v0.2.5.0/realesr-animevideov3.pth \
#     -O /opt/Real-ESRGAN/weights/realesr-animevideov3.pth

# Original ESRGAN (2018) model
# RUN wget -q https://github.com/xinntao/Real-ESRGAN/releases/download/v0.2.5.0/ESRGAN_x4.pth \
#     -O /opt/Real-ESRGAN/weights/ESRGAN_x4.pth</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>I haven&rsquo;t tested all of them, but for anyone who wants to, I think those are the links. Change it in the Dockerfile, run the build, change the MODEL variable, and test on your videos.</p>
<p>What&rsquo;s worth tweaking are things like DENOISE, which is set to &ldquo;1.0&rdquo;, the maximum, but that can leave the image too &ldquo;stretched&rdquo;. It&rsquo;s worth trying smaller values, like &ldquo;0.5&rdquo;. It varies case by case, only by testing.</p>
<p>Another factor is the scale, SCALE, where the default is &ldquo;4x&rdquo;, which turns 1080p into 4K or 720p into 1440p. But sometimes you don&rsquo;t need to go that high, and there&rsquo;s no point because the original image already has very little detail anyway. Maybe 2x is enough. The script supports 2x, 3x, 4x but it caps at 4K resolution, so there&rsquo;s no point trying to turn a video that&rsquo;s already 4K into 8K, for example.</p>
<p>My RTX 4090 and my Ryzen 9 are quite powerful. Processing just one video, I don&rsquo;t see them breaking a sweat at all:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2dz1abq9b1wteho3r1onowmij14v" alt="btop"  loading="lazy" /></p>
<p>Clearly you could run more videos in parallel. You have to be careful when it&rsquo;s unbalanced. If the CPU climbs close to 100% just to separate the frames, having spare GPU capacity doesn&rsquo;t help. With both having headroom, you can make the script spin up more than 1 process on the GPU and send frames in parallel with the &ldquo;-e NUM_PROC=2&rdquo; option. Look at how it now uses more of the GPU and the CPU still has room to spare. Maybe 3 is the maximum, but you have to test, it&rsquo;s not constant the whole time.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/qur7lw4ox5s3nymh025u6q446831" alt="btop x2"  loading="lazy" /></p>
<p>This can dramatically reduce the total time. You can choose between increasing the GPU processes via the script or having two separate video directories and spinning up two parallel containers of this program to run at the same time, whichever works best for you.</p>
<h2>Final Result<span class="hx:absolute hx:-mt-20" id="final-result"></span>
    <a href="#final-result" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Let&rsquo;s see how that first clip turned out with 4x upscale and 1.0 denoise:</p>
<div class="video-container">
    <video controls>
        <source src="https://akitaonrails-videos.s3.us-east-2.amazonaws.com/kenshin_clip1_upscaled_jp.mp4">
        Your browser does not support the video tag. [Direct Link](https://akitaonrails-videos.s3.us-east-2.amazonaws.com/kenshin_clip1_upscaled_jp.mp4)
    </video>
</div>
<p>The difference is HUGE, but whether it ended up &ldquo;GOOD&rdquo; is going to be VERY subjective. Personally I didn&rsquo;t find the original bad to watch. Maybe tweaking DENOISE down a bit, or SCALE to 2x instead of 4x, would already be good enough, I don&rsquo;t know.</p>
<p>For comparison, here&rsquo;s another clip in the original old version:</p>
<div class="video-container">
    <video controls>
        <source src="https://akitaonrails-videos.s3.us-east-2.amazonaws.com/kenshin_old_clip2.mp4">
        Your browser does not support the video tag. [Direct Link](https://akitaonrails-videos.s3.us-east-2.amazonaws.com/kenshin_old_clip2.mp4)
    </video>
</div>
<p>And again, how it looks after the 4x upscale:</p>
<div class="video-container">
    <video controls>
        <source src="https://akitaonrails-videos.s3.us-east-2.amazonaws.com/kenshin_clip2_upscaled_jp.mp4">
        Your browser does not support the video tag. [Direct Link](https://akitaonrails-videos.s3.us-east-2.amazonaws.com/kenshin_clip2_upscaled_jp.mp4)
    </video>
</div>
<p>At least it&rsquo;s working as it should, from here on it&rsquo;s a matter of doing some tuning. If anyone has more tips or knows of newer open source upscaling models or projects, send them in the comments below!</p>
<p>Before someone asks, why not use NVIDIA&rsquo;s DLSS 3, which they use to upscale game resolution? Answer: because they don&rsquo;t expose that functionality as an API you can access, it&rsquo;s only for their proprietary drivers in game profiles. They turn it on and tune it case by case. There&rsquo;s no general API in the CUDA lib.</p>
<h2>Post-publication Update<span class="hx:absolute hx:-mt-20" id="post-publication-update"></span>
    <a href="#post-publication-update" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>I decided to test the RealERSGAN_x4plus_anime_6B model, which is much heavier than the realesr-animevideov3 I uploaded to the repository.</p>
<p>Change the Dockerfile and add this after the wget for the other model:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>RUN wget -q https://github.com/xinntao/Real-ESRGAN/releases/download/v0.2.2.4/RealESRGAN_x4plus_anime_6B.pth \
     -O /opt/Real-ESRGAN/weights/RealESRGAN_x4plus_anime_6B.pth</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>This model can&rsquo;t be run in parallel even on my RTX 4090, which stays constantly at 100%. If before it ran at around 13 frames per second, with this one it only runs at 3 frames per second. It&rsquo;s 4x heavier and only for really beefy GPUs. If you don&rsquo;t have something like that, stick with &ldquo;realesr-animevideov3&rdquo;, or with the even lighter (lower quality) &ldquo;realesr-general-x4v3&rdquo;. These models depend heavily on GPU power and especially on the amount of VRAM.</p>
<p>To run it, add &ldquo;-e MODEL=RealESRGAN_x4plus_anime_6B&rdquo; to the &ldquo;docker run&rdquo; command.</p>
<p>See how the same clip from before turned out using this model:</p>
<div class="video-container">
    <video controls>
        <source src="https://akitaonrails-videos.s3.us-east-2.amazonaws.com/kenshin_upscaled_clip1_x4plus.mp4">
        Your browser does not support the video tag. [Direct Link](https://akitaonrails-videos.s3.us-east-2.amazonaws.com/kenshin_upscaled_clip1_x4plus.mp4)
    </video>
</div>
<p>For having spent 4x more of my machine&rsquo;s resources to process it, I didn&rsquo;t find the final result that different, and in some parts I think it even has more defects. So that&rsquo;s it, the result will vary, you have to test. It depends a lot on the quality of the original anime, but just because a bigger and theoretically &ldquo;better&rdquo; model exists doesn&rsquo;t mean every result will turn out better.</p>
]]></content:encoded><category>generative-media</category><category>performance</category><category>containers</category></item><item><title>Colorizing Black and White Images with A.I.</title><link>https://akitaonrails.github.io/en/2025/04/19/colorizing-black-and-white-images-with-ai/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2025/04/19/colorizing-black-and-white-images-with-ai/</guid><pubDate>Sat, 19 Apr 2025 01:40:00 -0300</pubDate><description>&lt;p&gt;My girlfriend gave me a challenge today: she had old black and white photos and wanted to know if I could colorize them. If you go searching the Web you bump into a few sites for this, like &lt;a href="https://palette.fm/"target="_blank" rel="noopener"&gt;Palette&lt;/a&gt;. But it is paid, it is not cheap, and I do not think it can do something she wanted: use another colored image as a reference to pull the colors instead of letting the model guess.&lt;/p&gt;</description><content:encoded><![CDATA[<p>My girlfriend gave me a challenge today: she had old black and white photos and wanted to know if I could colorize them. If you go searching the Web you bump into a few sites for this, like <a href="https://palette.fm/"target="_blank" rel="noopener">Palette</a>. But it is paid, it is not cheap, and I do not think it can do something she wanted: use another colored image as a reference to pull the colors instead of letting the model guess.</p>
<p>I went poking around GitHub and there is an <a href="https://github.com/MarkMoHR/Awesome-Image-Colorization"target="_blank" rel="noopener">Awesome Image Colorization</a> page with a bunch of research papers. Very useful for researchers but totally useless for me, since I am not going to build one from scratch lol. There were links to projects like ChromaGAN, but that one has been discontinued for about 5 years. And several other projects I saw were also discontinued ages ago. I do not know why there is so little open source still actively maintained.</p>
<p>Luckily I bumped into a promising one: <a href="https://github.com/piddnad/DDColor"target="_blank" rel="noopener">DDColor</a>.</p>
<p>Let&rsquo;s clone the repo. And to keep going, I prefer to do everything inside Docker. Downloading Python dependencies always messes up my system and leaves a bunch of garbage behind, so the best thing is to isolate it all. Another thing, you need to download the binaries of the pre-trained models. Just for that I will use a bit of Python because of the &ldquo;modelscope&rdquo; lib that handles it:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git clone https://github.com/piddnad/DDColor.git
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> DDColor
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">mkdir modelscope
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># create a new venv inside the DDColor project</span>
</span></span><span class="line"><span class="cl">python -m venv venv
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">venv/bin/pip3 install modelscope
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">venv/bin/python3 -c <span class="s2">&#34;from modelscope.hub.snapshot_download import snapshot_download; snapshot_download(&#39;damo/cv_ddcolor_image-colorization&#39;, cache_dir=&#39;./modelscope&#39;)&#34;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Good Python practice is to do everything inside a VENV, right? Anyway, this should download the model binary, which weighs almost 900MB. Not huge.</p>
<p>Now we need a Dockerfile:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code># Use NVIDIA CUDA base image with cuDNN 8 and Python support
FROM nvidia/cuda:11.8.0-cudnn8-runtime-ubuntu22.04

# Install Python and system dependencies
RUN apt-get update &amp;&amp; apt-get install -y --no-install-recommends \
    python3 python3-pip git libgl1 libglib2.0-0 &amp;&amp; \
    apt-get clean &amp;&amp; rm -rf /var/lib/apt/lists/*

# Set working directory
WORKDIR /app

# Copy DDColor source code into the image
COPY . /app

# Install PyTorch (CUDA 11.8 compatible) and matching torchvision/torchaudio
RUN pip3 install torch==2.2.0 torchvision==0.17.0 torchaudio==2.2.0 \
    --index-url https://download.pytorch.org/whl/cu118

# Install Python dependencies, skipping dlib (not needed for inference)
RUN sed &#39;/dlib/d&#39; requirements.txt &gt; temp-req.txt &amp;&amp; pip3 install -r temp-req.txt &amp;&amp; rm temp-req.txt

# Optional: install modelscope if you want to run snapshot_download manually inside the container
RUN pip3 install modelscope</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Too lazy to open a pull request for this, if anyone wants to, send it over there.</p>
<p>Now we need to put a black and white image somewhere:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>mkdir input
mkdir results
mv ~/Downloads/bw.jpg input/</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Since my girlfriend&rsquo;s photos are family and private, obviously I am not going to share them, so I grabbed a random one from Google Images:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/qgt3lynkp1f5uzkbysyvio2kmq04" alt="B&W Photo"  loading="lazy" /></p>
<p>Now we drop it in &ldquo;./input&rdquo; and run Docker with this command:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>docker run --rm --gpus all \
  -v &#34;$PWD/input&#34;:/workspace/input_images:ro \
  -v &#34;$PWD/results&#34;:/workspace/results:rw \
  -v &#34;$PWD/modelscope&#34;:/app/modelscope:ro \
  ddcolor:latest \
  python3 infer.py \
    --model_path /app/modelscope/damo/cv_ddcolor_image-colorization/pytorch_model.pt \
    --input /workspace/input_images \
    --output /workspace/results</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Pay attention to the directory mapping. And note the &ldquo;&ndash;gpus&rdquo; option, which only works in my case because I have an RTX 4090 installed here. I have no idea if it is the same with AMD, but since the image is based on cuDNN/CUDA, I would say no. Ask ChatGPT.</p>
<p>If everything is right and it runs successfully, it will take just 1 second and produce this output in the terminal:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>==========
== CUDA ==
==========

CUDA Version 11.8.0

Container image Copyright (c) 2016-2023, NVIDIA CORPORATION &amp; AFFILIATES. All rights reserved.

This container image and its contents are governed by the NVIDIA Deep Learning Container License.
By pulling and using the container, you accept the terms and conditions of this license:
https://developer.nvidia.com/ngc/nvidia-deep-learning-container-license

A copy of this license is made available in this container at /NGC-DL-CONTAINER-LICENSE for your convenience.

Output path: /workspace/results
100%|██████████| 1/1 [00:00&lt;00:00,  4.09it/s]</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>And here is the result that shows up in the <code>./results</code> directory:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/o2r085pzzpsavbyr39btg1vq0hwp" alt="Re-colorized photo"  loading="lazy" /></p>
<p>Yes, I was impressed. Even with the image being from the Web, with watermark and all, it did everything properly and worked!!</p>
<p>The only problem left is being able to use a reference image to try to pull the colors. That is a harder problem and I asked ChatGPT, which answered me this:</p>
<p><strong>No native support for a reference image:</strong></p>
<p>The DDColor model, both in the official version hosted on ModelScope and in the <code>piddnad/DDColor</code> GitHub repo, was designed only for <strong>automatic colorization of black and white images</strong>, with no option to accept a reference image as input. The official description itself makes it clear that the model takes <strong>a single B&amp;W image</strong> and returns a colored image.</p>
<p>The model&rsquo;s <code>forward</code> method expects <strong>only one tensor</strong>, the B&amp;W image converted into artificial RGB (actually the luminance expanded into 3 channels). In other words, <strong>there is no place in the model to pass a second image as a color reference</strong>.</p>
<p>This is also evident in the original <code>predict.py</code> from the Cog library, which only accepts the <code>image</code> parameter as input — with no support for <code>reference</code>, <code>ref_image</code> or anything of the kind.</p>
<h3>How to solve the problem: two possible approaches<span class="hx:absolute hx:-mt-20" id="how-to-solve-the-problem-two-possible-approaches"></span>
    <a href="#how-to-solve-the-problem-two-possible-approaches" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Since DDColor does not natively support a reference image to guide the colors, we have two alternatives:</p>
<h3>🔁 1. Use a model specialized in reference-based colorization<span class="hx:absolute hx:-mt-20" id="-1-use-a-model-specialized-in-reference-based-colorization"></span>
    <a href="#-1-use-a-model-specialized-in-reference-based-colorization" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>There are models developed specifically for this, such as:</p>
<ul>
<li>
<p><strong>Deep Exemplar-based Colorization</strong> (Zhang et al., CVPR 2018)</p>
</li>
<li>
<p><strong>ChromaGAN</strong> (2019)</p>
</li>
<li>
<p><strong>Exemplar-Transformer Colorization</strong> (Yin et al., ACM MM 2021)</p>
</li>
<li>
<p><strong>UniColor</strong> (Huang et al., 2022)</p>
</li>
</ul>
<p>These models accept <strong>two images</strong>:</p>
<ul>
<li>
<p>The B&amp;W image (main input)</p>
</li>
<li>
<p>A colored image as a <strong>color reference</strong></p>
</li>
</ul>
<p>These models map semantic similarities (e.g., &ldquo;skin here should have the same skin color as over there&rdquo;) and do a transfer <strong>based on the image content</strong>, getting much more detailed and specific results. However, this would require:</p>
<ul>
<li>
<p>Downloading different weights</p>
</li>
<li>
<p>Completely changing the architecture used</p>
</li>
<li>
<p>Adapting the pipeline and inference</p>
</li>
</ul>
<h3>🎯 2. Use global color transfer after the DDColor output (our current approach)<span class="hx:absolute hx:-mt-20" id="-2-use-global-color-transfer-after-the-ddcolor-output-our-current-approach"></span>
    <a href="#-2-use-global-color-transfer-after-the-ddcolor-output-our-current-approach" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The practical and lightweight solution we used was:
➡️ Run DDColor normally to generate the colored image automatically
➡️ Then <strong>globally</strong> adjust the colors of the result to look more like the reference image.</p>
<p>This adjustment is done with the <strong>Reinhard et al. (2001)</strong> algorithm:</p>
<ol>
<li>
<p>We convert both the output image and the reference to the Lab color space.</p>
</li>
<li>
<p>We compute the <strong>mean and standard deviation</strong> of the <code>a</code> and <code>b</code> channels (the ones that represent color).</p>
</li>
<li>
<p>We adjust the result to have <strong>the same statistical distribution</strong> of color as the reference image.</p>
</li>
<li>
<p>This tends to leave the image with the same overall &ldquo;mood&rdquo; or tone (warmer, cooler, more vibrant).</p>
</li>
</ol>
<p>This method is fast, lightweight, and works <strong>without altering the DDColor model</strong>.</p>
<h2>Continuing<span class="hx:absolute hx:-mt-20" id="continuing"></span>
    <a href="#continuing" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>I used the modification it suggested to the predict.py file:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">os</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">cv2</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">numpy</span> <span class="k">as</span> <span class="nn">np</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">torch</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">torch.nn.functional</span> <span class="k">as</span> <span class="nn">F</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">cog</span> <span class="kn">import</span> <span class="n">BasePredictor</span><span class="p">,</span> <span class="n">Input</span><span class="p">,</span> <span class="n">Path</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">basicsr.archs.ddcolor_arch</span> <span class="kn">import</span> <span class="n">DDColor</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">color_transfer</span><span class="p">(</span><span class="n">reference_bgr</span><span class="p">:</span> <span class="n">np</span><span class="o">.</span><span class="n">ndarray</span><span class="p">,</span> <span class="n">target_bgr</span><span class="p">:</span> <span class="n">np</span><span class="o">.</span><span class="n">ndarray</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">np</span><span class="o">.</span><span class="n">ndarray</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;&#34;&#34;Transfer color palette of reference image to target image using Lab mean/std.&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Convert BGR images to Lab color space (float32 for precision)</span>
</span></span><span class="line"><span class="cl">    <span class="n">ref_lab</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">cvtColor</span><span class="p">(</span><span class="n">reference_bgr</span><span class="p">,</span> <span class="n">cv2</span><span class="o">.</span><span class="n">COLOR_BGR2LAB</span><span class="p">)</span><span class="o">.</span><span class="n">astype</span><span class="p">(</span><span class="s2">&#34;float32&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">tgt_lab</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">cvtColor</span><span class="p">(</span><span class="n">target_bgr</span><span class="p">,</span> <span class="n">cv2</span><span class="o">.</span><span class="n">COLOR_BGR2LAB</span><span class="p">)</span><span class="o">.</span><span class="n">astype</span><span class="p">(</span><span class="s2">&#34;float32&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Split channels</span>
</span></span><span class="line"><span class="cl">    <span class="n">L_ref</span><span class="p">,</span> <span class="n">a_ref</span><span class="p">,</span> <span class="n">b_ref</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="n">ref_lab</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">L_tgt</span><span class="p">,</span> <span class="n">a_tgt</span><span class="p">,</span> <span class="n">b_tgt</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="n">tgt_lab</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Compute mean and std for reference and target (a and b channels)</span>
</span></span><span class="line"><span class="cl">    <span class="n">a_ref_mean</span><span class="p">,</span> <span class="n">a_ref_std</span> <span class="o">=</span> <span class="n">a_ref</span><span class="o">.</span><span class="n">mean</span><span class="p">(),</span> <span class="n">a_ref</span><span class="o">.</span><span class="n">std</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="n">b_ref_mean</span><span class="p">,</span> <span class="n">b_ref_std</span> <span class="o">=</span> <span class="n">b_ref</span><span class="o">.</span><span class="n">mean</span><span class="p">(),</span> <span class="n">b_ref</span><span class="o">.</span><span class="n">std</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="n">a_tgt_mean</span><span class="p">,</span> <span class="n">a_tgt_std</span> <span class="o">=</span> <span class="n">a_tgt</span><span class="o">.</span><span class="n">mean</span><span class="p">(),</span> <span class="n">a_tgt</span><span class="o">.</span><span class="n">std</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="n">b_tgt_mean</span><span class="p">,</span> <span class="n">b_tgt_std</span> <span class="o">=</span> <span class="n">b_tgt</span><span class="o">.</span><span class="n">mean</span><span class="p">(),</span> <span class="n">b_tgt</span><span class="o">.</span><span class="n">std</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Subtract target means, scale by reference/target std, add reference means</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># (Avoid division by zero in case of zero std)</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">a_tgt_std</span> <span class="o">&gt;</span> <span class="mf">1e-6</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">a_tgt</span> <span class="o">=</span> <span class="p">((</span><span class="n">a_tgt</span> <span class="o">-</span> <span class="n">a_tgt_mean</span><span class="p">)</span> <span class="o">*</span> <span class="p">(</span><span class="n">a_ref_std</span> <span class="o">/</span> <span class="n">a_tgt_std</span><span class="p">))</span> <span class="o">+</span> <span class="n">a_ref_mean</span>
</span></span><span class="line"><span class="cl">    <span class="k">else</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">a_tgt</span> <span class="o">=</span> <span class="n">a_tgt</span> <span class="o">-</span> <span class="n">a_tgt_mean</span> <span class="o">+</span> <span class="n">a_ref_mean</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">b_tgt_std</span> <span class="o">&gt;</span> <span class="mf">1e-6</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">b_tgt</span> <span class="o">=</span> <span class="p">((</span><span class="n">b_tgt</span> <span class="o">-</span> <span class="n">b_tgt_mean</span><span class="p">)</span> <span class="o">*</span> <span class="p">(</span><span class="n">b_ref_std</span> <span class="o">/</span> <span class="n">b_tgt_std</span><span class="p">))</span> <span class="o">+</span> <span class="n">b_ref_mean</span>
</span></span><span class="line"><span class="cl">    <span class="k">else</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">b_tgt</span> <span class="o">=</span> <span class="n">b_tgt</span> <span class="o">-</span> <span class="n">b_tgt_mean</span> <span class="o">+</span> <span class="n">b_ref_mean</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Clip values to valid Lab range [0,255] after transfer</span>
</span></span><span class="line"><span class="cl">    <span class="n">a_tgt</span> <span class="o">=</span> <span class="n">np</span><span class="o">.</span><span class="n">clip</span><span class="p">(</span><span class="n">a_tgt</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">255</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">b_tgt</span> <span class="o">=</span> <span class="n">np</span><span class="o">.</span><span class="n">clip</span><span class="p">(</span><span class="n">b_tgt</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">255</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Merge channels back (use original L from target to preserve brightness)</span>
</span></span><span class="line"><span class="cl">    <span class="n">merged_lab</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">merge</span><span class="p">([</span><span class="n">L_tgt</span><span class="p">,</span> <span class="n">a_tgt</span><span class="p">,</span> <span class="n">b_tgt</span><span class="p">])</span><span class="o">.</span><span class="n">astype</span><span class="p">(</span><span class="s2">&#34;uint8&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Convert back to BGR color space</span>
</span></span><span class="line"><span class="cl">    <span class="n">result_bgr</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">cvtColor</span><span class="p">(</span><span class="n">merged_lab</span><span class="p">,</span> <span class="n">cv2</span><span class="o">.</span><span class="n">COLOR_LAB2BGR</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">result_bgr</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">ImageColorizationPipeline</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;&#34;&#34;Helper pipeline to load DDColor model and process images.&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">model_path</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">input_size</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">256</span><span class="p">,</span> <span class="n">model_size</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="s2">&#34;large&#34;</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">device</span> <span class="o">=</span> <span class="n">torch</span><span class="o">.</span><span class="n">device</span><span class="p">(</span><span class="s2">&#34;cuda&#34;</span> <span class="k">if</span> <span class="n">torch</span><span class="o">.</span><span class="n">cuda</span><span class="o">.</span><span class="n">is_available</span><span class="p">()</span> <span class="k">else</span> <span class="s2">&#34;cpu&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># Choose encoder backbone based on model size</span>
</span></span><span class="line"><span class="cl">        <span class="n">encoder_name</span> <span class="o">=</span> <span class="s2">&#34;convnext-t&#34;</span> <span class="k">if</span> <span class="n">model_size</span> <span class="o">==</span> <span class="s2">&#34;tiny&#34;</span> <span class="k">else</span> <span class="s2">&#34;convnext-l&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># Initialize DDColor model</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">model</span> <span class="o">=</span> <span class="n">DDColor</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">            <span class="n">encoder_name</span><span class="o">=</span><span class="n">encoder_name</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="n">decoder_name</span><span class="o">=</span><span class="s2">&#34;MultiScaleColorDecoder&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="n">input_size</span><span class="o">=</span><span class="p">[</span><span class="n">input_size</span><span class="p">,</span> <span class="n">input_size</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">            <span class="n">num_output_channels</span><span class="o">=</span><span class="mi">2</span><span class="p">,</span>    <span class="c1"># model predicts ab channels</span>
</span></span><span class="line"><span class="cl">            <span class="n">last_norm</span><span class="o">=</span><span class="s2">&#34;Spectral&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="n">do_normalize</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="n">num_queries</span><span class="o">=</span><span class="mi">100</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="n">num_scales</span><span class="o">=</span><span class="mi">3</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="n">dec_layers</span><span class="o">=</span><span class="mi">9</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="p">)</span><span class="o">.</span><span class="n">to</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">device</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># Load weights</span>
</span></span><span class="line"><span class="cl">        <span class="n">state</span> <span class="o">=</span> <span class="n">torch</span><span class="o">.</span><span class="n">load</span><span class="p">(</span><span class="n">model_path</span><span class="p">,</span> <span class="n">map_location</span><span class="o">=</span><span class="s2">&#34;cpu&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># Some checkpoints store weights under &#39;params&#39; key</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">model</span><span class="o">.</span><span class="n">load_state_dict</span><span class="p">(</span><span class="n">state</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&#34;params&#34;</span><span class="p">,</span> <span class="n">state</span><span class="p">),</span> <span class="n">strict</span><span class="o">=</span><span class="kc">False</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">model</span><span class="o">.</span><span class="n">eval</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">input_size</span> <span class="o">=</span> <span class="n">input_size</span>  <span class="c1"># store for processing</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nd">@torch.no_grad</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">process</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">img_bgr</span><span class="p">:</span> <span class="n">np</span><span class="o">.</span><span class="n">ndarray</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">np</span><span class="o">.</span><span class="n">ndarray</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;&#34;&#34;Colorize a BGR image (numpy array) using the loaded DDColor model.&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># Preserve original resolution L channel</span>
</span></span><span class="line"><span class="cl">        <span class="n">orig_h</span><span class="p">,</span> <span class="n">orig_w</span> <span class="o">=</span> <span class="n">img_bgr</span><span class="o">.</span><span class="n">shape</span><span class="p">[:</span><span class="mi">2</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># Convert to Lab and extract L channel at original size</span>
</span></span><span class="line"><span class="cl">        <span class="n">img_lab</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">cvtColor</span><span class="p">(</span><span class="n">img_bgr</span><span class="p">,</span> <span class="n">cv2</span><span class="o">.</span><span class="n">COLOR_BGR2LAB</span><span class="p">)</span><span class="o">.</span><span class="n">astype</span><span class="p">(</span><span class="s2">&#34;float32&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">orig_L</span> <span class="o">=</span> <span class="n">img_lab</span><span class="p">[:,</span> <span class="p">:,</span> <span class="mi">0</span><span class="p">:</span><span class="mi">1</span><span class="p">]</span>  <span class="c1"># shape (H, W, 1)</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># Prepare grayscale input at model resolution</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># Resize input to model expected size</span>
</span></span><span class="line"><span class="cl">        <span class="n">inp</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">resize</span><span class="p">(</span><span class="n">img_bgr</span><span class="p">,</span> <span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">input_size</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">input_size</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">        <span class="n">inp_lab</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">cvtColor</span><span class="p">(</span><span class="n">inp</span><span class="p">,</span> <span class="n">cv2</span><span class="o">.</span><span class="n">COLOR_BGR2LAB</span><span class="p">)</span><span class="o">.</span><span class="n">astype</span><span class="p">(</span><span class="s2">&#34;float32&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">L</span> <span class="o">=</span> <span class="n">inp_lab</span><span class="p">[:,</span> <span class="p">:,</span> <span class="mi">0</span><span class="p">:</span><span class="mi">1</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># Create grayscale Lab image by zeroing AB channels</span>
</span></span><span class="line"><span class="cl">        <span class="n">gray_lab</span> <span class="o">=</span> <span class="n">np</span><span class="o">.</span><span class="n">concatenate</span><span class="p">([</span><span class="n">L</span><span class="p">,</span> <span class="n">np</span><span class="o">.</span><span class="n">zeros_like</span><span class="p">(</span><span class="n">L</span><span class="p">),</span> <span class="n">np</span><span class="o">.</span><span class="n">zeros_like</span><span class="p">(</span><span class="n">L</span><span class="p">)],</span> <span class="n">axis</span><span class="o">=</span><span class="mi">2</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># Convert back to RGB (now it&#39;s a gray RGB image of size input_size)</span>
</span></span><span class="line"><span class="cl">        <span class="n">gray_rgb</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">cvtColor</span><span class="p">(</span><span class="n">gray_lab</span><span class="o">.</span><span class="n">astype</span><span class="p">(</span><span class="s2">&#34;uint8&#34;</span><span class="p">),</span> <span class="n">cv2</span><span class="o">.</span><span class="n">COLOR_LAB2RGB</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># Prepare tensor and run model</span>
</span></span><span class="line"><span class="cl">        <span class="n">tensor</span> <span class="o">=</span> <span class="n">torch</span><span class="o">.</span><span class="n">from_numpy</span><span class="p">(</span><span class="n">gray_rgb</span><span class="o">.</span><span class="n">transpose</span><span class="p">(</span><span class="mi">2</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">))</span><span class="o">.</span><span class="n">float</span><span class="p">()</span><span class="o">.</span><span class="n">unsqueeze</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span><span class="o">.</span><span class="n">to</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">device</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">out_ab</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">model</span><span class="p">(</span><span class="n">tensor</span><span class="p">)</span>  <span class="c1"># output is (1,2,H_out,W_out) in Lab AB</span>
</span></span><span class="line"><span class="cl">        <span class="n">out_ab</span> <span class="o">=</span> <span class="n">out_ab</span><span class="o">.</span><span class="n">cpu</span><span class="p">()</span><span class="o">.</span><span class="n">float</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># Resize output AB to original image size</span>
</span></span><span class="line"><span class="cl">        <span class="n">out_ab_resized</span> <span class="o">=</span> <span class="n">F</span><span class="o">.</span><span class="n">interpolate</span><span class="p">(</span><span class="n">out_ab</span><span class="p">,</span> <span class="n">size</span><span class="o">=</span><span class="p">(</span><span class="n">orig_h</span><span class="p">,</span> <span class="n">orig_w</span><span class="p">),</span> <span class="n">mode</span><span class="o">=</span><span class="s2">&#34;bilinear&#34;</span><span class="p">,</span> <span class="n">align_corners</span><span class="o">=</span><span class="kc">False</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">out_ab_resized</span> <span class="o">=</span> <span class="n">out_ab_resized</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="o">.</span><span class="n">numpy</span><span class="p">()</span><span class="o">.</span><span class="n">transpose</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span>  <span class="c1"># (H, W, 2)</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># Combine original L and predicted AB, then convert to BGR</span>
</span></span><span class="line"><span class="cl">        <span class="n">out_lab</span> <span class="o">=</span> <span class="n">np</span><span class="o">.</span><span class="n">concatenate</span><span class="p">([</span><span class="n">orig_L</span><span class="p">,</span> <span class="n">out_ab_resized</span><span class="p">],</span> <span class="n">axis</span><span class="o">=</span><span class="mi">2</span><span class="p">)</span><span class="o">.</span><span class="n">astype</span><span class="p">(</span><span class="s2">&#34;uint8&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">out_bgr</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">cvtColor</span><span class="p">(</span><span class="n">out_lab</span><span class="p">,</span> <span class="n">cv2</span><span class="o">.</span><span class="n">COLOR_LAB2BGR</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">out_bgr</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">Predictor</span><span class="p">(</span><span class="n">BasePredictor</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">setup</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;&#34;&#34;Load models into memory for efficient multiple predictions.&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># Determine paths for large and tiny model weights</span>
</span></span><span class="line"><span class="cl">        <span class="n">large_model_path</span> <span class="o">=</span> <span class="s2">&#34;checkpoints/ddcolor_modelscope.pth&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="n">tiny_model_path</span>  <span class="o">=</span> <span class="s2">&#34;checkpoints/ddcolor_paper_tiny.pth&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># (Ensure the above files exist. If not, download from ModelScope or HuggingFace as described.)</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># Initialize pipelines for large and tiny models</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">colorizer_large</span> <span class="o">=</span> <span class="n">ImageColorizationPipeline</span><span class="p">(</span><span class="n">model_path</span><span class="o">=</span><span class="n">large_model_path</span><span class="p">,</span> <span class="n">input_size</span><span class="o">=</span><span class="mi">512</span><span class="p">,</span> <span class="n">model_size</span><span class="o">=</span><span class="s2">&#34;large&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">colorizer_tiny</span>  <span class="o">=</span> <span class="n">ImageColorizationPipeline</span><span class="p">(</span><span class="n">model_path</span><span class="o">=</span><span class="n">tiny_model_path</span><span class="p">,</span>  <span class="n">input_size</span><span class="o">=</span><span class="mi">512</span><span class="p">,</span> <span class="n">model_size</span><span class="o">=</span><span class="s2">&#34;tiny&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">predict</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">image</span><span class="p">:</span> <span class="n">Path</span> <span class="o">=</span> <span class="n">Input</span><span class="p">(</span><span class="n">description</span><span class="o">=</span><span class="s2">&#34;Grayscale input image.&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="n">reference</span><span class="p">:</span> <span class="n">Path</span> <span class="o">=</span> <span class="n">Input</span><span class="p">(</span><span class="n">description</span><span class="o">=</span><span class="s2">&#34;Optional reference image to guide colors&#34;</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="kc">None</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="n">model_size</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="n">Input</span><span class="p">(</span><span class="n">description</span><span class="o">=</span><span class="s2">&#34;Model size to use (large = highest quality, tiny = faster).&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                                <span class="n">choices</span><span class="o">=</span><span class="p">[</span><span class="s2">&#34;large&#34;</span><span class="p">,</span> <span class="s2">&#34;tiny&#34;</span><span class="p">],</span> <span class="n">default</span><span class="o">=</span><span class="s2">&#34;large&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span> <span class="o">-&gt;</span> <span class="n">Path</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;&#34;&#34;Run a single prediction. Colorizes the image, optionally using a reference image for color bias.&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># Read input image</span>
</span></span><span class="line"><span class="cl">        <span class="n">img</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">imread</span><span class="p">(</span><span class="nb">str</span><span class="p">(</span><span class="n">image</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="n">img</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="k">raise</span> <span class="ne">ValueError</span><span class="p">(</span><span class="s2">&#34;Failed to load input image&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># Select model pipeline</span>
</span></span><span class="line"><span class="cl">        <span class="n">pipeline</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">colorizer_tiny</span> <span class="k">if</span> <span class="n">model_size</span> <span class="o">==</span> <span class="s2">&#34;tiny&#34;</span> <span class="k">else</span> <span class="bp">self</span><span class="o">.</span><span class="n">colorizer_large</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># Colorize the image using DDColor</span>
</span></span><span class="line"><span class="cl">        <span class="n">output_bgr</span> <span class="o">=</span> <span class="n">pipeline</span><span class="o">.</span><span class="n">process</span><span class="p">(</span><span class="n">img</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># If a reference image is provided, transfer its color style to the output</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="n">reference</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="n">ref_img</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">imread</span><span class="p">(</span><span class="nb">str</span><span class="p">(</span><span class="n">reference</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">            <span class="k">if</span> <span class="n">ref_img</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">                <span class="k">raise</span> <span class="ne">ValueError</span><span class="p">(</span><span class="s2">&#34;Failed to load reference image&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="c1"># Only apply transfer if reference is a color image</span>
</span></span><span class="line"><span class="cl">            <span class="k">if</span> <span class="n">ref_img</span><span class="o">.</span><span class="n">shape</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span> <span class="o">==</span> <span class="mi">3</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">                <span class="n">output_bgr</span> <span class="o">=</span> <span class="n">color_transfer</span><span class="p">(</span><span class="n">ref_img</span><span class="p">,</span> <span class="n">output_bgr</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># Save result</span>
</span></span><span class="line"><span class="cl">        <span class="n">out_path</span> <span class="o">=</span> <span class="s2">&#34;/tmp/out.png&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="n">cv2</span><span class="o">.</span><span class="n">imwrite</span><span class="p">(</span><span class="n">out_path</span><span class="p">,</span> <span class="n">output_bgr</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">Path</span><span class="p">(</span><span class="n">out_path</span><span class="p">)</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>With this modification I can run with a new parameter, note the &ldquo;&ndash;ref&rdquo; option that exists now and place your image in the right spot:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>❯ docker run --rm --gpus all \
  -v &#34;$PWD/input&#34;:/workspace/input_images:ro \
  -v &#34;$PWD/results&#34;:/workspace/results:rw \
  -v &#34;$PWD/ref_images&#34;:/workspace/ref_images:ro \
  -v &#34;$PWD/modelscope&#34;:/app/modelscope:ro \
  -v &#34;$PWD/infer.py&#34;:/app/infer.py:ro \
  ddcolor:latest \
  python3 /app/infer.py \
    --model_path /app/modelscope/damo/cv_ddcolor_image-colorization/pytorch_model.pt \
    --input /workspace/input_images \
    --ref /workspace/ref_images/images.jpg \
    --output /workspace/results</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>I passed in another colored reference photo but saw no difference in the result. Once again, I asked ChatGPT:</p>
<h3>Why didn&rsquo;t you notice a difference in the result?<span class="hx:absolute hx:-mt-20" id="why-didnt-you-notice-a-difference-in-the-result"></span>
    <a href="#why-didnt-you-notice-a-difference-in-the-result" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The <strong>global color transfer</strong> only changes the &ldquo;overall palette&rdquo; — and not the colors of each object.</p>
<p>If the reference image has soft tones similar to what DDColor would already apply automatically, the final effect <strong>will look identical</strong>.</p>
<p>But if you use as reference a heavily saturated image (for example, an orange sky, or a deep blue environment), you will see DDColor&rsquo;s output <strong>visually pulled</strong> toward that style.</p>
<h2>Conclusion<span class="hx:absolute hx:-mt-20" id="conclusion"></span>
    <a href="#conclusion" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>I followed the suggestion and downloaded another totally unrelated image, colored, super saturated with vibrant colors to see if it made a difference. This is the reference image I passed to Docker:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/ryptql9u8mk04o3r7ah01fk1qz5q" alt="Reference Image"  loading="lazy" /></p>
<p>And here is the new result applying it to the previous image in post-processing (this step is no longer A.I., it really is just image post-processing).</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/cxcwwnj2ncszvmv4whj4qesafgnm" alt="Post-processed photo"  loading="lazy" /></p>
<p>Compare it with the other re-colorized photo, I will put it down here again so you can compare:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/o2r085pzzpsavbyr39btg1vq0hwp" alt="Repeating the first re-colorization"  loading="lazy" /></p>
<p>Got it? Now you have to keep testing with different references to see how they influence the final result, but indeed, it manages to apply the Reinhard algorithm and do a global color transfer. As the name says, it is &ldquo;global&rdquo;, not &ldquo;per object&rdquo;, so it is hard to control only parts of the image — it acts on the whole image. But in theory I think you could do a manual colorization in Photoshop with saturated colors, near where you want to influence, to try to nudge it, but I have not tried that yet. Later, if anyone does, drop it in the comments.</p>
<p>But that is it. Now at least I can have fun grabbing old family photos and re-colorizing them.</p>
]]></content:encoded><category>generative-media</category><category>containers</category><category>tutorials</category></item><item><title>My "Personal Netflix" with Docker Compose</title><link>https://akitaonrails.github.io/en/2024/04/03/my-personal-netflix-with-docker-compose/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2024/04/03/my-personal-netflix-with-docker-compose/</guid><pubDate>Wed, 03 Apr 2024 13:30:00 -0300</pubDate><description>&lt;p&gt;Anyone who followed my &lt;a href="https://www.youtube.com/channel/UCib793mnUOhWymCh2VJKplQ"target="_blank" rel="noopener"&gt;YouTube&lt;/a&gt; channel or my &lt;a href="https://www.instagram.com/akitaonrails/"target="_blank" rel="noopener"&gt;Instagram&lt;/a&gt; already knows the saga with my &lt;a href="https://instagram.fcgh13-1.fna.fbcdn.net/v/t51.29350-15/285491936_1978927048966317_5629598792539832539_n.jpg?stp=c0.181.965.965a_dst-jpg_e35_s150x150&amp;amp;_nc_ht=instagram.fcgh13-1.fna.fbcdn.net&amp;amp;_nc_cat=109&amp;amp;_nc_ohc=9AzXyFhsnbcAX_Rf1Ei&amp;amp;edm=AGW0Xe4BAAAA&amp;amp;ccb=7-5&amp;amp;oh=00_AfBLLcqehVZYeDCRMQJ_Ont8CcybMrpmtHtoW4UkpV1GJA&amp;amp;oe=660E928F&amp;amp;_nc_sid=94fea1"target="_blank" rel="noopener"&gt;NAS&lt;/a&gt; (my personal server), my &lt;a href="https://instagram.fcgh13-1.fna.fbcdn.net/v/t51.29350-15/332393994_6261434103896443_128632134976019123_n.webp?stp=c170.267.761.761a_dst-jpg_e35_s150x150&amp;amp;_nc_ht=instagram.fcgh13-1.fna.fbcdn.net&amp;amp;_nc_cat=107&amp;amp;_nc_ohc=GxgcgjHRywEAX-oyW9H&amp;amp;edm=AGW0Xe4BAAAA&amp;amp;ccb=7-5&amp;amp;oh=00_AfAsh8cPFO7E4NzEwOw7MeNrMD4UBzFcL3qaeHT7qYLDtA&amp;amp;oe=660E9C85&amp;amp;_nc_sid=94fea1"target="_blank" rel="noopener"&gt;Synology DS1821+&lt;/a&gt; with almost 80 TB of space.&lt;/p&gt;
&lt;p&gt;Every video from my channel, including the original source files, is there. That alone is terabytes. My whole Steam library is there, around 4 terabytes. All my old games, retro games too, including Xbox 360 and PS3 ISOs. That&amp;rsquo;s another 4 terabytes. My entire Ultra HD (4K BluRay) disc collection &lt;a href="https://instagram.fcgh13-1.fna.fbcdn.net/v/t51.29350-15/412635176_915622043464387_8034096537912791825_n.heic?stp=c513.618.363.363a_dst-jpg_e35_s150x150&amp;amp;_nc_ht=instagram.fcgh13-1.fna.fbcdn.net&amp;amp;_nc_cat=107&amp;amp;_nc_ohc=OOeWxOEhySoAX8yISjX&amp;amp;edm=AGW0Xe4BAAAA&amp;amp;ccb=7-5&amp;amp;oh=00_AfAaINFVuRSfg-isKRLy-fmtXvSTVz-d7Axf1d2jQL6ZvQ&amp;amp;oe=660EA292&amp;amp;_nc_sid=94fea1"target="_blank" rel="noopener"&gt;I ripped&lt;/a&gt; (made a backup), a few more terabytes. Right now I&amp;rsquo;m already using more than 50 terabytes.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Anyone who followed my <a href="https://www.youtube.com/channel/UCib793mnUOhWymCh2VJKplQ"target="_blank" rel="noopener">YouTube</a> channel or my <a href="https://www.instagram.com/akitaonrails/"target="_blank" rel="noopener">Instagram</a> already knows the saga with my <a href="https://instagram.fcgh13-1.fna.fbcdn.net/v/t51.29350-15/285491936_1978927048966317_5629598792539832539_n.jpg?stp=c0.181.965.965a_dst-jpg_e35_s150x150&amp;_nc_ht=instagram.fcgh13-1.fna.fbcdn.net&amp;_nc_cat=109&amp;_nc_ohc=9AzXyFhsnbcAX_Rf1Ei&amp;edm=AGW0Xe4BAAAA&amp;ccb=7-5&amp;oh=00_AfBLLcqehVZYeDCRMQJ_Ont8CcybMrpmtHtoW4UkpV1GJA&amp;oe=660E928F&amp;_nc_sid=94fea1"target="_blank" rel="noopener">NAS</a> (my personal server), my <a href="https://instagram.fcgh13-1.fna.fbcdn.net/v/t51.29350-15/332393994_6261434103896443_128632134976019123_n.webp?stp=c170.267.761.761a_dst-jpg_e35_s150x150&amp;_nc_ht=instagram.fcgh13-1.fna.fbcdn.net&amp;_nc_cat=107&amp;_nc_ohc=GxgcgjHRywEAX-oyW9H&amp;edm=AGW0Xe4BAAAA&amp;ccb=7-5&amp;oh=00_AfAsh8cPFO7E4NzEwOw7MeNrMD4UBzFcL3qaeHT7qYLDtA&amp;oe=660E9C85&amp;_nc_sid=94fea1"target="_blank" rel="noopener">Synology DS1821+</a> with almost 80 TB of space.</p>
<p>Every video from my channel, including the original source files, is there. That alone is terabytes. My whole Steam library is there, around 4 terabytes. All my old games, retro games too, including Xbox 360 and PS3 ISOs. That&rsquo;s another 4 terabytes. My entire Ultra HD (4K BluRay) disc collection <a href="https://instagram.fcgh13-1.fna.fbcdn.net/v/t51.29350-15/412635176_915622043464387_8034096537912791825_n.heic?stp=c513.618.363.363a_dst-jpg_e35_s150x150&amp;_nc_ht=instagram.fcgh13-1.fna.fbcdn.net&amp;_nc_cat=107&amp;_nc_ohc=OOeWxOEhySoAX8yISjX&amp;edm=AGW0Xe4BAAAA&amp;ccb=7-5&amp;oh=00_AfAaINFVuRSfg-isKRLy-fmtXvSTVz-d7Axf1d2jQL6ZvQ&amp;oe=660EA292&amp;_nc_sid=94fea1"target="_blank" rel="noopener">I ripped</a> (made a backup), a few more terabytes. Right now I&rsquo;m already using more than 50 terabytes.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/29mxcuiblyunqnnbtc10sdnqjl3f" alt="My Plex with UHDs"  loading="lazy" /></p>
<p>Before anyone jumps in with opinions, yes, this isn&rsquo;t for everyone. We&rsquo;re talking about 8 drives of 10.9 TB, plus an upgrade of 2 drives of 20 TB, plus another 2 drives of 20 TB for backup in case one of them fails, plus 2 NVMEs of 1 TB each just for cache, plus a 10 Gbps network card. On top of that I keep several online copies of part of this content on Google Drive or Dropbox, and a full backup on Amazon Glacier, for the rare occasion my house catches fire, for example. I&rsquo;m always prepared for any catastrophe.</p>
<p>Synology is one of the best home NAS brands. Yes, you can go way more sophisticated too, just build a real server, with CPUs like AMD EPYC ThreadRipper, using systems like <a href="https://www.truenas.com/"target="_blank" rel="noopener">TrueNAS</a>. But that&rsquo;s too much for me, I don&rsquo;t feel comfortable maintaining it. Like I said, everyone has to know their own limits.</p>
<blockquote class="twitter-tweet" data-media-max-width="560"><p lang="pt" dir="ltr">1o HD de 20TB terminou de se integrar ao RAID! Levou 2 dias inteiros!! Colocando o 2o HD ... de 4 😅 <a href="https://t.co/Rv6dqBQr05">pic.twitter.com/Rv6dqBQr05</a></p>— Akitando.com (@AkitaOnRails) <a href="https://twitter.com/AkitaOnRails/status/1772995683915649435?ref_src=twsrc%5Etfw">March 27, 2024</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
<p><em>&ldquo;Why not just keep everything online??&rdquo;</em> For most people that really is the easiest path. Like I said, my case is unusual. You do NOT want to have more than 50 TERAbytes online. My local wired network is 10 Gbps, my fiber internet is a measly 0.5 Gbps (500 Mbps). Streaming high quality video over the internet is horribly slow. I want everything in real time, and for that it has to be a local wired network.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/hejrhguxmbprma7c02hsp4mpr2rq" alt="Synology Dashboard"  loading="lazy" /></p>
<p>That said, I built a second mini-computer, using an Intel NUC, connected to the same wired network as the NAS. It&rsquo;s a Core i7, 32GB of RAM, 2.5Gbps ethernet, with the single purpose of being an empty Ubuntu server (it could be any Linux, I just picked the easiest one) whose job is to run Docker containers.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/byblhfmjthild3vjq4kyif974vt2" alt="Intel Nuc Core i7"  loading="lazy" /></p>
<p>Even this little PC is overkill, way more than necessary. Many will comment that a Raspberry Pi 5 would be enough, and they&rsquo;re not wrong. The best solution is the one you can afford and feel comfortable maintaining. Don&rsquo;t try to copy anyone&rsquo;s setup exactly, study what fits you best. And don&rsquo;t waste time bikeshedding other people&rsquo;s setups, build your own.</p>
<p>Anyway, I decided to share with you all the docker-compose files I&rsquo;m using to bring up the docker containers on this server. Here&rsquo;s the <a href="https://github.com/akitaonrails/plex_home_server_docker"target="_blank" rel="noopener">GitHub repository</a>.</p>
<p><em>WARNING:</em> don&rsquo;t forget you need to edit the directories in the scripts. It&rsquo;s configured to access my NAS network mount point, which is <code>/mnt/terachad</code>. Fix it to point to the right places on your machine.</p>
<p><em>WARNING 2:</em> there are a lot of details I&rsquo;m not covering in this article. This <a href="https://www.reddit.com/r/pirataria/comments/18ch7bt/guia_do_streaming_dom%C3%A9stico_automatizado_sonarr/"target="_blank" rel="noopener">Reddit forum</a> has more details and more discussion. Depending on your specific doubts or problems, it may be answered there.</p>
<p>So you don&rsquo;t get lost in the middle of so many services, here&rsquo;s the full picture of what we&rsquo;re going to build, from the request all the way to watching on the TV, including the protection layers that keep malware and wrong downloads out of your library:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260905220142_home-server-arquitetura-en.png" alt="Full architecture of the Plex home server, from request to playback"  loading="lazy" /></p>
<p>The idea is this: you request in one place (Seerr), the system finds it (Prowlarr and the indexers), downloads it with protection (qBittorrent and SABnzbd, plus the security layers), organizes it on the NAS (with subtitles from Bazarr) and you watch it on any screen (Plex and Navidrome). Let me start explaining the main ones one by one.</p>
<h2>Portainer and Utilities<span class="hx:absolute hx:-mt-20" id="portainer-and-utilities"></span>
    <a href="#portainer-and-utilities" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/4g7nani6heneielnqirwt8xba9iq" alt="Portainer Dashboard"  loading="lazy" /></p>
<p>Here&rsquo;s the snippet (see the full thing in the <a href="https://github.com/akitaonrails/plex_home_server_docker/blob/master/kaizoku-docker-compose.yml"target="_blank" rel="noopener">repository</a>):</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">portainer_data</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">portainer</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">portainer/portainer-ce:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">portainer_data:/data</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/var/run/docker.sock:/var/run/docker.sock</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s1">&#39;9000:9000&#39;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p><a href="https://www.portainer.io/"target="_blank" rel="noopener">Portainer</a> is a visual manager for the resources managed by the Docker service on your machine. Its purpose is more for me to visually &ldquo;glance&rdquo; and check if any container has a problem, or to test some quick little config tweak before editing the docker compose file.</p>
<p>In the <a href="https://github.com/akitaonrails/plex_home_server_docker/blob/master/utils-docker-compose.yml"target="_blank" rel="noopener">utils-docker-compose.yml</a> file there are other services like Organizr that I installed but haven&rsquo;t stopped to configure yet, nor decided if I really want to keep. There&rsquo;s Librespeed, which works like those internet speed tests, but for the local network. There&rsquo;s <a href="https://github.com/containrrr/watchtower"target="_blank" rel="noopener">Watchtower</a>, which monitors image updates for all Docker containers, downloads them and automatically restarts with new versions, to keep all containers always up to date.</p>
<p>And to reach your local services from outside home, there are easy VPN options like <a href="https://tailscale.com/"target="_blank" rel="noopener">Tailscale</a> or <a href="https://www.zerotier.com/"target="_blank" rel="noopener">ZeroTier</a>, with little configuration. Yes, yes, before someone runs to the comments, you can set up OpenVPN from scratch, but trust me, these options are infinitely more &ldquo;plug and play&rdquo; and possibly safer, because everyone installs it, but few have the discipline to keep it maintained and updated all the time. These days I actually ended up moving to a Cloudflare tunnel to expose my services, but to get started, a Tailscale does the job really well.</p>
<h2>MakeMKV and HandBrake<span class="hx:absolute hx:-mt-20" id="makemkv-and-handbrake"></span>
    <a href="#makemkv-and-handbrake" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>To build your personal movie library, legitimately, the correct way is to own BluRay or UHD discs and &ldquo;rip&rdquo; them. If that&rsquo;s your case, I recommend buying disc reader models like the <a href="https://www.amazon.com/Pioneer-BDR-XS07S-Silver-Revision-Blu-ray/dp/B081R74KVW"target="_blank" rel="noopener">Pioneer BDR-XS07S</a>. You have to be VERY careful because not every reader works.</p>
<p>Check the <a href="https://forum.makemkv.com/forum/viewforum.php?f=16"target="_blank" rel="noopener">MakeMKV forum</a> for the most recommended models. Some models got firmware updates that stop them from being used to rip BluRay. You need to find models that either weren&rsquo;t updated, or that aren&rsquo;t hard to firmware-downgrade. The ideal is to buy models everyone already knows work, and for that you have to dig through forums like that one.</p>
<p><a href="https://www.makemkv.com/"target="_blank" rel="noopener">MakeMKV</a>, as the name says, &ldquo;Makes MKV video files&rdquo;, which is the good old Matroska video envelope format, one of the most versatile (because it supports multiple streams not just of audio, but of subtitles too).</p>
<p><em>Crash course:</em> one thing is the codec that does the encoding of the video streams and audio streams. For example, H.265 or AV1 are video codecs. MP3 or AAC are audio codecs. Now we need to package these two streams (or more, if there are multiple dubs, for example) into an &ldquo;envelope&rdquo;, a &ldquo;container&rdquo;, a &ldquo;file format&rdquo;, like &ldquo;.mp4&rdquo; or &ldquo;.mov&rdquo; files. Matroska is a file envelope format that&rsquo;s very flexible, allows multiple streams of everything, including subtitles, which is why it&rsquo;s so widely used for sharing over torrent.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/0swbtawzxepqgd4r88cfnbg97ne9" alt="MakeMKV"  loading="lazy" /></p>
<p>With this container, you can access the app&rsquo;s graphical interface through the browser (it&rsquo;s like a remote Windows client, like VNC). Just have the Blu-Ray reader connected via USB to your computer and it should find it. It will use the <a href="https://forum.makemkv.com/forum/viewtopic.php?t=18856"target="_blank" rel="noopener">libredrive</a> library to decrypt the Blu-Ray data streams, which is why the firmware versions being compatible (already cracked) matters.</p>
<p>Many people don&rsquo;t know that we call everything &ldquo;Blu-Ray&rdquo; but in reality Blu-Ray is for movies encoded in Full HD, which is 1080p. The 4K versions (2160p) come as UHD, which is <strong>Ultra HD</strong>. The Blu-Ray case is usually blue, the UHD ones are black, you want the UHD ones whenever possible, of course. And I have a huge collection:</p>
<blockquote class="twitter-tweet" data-media-max-width="560"><p lang="pt" dir="ltr">"onde eu tô gastando tanto espaço??"<br><br>Boa parte é fazendo Rip (backup) de todos os meus UHD (BluRay 4K). E isso porque estou re-encodando em bitrate menor depois de ripar kkkk <a href="https://t.co/T1yPa6EIcV">pic.twitter.com/T1yPa6EIcV</a></p>— Akitando.com (@AkitaOnRails) <a href="https://twitter.com/AkitaOnRails/status/1772698430063731135?ref_src=twsrc%5Etfw">March 26, 2024</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
<p>The advantage of an app like MakeMKV is being able to decrypt the video stream and copy it bit by bit exactly as it is on the UHD, the best quality possible. This is what&rsquo;s called REMUX (remuxing), which is extracting the stream from the container and putting it in another container without re-encoding. You&rsquo;ve probably seen it in some torrent files, it&rsquo;s the best quality. But it means it will use the same space as on the disc, a range of 50 to 80 GIGAbytes per movie. Every dozen movies will eat up almost 1 terabyte. Depending on how many movies you want to have, your free space will run out very fast.</p>
<p>To minimize this, we can recode or &ldquo;re-encode&rdquo;. It&rsquo;s like when we take a RAW photo and re-encode it to a lower quality format, like PNG or even JPEG. If you know where to look, you&rsquo;ll find the compression artifacts, but overall, it&rsquo;s worth it. In my case, for example, encoding to H.265 10-bit, QP 15 Constant Quality.</p>
<p>The best option is to have an NVIDIA graphics card, with hardware video encoding support via NVENC, and use the best video encoding software: <a href="https://handbrake.fr/"target="_blank" rel="noopener">Handbrake</a>. In my case, I pick the pre-configured <strong>Matroska H.265 MKV 4K60fps</strong> profile and switch it to use the NVENC encoder on my RTX 4090, which speeds up encoding several times over.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/vcjscocbej7d64mo6gnvu97rcoiz" alt="HandBrake"  loading="lazy" /></p>
<p>In docker compose, just like MakeMKV, I can bring up this app for browser access, like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="w">  </span><span class="nt">handbrake</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">jlesage/handbrake</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;5801:5800&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">AUTOMATED_CONVERSION=0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">HANDBRAKE_GUI=1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">DARK_MODE=1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">TZ=America/Sao_Paulo</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;/home/akitaonrails/handbrake:/config:rw&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;/home/akitaonrails:/storage:ro&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;/mnt/terachad/Videos/BluRay:/watch:rw&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;/mnt/terachad/Videos/BluRayOptimized:/output:rw&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">devices</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/dev/dri:/dev/dri</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Your PC has at least an Intel graphics card with hardware encoding support via QSV, or better, an NVIDIA one with NVENC support. In both cases, we give the container access to that by mapping the <code>/dev/dri</code> device. Without it, encoding goes through the CPU, and it takes orders of magnitude longer.</p>
<p>Keep in mind that software encoding is slower but has better quality than Intel QSV, which, despite being fast, always gives me inferior video quality. NVENC is the best of both worlds: excellent quality and very fast.</p>
<p><strong>WARNING:</strong> in my case, I want the best quality possible, that&rsquo;s why I back up UHDs, that&rsquo;s why even over torrent I download BR-DISC, which is a UHD backup, and that&rsquo;s absurdly heavy as I explained. So I manually redo the encoding to the Matroska H.265 profile, as I also explained.</p>
<p>Many people don&rsquo;t want this hassle, so be sure to read the <a href="https://www.reddit.com/r/pirataria/comments/18ch7bt/guia_do_streaming_dom%C3%A9stico_automatizado_sonarr/"target="_blank" rel="noopener">Reddit forum</a>, which explains how to configure the next services to not download huge files like this. I won&rsquo;t show it here, because it&rsquo;s not my case.</p>
<h2>QBitTorrent<span class="hx:absolute hx:-mt-20" id="qbittorrent"></span>
    <a href="#qbittorrent" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>No matter how much Blu-Ray and UHD you own or can buy, a lot of stuff doesn&rsquo;t even exist on physical disc. In that case, your only option is BitTorrent. And yes, piracy is <strong>illegal</strong>, I am <strong>not encouraging piracy</strong> blah blah blah&hellip;</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/ar374pd43442x45030482kqhzq17" alt="QBitTorrent"  loading="lazy" /></p>
<p>Every country has its own law, go research it. I&rsquo;ll assume everyone knows what they&rsquo;re doing and it&rsquo;s not my problem. No point commenting about this, I won&rsquo;t go on about it. The web is already stuffed with material about it, go to Google and search yourself.</p>
<p>The first thing we want is to install <a href="https://www.qbittorrent.org/"target="_blank" rel="noopener">QBitTorrent</a>, probably the best and most complete app to download torrents, which also has a web interface and works via Docker:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="w">  </span><span class="nt">qbittorrent</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">linuxserver/qbittorrent:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">TZ=America/Sao_Paulo</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">WEBUI_PORT=8080</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">PUID=1000</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">PGID=1000</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/home/akitaonrails/qbittorrent/config:/config</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/mnt/terachad/Downloads/torrents:/downloads</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s1">&#39;8080:8080&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s1">&#39;62609:62609&#39;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>It&rsquo;s important to remember to generate a random port in the QBitTorrent settings, as shown in the image below. Any port between 1025 and 65535 should work, but your network might block some port range in that interval, so you have to test. Search web forums, and be careful because a lot of online tips are already outdated.</p>
<p>You need to understand the minimum about networking, understand whether you&rsquo;re behind a NAT, how to map external ports, whether you have an active firewall blocking ports, etc.</p>
<p>If the torrent isn&rsquo;t downloading, &ldquo;it might be&rdquo; this. I put it in quotes because it&rsquo;s only a possibility. So click &ldquo;random&rdquo; to pick another port, edit the docker compose config to map the new port and restart the container.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/vb3jnft1drvqsjjreo2bycp1y0e5" alt="QBitTorrent Port Listen"  loading="lazy" /></p>
<p>For some reason the saving management comes all set to Manual, it&rsquo;s important to change it to automatic:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/jqk5vdpxthahdwi2q5y1edgyxxa6" alt="QBitTorrent Saving Management"  loading="lazy" /></p>
<p>Another detail is to tell QBitTorrent to re-announce downloads to the trackers when the port changes. For that, enable the &ldquo;Reannounce to all trackers when IP or port changed&rdquo; option, otherwise downloads might not restart.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/yoo525n36cc08u6mijoxzwpgg79e" alt="QBitTorrent Reannounce"  loading="lazy" /></p>
<p>Now the most important setting in this whole section, and one I learned the hard way: <strong>block executables at download time</strong>. One fine day I found nine &ldquo;completed&rdquo; torrents in my library whose only content was a Windows executable of almost 1 GB, no video at all. And worse, the release name was squeaky clean, stuff like <code>Reacher S04E07 1080p WEB H264-CAKES</code> or <code>Rick and Morty S09E10 ... EDITH.scr</code>. You look at the name and swear it&rsquo;s the episode. The <code>.exe</code> or <code>.scr</code> was hidden in the file inside.</p>
<p>A name filter doesn&rsquo;t catch this, because the name is clean. The defense that actually works is blocking the extension at write time. In QBitTorrent, go to Options, Downloads, and enable &ldquo;Excluded file names&rdquo; with this list:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>*.exe
*.scr
*.bat
*.cmd
*.msi
*.lnk
*.com
*.vbs
*.pif</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>With that, the malicious file simply never gets written to disk. The torrent &ldquo;completes&rdquo; with nothing useful, Sonarr or Radarr notice the import failed, dump that release into the blocklist and go looking for another one on their own. The malware never touches the disk, and you don&rsquo;t even pass the garbage forward by seeding it. This is the first layer of defense, and the most effective.</p>
<p>Finally, how do I search for things to download? I can manually go to Google or DuckDuckGo or straight to sites like the PirateBays of the world and look for &ldquo;.torrent&rdquo; files or magnet links. To understand what that is, watch my video about <a href="https://www.youtube.com/watch?v=iAA8NrfQtHo"target="_blank" rel="noopener">Cryptography in Practice</a>, where I explain everything about Torrent too.</p>
<p>QBitTorrent supports search plugins. It comes with a few enabled. In the bottom right corner, there&rsquo;s the &ldquo;Search plugins&rdquo; button. We want to add the plugin that covers everything: the <a href="https://github.com/qbittorrent/search-plugins/wiki/How-to-configure-Jackett-plugin"target="_blank" rel="noopener">Jackett</a> service.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/dvndok2h1lgavk0igv24dazi2pwp" alt="Jackett plugin"  loading="lazy" /></p>
<p>Notice it wants a host named &ldquo;jackett&rdquo; enabled on port 9117, and for that we have this snippet in the docker compose:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="w">  </span><span class="nt">jackett</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">ghcr.io/linuxserver/jackett:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">TZ=America/Sao_Paulo</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">PUID=1000</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">PGID=1000</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/home/akitaonrails/jackett/downloads:/downloads</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/home/akitaonrails/jackett/config:/config</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s1">&#39;9117:9117&#39;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>It brings up a service where we can access the web interface like this:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/p2f8xj5cemh9g48f8sayg52os2ts" alt="Jackett Web UI"  loading="lazy" /></p>
<p>It&rsquo;s a service that consolidates dozens of torrent search sites in one place, can test whether they&rsquo;re online and, more importantly, provides an API key in the top right corner, which we should copy and edit into the QBitTorrent <a href="https://github.com/qbittorrent/search-plugins/wiki/How-to-configure-Jackett-plugin"target="_blank" rel="noopener">plugin config</a>. With that you can search directly, getting results like this:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/q8tbrlvwuoj2ilehs05kbfj40cs4" alt="QBitTorrent Search Engine"  loading="lazy" /></p>
<p>Notice in the right column that lists where these results were pulled from: it&rsquo;s coming from the Jackett service.</p>
<p>An important heads-up, because this changed over time: Jackett here is for THAT, the manual searches you do yourself in QBitTorrent. Don&rsquo;t fall for the temptation of wiring Jackett straight into Sonarr and Radarr. The automated part is going to be managed by Prowlarr, which I explain later, and it&rsquo;s Prowlarr that feeds the services&rsquo; indexers. I keep Jackett running only for manual searches and, at most, as a bridge for some tracker Prowlarr doesn&rsquo;t support natively, like RuTracker, which Prowlarr can consume through Jackett.</p>
<h2>Radarr (Movies)<span class="hx:absolute hx:-mt-20" id="radarr-movies"></span>
    <a href="#radarr-movies" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Searching manually works, but there are better ways: let a service do the searching and manage the download for you, and to download movies, we can use the <a href="https://radarr.video/"target="_blank" rel="noopener">Radarr</a> service:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/d9fz9nc4dc8mgcaowpo5xul6w71e" alt="Radarr Web UI"  loading="lazy" /></p>
<p>For that, we have this snippet in the docker compose:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="w">  </span><span class="nt">radarr</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">ghcr.io/linuxserver/radarr:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">depends_on</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">qbittorrent</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">TZ=America/Sao_Paulo</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">PUID=1000</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">PGID=1000</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/home/akitaonrails/radarr/appdata/config:/config</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/mnt/terachad/Downloads/torrents:/downloads</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/mnt/terachad/Videos/radarr/movies:/movies</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/mnt/terachad/Videos/radarr/anime:/anime</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s1">&#39;7878:7878&#39;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Notice I mapped different directories for western movies and anime movies, that&rsquo;s a preference of mine, you can organize it however you want. As I warned before, don&rsquo;t copy and paste exactly what I&rsquo;m showing here: adjust the directories for your machine.</p>
<p>The main thing is that once it&rsquo;s started, we can navigate the web interface to the &ldquo;Settings&rdquo; option. There you&rsquo;ll find &ldquo;Download Clients&rdquo; and we can register the QBitTorrent web service:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/o3bkso3dmxppqk7qkbxc5miwt3ul" alt="Radarr QBitTorrent"  loading="lazy" /></p>
<p>I didn&rsquo;t show it, but in the QBitTorrent settings there&rsquo;s a place to set the admin password to access its API via web. Don&rsquo;t forget to set it there and use the same password here. That way Radarr can control the movie downloads directly with QBitTorrent, without you having to get involved.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/hx4xnogjlt7i4hjkpkrniqgboaui" alt="QBitTorrent Web UI"  loading="lazy" /></p>
<p>There&rsquo;s a setting in Radarr I recommend doing right away, because it ships with a silly hole: by default, EVERY quality definition comes with a minimum size of zero. That lets a tiny fake file passing itself off as 4K slip through. I&rsquo;ve been a victim: a <code>Superman (2025) WEBDL-2160p.iso</code> of 598 MB came in as if it were the movie and stayed showing in Plex for months. Inside the ISO was a <code>.exe</code> of 597 MB in disguise.</p>
<p>The fix is to go to Settings, Quality, and set a size floor on each quality definition. I use roughly this, in MB per minute of video:</p>
<ul>
<li>720p (HDTV/WEBDL/WEBRip/Bluray): 3</li>
<li>1080p (HDTV/WEBDL/WEBRip): 5, Bluray 1080p: 8, Remux 1080p: 25</li>
<li>2160p (HDTV/WEBDL/WEBRip): 10, Bluray 2160p: 15, Remux 2160p: 50</li>
</ul>
<p>It&rsquo;s low enough to accept a good lean x265 encode, and high enough to refuse a &ldquo;4K&rdquo; under 1 GB, which can only be a trap. One annoying detail: Radarr has a bug where saving everything at once doesn&rsquo;t persist the minimum size, so you have to save quality by quality, one at a time.</p>
<h2>Sonarr (TV Shows)<span class="hx:absolute hx:-mt-20" id="sonarr-tv-shows"></span>
    <a href="#sonarr-tv-shows" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Same thing as Radarr, but for TV shows, we have the <a href="https://sonarr.tv/"target="_blank" rel="noopener">Sonarr</a> service:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/ky9rxjko4m5mfordc84fn6so7xlx" alt="Sonarr Web UI"  loading="lazy" /></p>
<p>This service is fantastic. It finds your shows, cross-referencing online databases with details like seasons, each episode&rsquo;s name and lots of other metadata. So it can not only download old episodes, but also keeps a schedule to download shows that are still airing. Every week a new episode shows up on its own:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/xf3cj62rrzl30q34qnf1f629fmm3" alt="Sonarr Show"  loading="lazy" /></p>
<p>And same as Radarr: in Settings and Download Clients, I can register my QBitTorrent, then Sonarr handles downloading the episodes:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/qxxuy0wa90se9x8alm9pi7ycmzt0" alt="Sonarr QBitTorrent"  loading="lazy" /></p>
<p>And to bring up the service, in the docker compose we have this snippet:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="w">  </span><span class="nt">sonarr</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">ghcr.io/linuxserver/sonarr:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">depends_on</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">qbittorrent</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">TZ=America/Sao_Paulo</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">PUID=1000</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">PGID=1000</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/home/akitaonrails/sonarr/appdata/config:/config</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/mnt/terachad/Videos/sonarr:/tv</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/mnt/terachad/Downloads/torrents:/downloads</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s1">&#39;8989:8989&#39;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Same as Radarr, I mapped different folders for anime and non-anime, which is a matter of your organization preference.</p>
<p>Both in Sonarr and Radarr, there&rsquo;s a setting that became mandatory for me after I started getting caught by traps: a Release Profile that blocks release names known to be booby-trapped. In Settings, Profiles, I keep a &ldquo;Must Not Contain&rdquo; list with terms like these:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>BROADCAST
FULL HD
.scr
.exe
SODAPOP
REACHERSON</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Each of these has a story. <code>BROADCAST</code>, <code>SODAPOP</code> and <code>REACHERSON</code> are groups that pack split RARs with a <code>Sample/</code> folder that jams the import (I caught almost 60 GB of junk from a fake <code>Welcome to Derry</code> in one go). <code>FULL HD</code> is the naming signature of the ones carrying <code>.exe</code>. And <code>.scr</code>/<code>.exe</code> in the name is literal. A heads-up for anyone copying this: Radarr, under the hood, uses the same <code>ignored</code> field as Sonarr, even though the interface calls it &ldquo;Must Not Contain&rdquo;, so configure the same list in both.</p>
<p>There&rsquo;s still a class no filter catches on its own: pre-air fakes. A <code>Reacher S04E07</code> shows up with zero seeds for an episode that only airs next week. Sonarr doesn&rsquo;t reject a release dated before the air date, so I clean those out by hand. The good news is that, with the QBitTorrent executable block from above, even when one of those downloads, it comes in empty and gets discarded automatically.</p>
<h2>Downloads that get stuck<span class="hx:absolute hx:-mt-20" id="downloads-that-get-stuck"></span>
    <a href="#downloads-that-get-stuck" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>A classic everyday problem: the torrent gets stuck forever in &ldquo;downloading metadata&rdquo; or &ldquo;stalled&rdquo;, with zero peers, and Sonarr never gives up on it on its own. I&rsquo;ve had an episode sitting behind a dead magnet for five days while the good release passed by, waiting for a turn that never came.</p>
<p>The solution I set up is a simple script running once a day (a cron or a systemd timer, doesn&rsquo;t matter). It queries the Sonarr, Radarr and Lidarr queue via the API, finds the items stuck in &ldquo;downloading metadata&rdquo; or &ldquo;stalled with no connections&rdquo; for more than 72 hours, and removes them with a single command:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>DELETE /api/v3/queue/{id}?removeFromClient=true&amp;blocklist=true</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>That deletes the dead download, dumps the release into the blocklist so it won&rsquo;t grab it again, and the app goes after another source automatically. The trick is being careful to NOT touch a download that&rsquo;s merely slow (under 72h) nor an item waiting for a manual import, otherwise you delete good stuff by mistake. The first time I ran it, it cleared out nineteen zombie downloads in one shot.</p>
<h2>Prowlarr (indexer)<span class="hx:absolute hx:-mt-20" id="prowlarr-indexer"></span>
    <a href="#prowlarr-indexer" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>For both Radarr and Sonarr to know where to pull the show and movie torrents from, they need to search public indexers or trackers (or private ones, if you have good contacts, I don&rsquo;t, don&rsquo;t even ask). For that we have <a href="https://prowlarr.com/"target="_blank" rel="noopener">Prowlarr</a>. This is the most important service of all, because if this one doesn&rsquo;t work, nothing downloads properly.</p>
<p>It took me a while to reach a good configuration, but today I have a simple rule that solves it: Prowlarr is the only place that manages indexers. Sonarr, Radarr and Lidarr should NOT have any indexer registered directly, only the ones Prowlarr syncs. Every indexer I had registered by hand inside Radarr was a source of headaches, including a &ldquo;Lime Torrents&rdquo; that was exactly the one that served me that Superman ISO malware. I deleted them all and left only Prowlarr feeding the three.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/a1xmx4qhm0i554d0vwsij051ivnz" alt="Prowlarr"  loading="lazy" /></p>
<p>Notice it lists dozens of indexers, or trackers. It&rsquo;s similar to Jackett. The difference is that Jackett is for manual searches you&rsquo;ll do yourself, straight from QBitTorrent. Prowlarr&rsquo;s indexers, on the other hand, get configured straight into Sonarr and Radarr. Here&rsquo;s where the confusion starts, let&rsquo;s understand it:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/5fu7qbgxgr4qf5kzzjyma1zevr9u" alt="Sonarr Indexes"  loading="lazy" /></p>
<p>Both in Sonarr and Radarr, besides Download Clients, where we can configure QBitTorrent, there&rsquo;s also this &ldquo;Indexers&rdquo; option, where we can configure which trackers to search for the media.</p>
<p>Think of a tracker like a public mini-Google, it&rsquo;s where people register their torrents when they want to share. Torrents aren&rsquo;t magic and don&rsquo;t appear out of nowhere. Someone has to create a record somewhere. But there isn&rsquo;t just &ldquo;one&rdquo; place: it&rsquo;s a distributed network and any participant can put up a tracker if they want. It&rsquo;s as if you could put up your own miniature Google, just to register torrent files.</p>
<p>It&rsquo;s a pain to keep managing this. Some small trackers decide to go offline, new ones appear, configs get changed, and we have to keep managing this manually, both in Sonarr and Radarr. That&rsquo;s what Prowlarr is for: to manage the trackers for you. Let&rsquo;s understand how.</p>
<p>In the Prowlarr settings, we start by configuring where to find our Sonarr and Radarr services:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/t5opcjgeutzt3l4omy6wkk4gbw98" alt="Prowlarr Apps"  loading="lazy" /></p>
<p>For example, this is my Sonarr config. Notice the hostname is &ldquo;sonarr&rdquo; and that works because in my docker compose I bring all services up on the same VLAN, and by default docker registers the service name configured in the YAML file as the hostname. If you don&rsquo;t understand Docker, be sure to watch my videos about <a href="https://www.youtube.com/watch?v=85k8se4Zo70"target="_blank" rel="noopener">containers</a>.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/d95y3u96q4tw9q9hs5u5q3eq26zf" alt="Prowlarr Sonarr"  loading="lazy" /></p>
<p>We also need to configure access to QBitTorrent:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/179tso31cnwvyxan9ltvgibeu8sm" alt="Prowlarr QBitTorrent"  loading="lazy" /></p>
<p>This other one, <strong>SABnzbd</strong>, uses another, older download protocol, based on USENET newsreaders. Yes, that old Usenet. Those who know, know. I keep torrent as the primary, but I keep SABnzbd configured as a second source, because a lot of content only exists in those older groups. If you&rsquo;re going to use it, apply the same extension protection as QBitTorrent: in the <code>sabnzbd.ini</code> file, in the <code>[misc]</code> section, set <code>unwanted_extensions = exe, scr, bat, cmd, msi, lnk, com, vbs, pif</code> and <code>action_on_unwanted_extensions = 2</code>, which tells it to abort the whole download if one of those shows up. That way Sonarr and Radarr see the failure and look for another source.</p>
<p>Finally, many trackers today implement some kind of bot protection using Cloudflare captcha, you know that thing that goes &ldquo;are you a human?&rdquo;. To get past that, we need the FlareSolverr service, which we can bring up in the docker compose like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="w">  </span><span class="nt">flaresolverr</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">ghcr.io/flaresolverr/flaresolverr:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/home/akitaonrails/flaresolverr/config:/config</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s1">&#39;8191:8191&#39;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>In Prowlarr it enters as an &ldquo;indexer proxy&rdquo;. You register FlareSolverr pointing to <code>http://flaresolverr:8191/</code>, create a tag for it, and mark only the indexers that sit behind Cloudflare with that tag. Those then start solving the captcha under the hood, and the others don&rsquo;t even need to go through it:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/7949bp73gbb49994tynwuil9249h" alt="Prowlarr FlareSolvrr"  loading="lazy" /></p>
<p>We&rsquo;re not done yet. On the main screen, you go enabling the indexers that make sense for you. Don&rsquo;t enable everything: it gets heavy and, worse, some indexers serve more traps than good content. Check each one&rsquo;s tags. Some indexers are porn-only, some are only Russian or Chinese content, those I skip.</p>
<p>The ones that serve me well today are <strong>EZTV</strong> and <strong>1337x</strong> (both behind Cloudflare, so they depend on FlareSolverr), <strong>YTS</strong> for movies, and <strong>AnimeTosho</strong> plus <strong>Nyaa</strong> for anime. In total I&rsquo;m left with somewhere between 18 and 21 active indexers per app. And more important than enabling the good ones is disabling the bad ones: I turned off <strong>LimeTorrents</strong> and <strong>TorrentDownload</strong>, because, together, they generated more than two hundred downloads and almost no usable import, on top of being a recurring source of malware and fakes. Fewer good indexers is worth more than a pile of bad ones.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/cnl4yefcsvas84m6eslb3in7jmq9" alt="Add Indexer"  loading="lazy" /></p>
<p>At the end, it&rsquo;s good to click &ldquo;Test All Indexers&rdquo; to check which ones are still active or not. And finally, we can click &ldquo;Sync App Indexers&rdquo;, which will register all the indexers we know are working into Sonarr and Radarr. Get it? Prowlarr will configure both with the most up-to-date indexers, and remove the ones that are no longer active. We don&rsquo;t need to configure each of them manually.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/if86fzdhieaqt5fxpbozfrh0x13k" alt="Sync App"  loading="lazy" /></p>
<p>It&rsquo;s important to understand this, because otherwise you&rsquo;ll see Sonarr and Radarr not managing to download anything and won&rsquo;t know why: but it&rsquo;s because the indexers are wrong or obsolete or insufficient. Spend a good amount of time reading the <a href="https://prowlarr.com/"target="_blank" rel="noopener">Prowlarr</a> documentation.</p>
<h2>Plex (Private Netflix)<span class="hx:absolute hx:-mt-20" id="plex-private-netflix"></span>
    <a href="#plex-private-netflix" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>To watch everything you download, there are several options. If it&rsquo;s just for you, you can just download everything to an external drive or something, and watch directly from your PC or laptop. Depending on your Smart TV or game console, you can also connect your drive or flash drive via USB and watch directly.</p>
<p>If you want something a bit more sophisticated, and have a spare PC or Raspberry Pi, you can install Sonarr, Radarr, and share the downloads directory over the network, using protocols like CIFS, which is for Windows shared folders. Again, many Smart TVs and consoles support mounting network shared folders.</p>
<p>But if you want something friendlier and more sophisticated, you can literally have your own &ldquo;Private Netflix&rdquo;, with a graphical interface similar to any streaming platform, showing thumbnails, descriptions, movie or show details, a video player and all that, and even suggestions of what to watch, movie suggestions based on what you&rsquo;re watching, actor info and everything else.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/uvfh2i3ppr5mgrt1xl1r6kjgtvdb" alt="Plex"  loading="lazy" /></p>
<p>There are several projects that do this. One of them, which the community likes more for being 100% open, is <a href="https://jellyfin.org/"target="_blank" rel="noopener">Jellyfin</a> (derived from the old Emby). I recommend testing it, it has everything most people need. Just map the Sonarr and Radarr download folders and it will index everything and organize it for you. Then, from any PC, just access the web interface and it all just works.</p>
<p>But I personally prefer <a href="https://www.plex.tv/"target="_blank" rel="noopener">Plex</a>, which is older, more closed (which is why some people don&rsquo;t like it), but because it offers <a href="https://support.plex.tv/articles/202526943-plex-free-vs-paid/"target="_blank" rel="noopener">paid plans</a> with exclusive features, the level of polish is clearly better. The interface is much prettier, and looks more &ldquo;professional&rdquo;. Jellyfin, as good as it is, still has that look like a backend programmer made it, with a lot of ill will toward UX and design.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/gh6kum745hoedx34km4rggpfubjd" alt="Plex details"  loading="lazy" /></p>
<p>On top of that, to watch on a Smart TV or Android TV, you need an app on the Google Play Store. Jellyfin doesn&rsquo;t have an app for everything, some TVs, I don&rsquo;t remember if LG or Samsung, don&rsquo;t even have the app available. So you have to side-load. Not friendly. Plex, on the other hand, is available in way more places, so it tends to be more &ldquo;plug and play&rdquo;, similar to downloading the Netflix or Amazon Prime app.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/zvipl6wce0rruosctykjnou6ayu0" alt="Plex Skip Intro"  loading="lazy" /></p>
<p>Plex also has features that &ldquo;just work&rdquo;, trivial things you expect in a player, like a &ldquo;skip intro&rdquo; button or &ldquo;resume from where you stopped&rdquo; or &ldquo;skip to the next episode&rdquo; or &ldquo;search and change subtitle&rdquo;. That&rsquo;s what interfaces like these offer you, so the usability really is a lot like watching Netflix. You don&rsquo;t get that with a generic PC player like a VLC.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/mev5extpv8lgv7k1lhsq0bcm8i46" alt="Plex Subtitles"  loading="lazy" /></p>
<p>Bringing it up is easy, here&rsquo;s my docker compose snippet:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="w">  </span><span class="nt">plex</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">plexinc/pms-docker:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">TZ=America/Sao_Paulo</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">PUID=1000</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">PGID=1000</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">VA_DRIVER=IHD</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">PLEX_HW_TRANS_MAX=16</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/home/akitaonrails/plex/config:/config</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/home/akitaonrails/plex/data:/data</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/home/akitaonrails/plex/transcode:/transcode</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/mnt/terachad/Videos:/media</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">devices</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/dev/dri:/dev/dri</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/dev/bus/usb:/dev/bus/usb</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">network_mode</span><span class="p">:</span><span class="w"> </span><span class="l">host</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>There&rsquo;s a small detail many people don&rsquo;t understand: Plex does <em>transcoding</em> of the videos. That is, it will re-encode the video before sending it to the app or browser that will play it, and that&rsquo;s heavy, it&rsquo;s video stream conversion. If your server isn&rsquo;t strong enough, your video will stutter when playing.</p>
<p>In reality, it&rsquo;s more complicated than that. There&rsquo;s a protocol called &ldquo;Direct Play&rdquo; or &ldquo;Direct Stream&rdquo;. There&rsquo;s an <a href="https://support.plex.tv/articles/200250387-streaming-media-direct-play-and-direct-stream/"target="_blank" rel="noopener">article that explains this</a> on the Plex site. If the PC, SmartTV, console, etc. support the codec of the video in question, in theory Plex will just pass the bits straight through, without trying to convert anything. Understand: not every device can play any video codec.</p>
<p>Most content nowadays is encoded in H.264 or H.265 or even AV1, which is newer and more efficient. The problem, let&rsquo;s say you want to play it on a really old PC or a PS4. They&rsquo;ll support H.264, maybe H.265 if you&rsquo;re lucky, but certainly not AV1. So it would be super slow to try to play using only the CPU. The right thing is to have hardware instructions to play AV1, especially on an old CPU.</p>
<p>So Plex asks the device: <em>&ldquo;do you support AV1?&rdquo;</em> The player will say <em>&ldquo;nope, I&rsquo;m weak.&rdquo;</em> So Plex will transcode from AV1 to H.264, for example. All of this is transparent to you.</p>
<p>But then you&rsquo;ll notice that when playing the video it stutters, pauses all the time. It could be a bandwidth problem on your network, or it could be a problem of your Plex server being too weak, or of it not being configured for hardware transcoding, and brute-forcing it using only the CPU.</p>
<p>Watch <a href="https://www.youtube.com/watch?v=j4a1SgUWh1c"target="_blank" rel="noopener">my video about compression</a>, where I explain codecs, but in short, the right thing is that whatever little PC you&rsquo;re using for your Plex, it should have at least a minimum of hardware video encoding capability. Most have at least H.264 capability, like the <strong>Intel QSV</strong> instructions, check if your CPU has that support. It only won&rsquo;t have it if it&rsquo;s too old, like something more than 10 years back. If it&rsquo;s a strong server, with an NVidia GPU, it&rsquo;ll have things like <strong>NVENC</strong>, which supports H.265. If it&rsquo;s a modern Intel CPU, it&rsquo;ll support <strong>AV1</strong>.</p>
<p>In any case, on Linux, these instructions should be exposed on the <code>/dev/dri</code> device, which is why we need to map it into the docker container as I did above. Check that this device exists.</p>
<p>If everything is correct, in Plex you&rsquo;ll have this option to configure the &ldquo;Transcoder&rdquo;:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/q6y2swsu9u0uhzggskx3bimnxcou" alt="Plex Transcoder"  loading="lazy" /></p>
<p>We want to make sure the quality is set to &ldquo;Make my CPU hurt&rdquo; and that the &ldquo;Use hardware acceleration when available&rdquo; option is enabled too, so it will use <code>/dev/dri</code> to accelerate transcoding. And that makes a BRUTAL difference in speed. If you&rsquo;re having stuttering playback, it&rsquo;s very likely the transcoding.</p>
<p>When the device, your smartphone or your TV, supports the exact codec used in the video file we want to watch, in theory, Plex can pass the stream straight through, without transcoding, which is the ideal world, because then there&rsquo;s no processing at all. That&rsquo;s Direct Play. But for that the video files need to be in a certain codec, in a certain configuration. But even in a supported codec, if your TV is weak and doesn&rsquo;t support 4K, and asks Plex for Full HD (1080p), then Plex will transcode from H.264 4K to H.264 1080p, for example. That&rsquo;s why I say it&rsquo;s more complicated than it seems.</p>
<p><strong>IMPORTANT DETAIL:</strong> Recently I got confused playing some of my videos. I have the best hardware, both for the Plex server, with hardware acceleration, and an Android TV to play it which is my NVIDIA Shield Pro, on a wired network, etc. But some videos kept stuttering. And in the end the culprit wasn&rsquo;t hardware, or config, or any of that, it was the damn subtitle:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/z8wlwg5nze02ubi9m8dbkxlqqnin" alt="Plex PGS Subtitle problem"  loading="lazy" /></p>
<p>Anyone who&rsquo;s had to download subtitles has probably seen the most common format, which is <strong>.SRT</strong> (SubRip). Some who ventured further have seen other formats, like SSA (SubStation Alpha), TTML (Timed Text Markup Language), VTT (Web Video Text Track), VOBSUB, etc.</p>
<p>Yes, there&rsquo;s a LOT more than SRT. In particular, if you do like me and rip UHDs and extract the subtitle tracks from the disc itself, apps like MakeMKV can download them in VOBSUB format (which is ok) or the cursed <strong>PGS</strong> (Presentation Graphic Stream Subtitle Format).</p>
<p>Most subtitle tracks are made to be sent to the player in parallel, and separate, from the video track. Just like the audio track goes separately. The player receives a video track, an audio track, and plays the two in sync. Same with the subtitle track, in the case of SRT or SSA or TTML files.</p>
<p>But PGS is different: it has graphical capabilities! I&rsquo;m guessing here, but if you&rsquo;ve seen colored, animated subtitles, that move or position themselves at absolute spots on screen (like sitting exactly on top of a billboard in the video, for example), then there has to be a step to rasterize and render that graphical subtitle <strong>ON TOP</strong> of the video stream. That&rsquo;s a heavier encoding step, meaning it&rsquo;ll be <strong>HEAVY</strong> to play in real time.</p>
<p>The solution: use Plex to search for a new subtitle, in SRT format, and ignore the PGS subtitle. Just doing that stopped the stuttering when playing.</p>
<h2>Seerr (formerly Overseerr)<span class="hx:absolute hx:-mt-20" id="seerr-formerly-overseerr"></span>
    <a href="#seerr-formerly-overseerr" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>With just Sonarr and Radarr, plus Prowlarr&rsquo;s indexers, it&rsquo;s already enough to search and download everything. But there&rsquo;s another app that makes discovering new stuff to download a lot easier. It&rsquo;s called <a href="https://seerr.dev/"target="_blank" rel="noopener">Seerr</a>, and it&rsquo;s the evolution of what used to be Overseerr. The Overseerr project merged with Jellyseerr, the fork that served Jellyfin, and the two became one: Seerr, which now serves Plex, Jellyfin and Emby in the same place. If you still hear people talk about &ldquo;Overseerr&rdquo; out there, this is it.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/scl2gpzjir3g0hga7p1ip50cs54w" alt="Seerr"  loading="lazy" /></p>
<p>It goes to free online databases (like the IMDBs of the world) and keeps an eye on everything new coming out, everything that&rsquo;s &ldquo;trending&rdquo;. If I want to find something new, it&rsquo;s a good one. Even things I already know I want, I just search in Seerr and click &ldquo;Request&rdquo;:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/4rcrqhxpooq14nip3zj3m7epo7p3" alt="Seerr Request"  loading="lazy" /></p>
<p>I configure the integration with Sonarr and Radarr in the settings, so it knows who to send the request to. It all works integrated. I just need to use the Seerr interface to request things and I can forget about Sonarr and Radarr running in the background.</p>
<h2>Bazarr (subtitles)<span class="hx:absolute hx:-mt-20" id="bazarr-subtitles"></span>
    <a href="#bazarr-subtitles" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>I don&rsquo;t need it, but my girlfriend sometimes prefers watching with Portuguese subtitles. And it&rsquo;s a pain because there isn&rsquo;t always one, or what exists is low quality. By the way, let me warn you that both official and unofficial subtitles, I&rsquo;m tired of seeing wrong translations. My fun is finding the errors in the subtitle. If you depend on subtitles, know that many important meanings are actually wrong. Always prefer the original.</p>
<p>Back in the day you had to go manually to sites like <a href="https://www.opensubtitles.org/en/search"target="_blank" rel="noopener">OpenSubtitles.org</a>, download the &ldquo;.SRT&rdquo; file, rename it the same as the video file name and keep testing manually to see if it was in sync or not.</p>
<p>There are two ways to solve this. One is through Plex itself, which has the option to search subtitles in various languages and keep testing them straight from the player interface. It helps a lot because it skips the step of having to sit at the PC, manually downloading files. As I explained in the previous section, in my case, since the UHD backup subtitles were extracted in PGS format, I need to search in SRT format so it doesn&rsquo;t stutter when playing.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/ok05g5tdrevvaywqfqmjwy0we3d6" alt="Plex Subtitles"  loading="lazy" /></p>
<p>Another way is to try downloading the subtitles automatically beforehand. This isn&rsquo;t perfect because there are several versions of subtitles, some made for DVD, others for BluRay, some even for the more popular &ldquo;CAM&rdquo; versions, which are torrents of videos recorded straight in the theater with a smartphone, the crappiest version of all.</p>
<p>It&rsquo;s very important to rename the video files in the right format, with the metadata well organized, as explained in the <a href="https://www.reddit.com/r/pirataria/comments/18ch7bt/guia_do_streaming_dom%C3%A9stico_automatizado_sonarr/"target="_blank" rel="noopener">Reddit forum</a> I mentioned at the start of the article. That helps Bazarr match the right subtitle to the right file. And a heads-up: Brazilian Portuguese is one of the languages with the fewest subtitles. It&rsquo;s easier to find them in French, Italian or Russian than in Portuguese.</p>
<p>To make this easier we have the <a href="https://www.bazarr.media/"target="_blank" rel="noopener">Bazarr</a> service:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/mjziydblv9rh4yowhuodgxycjzf6" alt="Bazarr Web UI"  loading="lazy" /></p>
<p>In the Bazarr config, we point it to our local Sonarr and Radarr services, so it finds out every time a new show episode or a new movie appears, and schedules a subtitle search:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/rhiefnojwsullwi1m9swhxds6ocj" alt="Bazarr integration"  loading="lazy" /></p>
<p>The main thing is to configure the Providers, which are the sites the subtitles come from. This is where the difference between &ldquo;finds nothing&rdquo; and &ldquo;finds almost everything&rdquo; lives. The five that give me the best results are: <strong>opensubtitlescom</strong>, <strong>tvsubtitles</strong>, <strong>yifysubtitles</strong> (movies only), <strong>animetosho</strong> (for anime, no account needed) and <strong>gestdown</strong> (an Addic7ed proxy for English, also no account). One gotcha that cost me hours: the OpenSubtitles.com provider wants your <strong>username, not your email</strong>. If you put the email, it errors out with &ldquo;you cannot consume this service&rdquo;, and still keeps you grounded for 12 hours even after fixing it.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/8lzq8umwyjgg40abb1tlvtvt8nxu" alt="Bazarr Providers"  loading="lazy" /></p>
<p>To bring up the service, in the docker compose, we have this snippet:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="w">  </span><span class="nt">bazarr</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">ghcr.io/linuxserver/bazarr:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">TZ=America/Sao_Paulo</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">PUID=1000</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">PGID=1000</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/mnt/terachad/Videos/radarr:/movies</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/mnt/terachad/Videos/sonarr:/tv</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/home/akitaonrails/bazarr/appdata/config:/config</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s1">&#39;6767:6767&#39;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Here we need to map the radarr and sonarr folders so it knows where to write the subtitles. One detail that trips a lot of people up: map the exact same subpaths that Radarr and Sonarr use (in my case, <code>/movies</code> and <code>/anime</code>), otherwise Bazarr keeps complaining that it &ldquo;can&rsquo;t find the video file for analysis&rdquo; across the whole library.</p>
<p>The last step, and the one that finally made Bazarr useful for me, is the language profile. I created a &ldquo;Brazilian Portuguese with English fallback&rdquo; profile: it lists <code>pb</code> first and <code>en</code> after, with the cutoff on <code>pb</code>. In practice, Bazarr searches for both, downloads whichever shows up first, and considers the item resolved once a Portuguese subtitle exists. If pt-BR never appears, English stays as the backup.</p>
<p>And the trick, which cost me dearly: <strong>every item needs a language profile assigned</strong>. Bazarr silently ignores any movie or show without a profile. When I went to check, there were 140 shows and 201 movies with no profile at all, silently ignored ever since they were added to the library. Marking the profile as default only covers what you add from then on; for the library that already exists, you have to assign the profile in bulk, all at once, to everything already there.</p>
<p>There&rsquo;s also a sibling problem to this one, but on the audio side. Every now and then Sonarr or Radarr download a release whose only audio is a foreign dub: an Italian one, a French one, a Russian one. And no subtitle saves wrong audio. For that I use a Custom Format in both, the &ldquo;foreign-language-only&rdquo; one, which recognizes foreign-dub markers (ITALIAN, FRENCH, RUS, GERMAN and so on) and gives a score of -500, which makes the release get refused at search time. A detail I learned the hard way: a Russian release came with the whole title in Cyrillic, without any &ldquo;RUS&rdquo; in Latin letters, and slipped right past. So the filter also needs to catch any Cyrillic character.</p>
<p>After I fixed the providers, moved the entire library onto that profile and turned on this audio filter, Bazarr stopped being the most useless service in my stack and became one that resolves most cases on its own. When something&rsquo;s still missing, then yes I hunt for it by hand through Plex, but today that&rsquo;s the exception. Just to reiterate what I said before: Brazilian Portuguese subtitles are among the scarcest out there, so even with everything set up right, sometimes there just isn&rsquo;t one.</p>
<h2>Lidarr (Music)<span class="hx:absolute hx:-mt-20" id="lidarr-music"></span>
    <a href="#lidarr-music" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>I&rsquo;m not much of a music person. I have a few MP3s I downloaded decades ago and, day to day, I play the Spotify Top 50 playlist and that&rsquo;s fine. But the same logic as Sonarr and Radarr applies to music, so I ended up setting it up too: the service is called <a href="https://lidarr.audio/"target="_blank" rel="noopener">Lidarr</a>.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260905215028_lidarr-library.png" alt="Lidarr Web UI"  loading="lazy" /></p>
<p>It catalogs your artists, cross-references discography databases and goes after the missing albums, marking each artist as &ldquo;Monitored&rdquo; to look for new releases on its own, the same way Sonarr does with show episodes. In the screenshot above you can see my library with everything in &ldquo;Lossless&rdquo;, which is the quality profile I configured.</p>
<p>In the docker compose it&rsquo;s the same pattern as the others:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="w">  </span><span class="nt">lidarr</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">ghcr.io/linuxserver/lidarr:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">depends_on</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">qbittorrent</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">TZ=America/Sao_Paulo</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">PUID=1000</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">PGID=1000</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/home/akitaonrails/lidarr/appdata/config:/config</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/mnt/terachad/Music:/music</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/mnt/terachad/Downloads/torrents:/downloads</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s1">&#39;8686:8686&#39;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>The configuration is identical in spirit to Radarr and Sonarr. In Settings and Download Clients I register the same QBitTorrent, but using its own category called <code>lidarr</code>, so it doesn&rsquo;t mix the music downloads with the movie and show ones. The library root folder points to <code>/music</code>, and the quality profile can be left open (the &ldquo;Any&rdquo;, which grabs whatever shows up) or locked to &ldquo;Lossless&rdquo; when you only want FLAC, which is my case.</p>
<p>And the best part: I don&rsquo;t configure the indexers by hand. Prowlarr, which I explained above, also registers Lidarr as an application and syncs the music-category indexers on its own. It&rsquo;s the same &ldquo;Sync App Indexers&rdquo; it does for Sonarr and Radarr, now for music too.</p>
<p>To listen, Plex itself already plays music: just create a Music-type library pointing to that folder, and on the phone the Plexamp app is great just for this. And, the same way I have Kavita as &ldquo;Plex for manga&rdquo;, for music you can also use <a href="https://www.navidrome.org/"target="_blank" rel="noopener">Navidrome</a>, an open source streaming server like a private Spotify, which indexes the same <code>/music</code> folder (read-only) and has several compatible phone apps. Lidarr downloads and organizes, Navidrome or Plex serve.</p>
<h2>Other Services<span class="hx:absolute hx:-mt-20" id="other-services"></span>
    <a href="#other-services" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>In <a href="https://github.com/akitaonrails/plex_home_server_docker/tree/master"target="_blank" rel="noopener">my repository</a> there are other Docker Compose files for my local infra. I won&rsquo;t explain them in detail, but just so you know:</p>
<p>To block banners, ads, trackers and even malware sites across the whole network, I use filtered DNS. For a long time I ran a local <a href="https://pi-hole.net/"target="_blank" rel="noopener">Pi-Hole</a> for that, which is a home DNS server with built-in blocking. It works great, but it needs maintenance. These days I switched to a hosted service, <a href="https://nextdns.io/"target="_blank" rel="noopener">NextDNS</a>, which does the same ad, tracker and malware blocking, except I don&rsquo;t have to maintain any server, and it comes with encrypted DNS out of the box.</p>
<p>And why does encrypted DNS matter? A DNS query, by default, is an open text protocol. Your provider knows exactly everything you browse. I prefer not to hand them that data for free. With NextDNS the queries go out encrypted and still pass through the blocklist I choose. It doesn&rsquo;t replace an anti-virus, but it makes it much harder for third parties to build a fingerprint of my browsing habits, and it&rsquo;s a lot less work than the Pi-Hole I used to run.</p>
<p>Now, in the <a href="https://github.com/akitaonrails/plex_home_server_docker/blob/master/kaizoku-docker-compose.yml"target="_blank" rel="noopener">kaizoku-docker-compose.yml</a> file there&rsquo;s another service like Sonarr for TV, Radarr for movies, which is <a href="https://github.com/oae/kaizoku"target="_blank" rel="noopener">Kaizoku</a> for manga. If you read manga online, you&rsquo;ve probably seen many sites getting shut down over copyright, intellectual property and all that. Eventually, the online manga you read will disappear. With Kaizoku we can preserve them locally too.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/iucprcsmn9emkzzurs7z2fstcsjm" alt="Kaizoku"  loading="lazy" /></p>
<p>Kaizoku is only for downloading. To read, we need a reader, the same way we have Plex for video, we have <a href="https://www.kavitareader.com/"target="_blank" rel="noopener">Kavita Reader</a> to read the downloaded manga. Just like Plex, on the first setup you just point it to the folder where Kaizoku downloads everything, then Kavita handles organizing the library, downloading covers and other metadata. It&rsquo;s literally Plex for Manga.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/bxuz95wo3luc308lz1dos9bf2kcr" alt="Kavita"  loading="lazy" /></p>
<p>I haven&rsquo;t explored this service much yet. Either there are still things to refine in it, or I just don&rsquo;t know how to configure it right, but not everything downloads covers correctly. See how it doesn&rsquo;t look nice. If you have tips about Kavita and Kaizoku, be sure to share them below.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/bcrh2ifucs86fupzk8gqa38yx5iq" alt="Kavita Reader"  loading="lazy" /></p>
<p>For the actual reading, it&rsquo;s a lot like a Kindle Web or other online manga sites. You can read page by page, or &ldquo;scroll&rdquo; the whole chapter, it remembers where you stopped and basic stuff like that. It could be a good project to contribute to, if you feel like practicing with open source.</p>
<h2>Conclusion<span class="hx:absolute hx:-mt-20" id="conclusion"></span>
    <a href="#conclusion" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>I&rsquo;m very happy with the current state of file-sharing services. Don&rsquo;t waste too much time overthinking. Just install them and have fun. The code for everything is open, if you&rsquo;re a programmer, go dig through the code and learn something new, and who knows, even contribute.</p>
<p>I&rsquo;m not into apocalyptic theories, conspiracy theories, or anything like that. I just consider that whatever I don&rsquo;t have physical control over, I <strong>don&rsquo;t have</strong>. If what &ldquo;is mine&rdquo; is under a third party&rsquo;s control, then &ldquo;it isn&rsquo;t mine&rdquo;. That&rsquo;s the principle. I want everything that&rsquo;s mine under my sole and exclusive control, period. It doesn&rsquo;t matter if it&rsquo;s less convenient, it doesn&rsquo;t matter if it&rsquo;s more expensive. It&rsquo;s a matter of principle: mine is mine and that&rsquo;s final.</p>
<p>Besides, on the streaming platforms, not everything exists. A lot of stuff from past decades was simply erased from memory. Many don&rsquo;t even remember anymore, especially those who didn&rsquo;t see it back then. I feel nostalgia for TV shows, movies, that don&rsquo;t exist online. Many don&rsquo;t even exist on physical media. There are movies that after VHS never even came out on DVD. There are DVDs that never came out as Blu-Ray. There&rsquo;s stuff on physical disc that will never come to streaming. There&rsquo;s stuff on streaming that will be erased soon, and many won&rsquo;t even notice.</p>
<p>What&rsquo;s mine, is mine. They can erase as much as they want. What&rsquo;s mine, no one erases. If you value what&rsquo;s yours, start exploring these options. It doesn&rsquo;t have to be a giant setup like mine. Any spare external drive is enough to start. Start small, grow little by little. I&rsquo;ve been collecting this stuff for decades! I didn&rsquo;t start today.</p>
]]></content:encoded><category>homelab</category><category>containers</category><category>audio-and-video</category></item><item><title>[SmallTips] Running GitLab CI Runner Locally</title><link>https://akitaonrails.github.io/en/2018/04/28/smalltips-running-gitlab-ci-runner-locally/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2018/04/28/smalltips-running-gitlab-ci-runner-locally/</guid><pubDate>Sat, 28 Apr 2018 18:35:00 -0300</pubDate><description>&lt;p&gt;If there is one thing you should always do, it&amp;rsquo;s maintain your test suite. Every bug you fix, every new feature, you should add new tests.&lt;/p&gt;
&lt;p&gt;I have been working very hard the past few weeks, quite literally coding non-stop, 7 days a week. Remember that &amp;ldquo;stop complaining you don&amp;rsquo;t have time, what do you do from midnight to 6 AM?&amp;rdquo; thing? Yeah, won&amp;rsquo;t apply to me.&lt;/p&gt;
&lt;p&gt;And it&amp;rsquo;s exactly when you&amp;rsquo;re sleep deprived and exhausted that you start to make mistakes. Even though your code runs, locally, you will shoot yourself in the foot in one of those 3 AM coding spree sessions.&lt;/p&gt;</description><content:encoded><![CDATA[<p>If there is one thing you should always do, it&rsquo;s maintain your test suite. Every bug you fix, every new feature, you should add new tests.</p>
<p>I have been working very hard the past few weeks, quite literally coding non-stop, 7 days a week. Remember that &ldquo;stop complaining you don&rsquo;t have time, what do you do from midnight to 6 AM?&rdquo; thing? Yeah, won&rsquo;t apply to me.</p>
<p>And it&rsquo;s exactly when you&rsquo;re sleep deprived and exhausted that you start to make mistakes. Even though your code runs, locally, you will shoot yourself in the foot in one of those 3 AM coding spree sessions.</p>
<p>The test suite was really my sidekick, co-pilot, bringing me back to my senses whenever the night got too dark.</p>
<p>I maintain my own GitLab custom server for all of my company&rsquo;s client and internal projects. Just a bit of paranoia, to make sure my code is owned by me alone. And GitLab is a fantastic tool. Even more so because it has <a href="https://about.gitlab.com/features/gitlab-ci-cd/"target="_blank" rel="noopener">its own Continuous Integration companion</a>.</p>
<p>Just add your <code>.gitlab-ci.yml</code> file to your project, create a new branch, push it and create a Merge Request and the CI kicks in, with multiple <a href="https://docs.gitlab.com/ee/ci/yaml/#jobs"target="_blank" rel="noopener">parallel jobs</a>  if you want. Even when I was forgetting to run my tests, the CI would not let me fail.</p>
<p>Keeping a CI up and running does require you to stop and maintain your specs though. Sometimes you will wonder, why does a test run on your local machine but keeps failing in the CI server?</p>
<p>That&rsquo;s when it&rsquo;s super useful to run the CI docker image locally, to iron out remaining environment-dependent quirks.</p>
<p>And you can do just that by running the GitLab CI Runner itself. It will pick up your project&rsquo;s <code>.gitlab-ci.yml</code> and run it locally through docker. So whatever problem you&rsquo;re seeing on the server will quite definitely happen locally as well. With the added benefit that you don&rsquo;t need to wait in line in case there is a queue of jobs waiting to run (many developers working, not as many machines to chew the CI work immediately). And you won&rsquo;t be the one adding useless jobs to the same queue.</p>
<p>And, if you&rsquo;ve followed my recommendations and installed the awesome Arch Linux distro (or my personal favorite derivative, Manjaro Gnome) you can easily install the runner through AUR like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>pacaur -S gitlab-runner</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Otherwise you will have to check your particular distro repos or download the binary from <a href="https://gitlab.com/gitlab-org/gitlab-runner/blob/master/docs/install/bleeding-edge.md#download-the-standalone-binaries"target="_blank" rel="noopener">here</a>. For example:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>wget https://gitlab-runner-downloads.s3.amazonaws.com/master/binaries/gitlab-runner-linux-amd64
sudo mv gitlab-runner-linux-amd64 /usr/local/bin
sudo chmod &#43;x /usr/local/bin/gitlab-runner</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Once you have it installed, let&rsquo;s say you have a snippet like this in your <code>.gitlab-ci.yml</code> configuration:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">backend</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">stage</span><span class="p">:</span><span class="w"> </span><span class="l">test</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">script</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">./bin/cc-test-reporter before-build</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">bundle exec rspec --exclude-pattern &#34;**/features/**/*_spec.rb&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">after_script</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">./bin/cc-test-reporter after-build --exit-code $? || true</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>You can configure many test jobs to run in parallel. I recommend that you separate front-end related JS tests, Rspec/minitest back-end unit tests, Capybara-based feature tests, Brakeman related security checks, for instance. Each will run in parallel and you won&rsquo;t have to wait for everything to run if you&rsquo;re only interested in the JS tests, for example. So you have faster feedback and can start fixing immediately.</p>
<p>Locally, from your project root directory, just run:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>gitlab-runner exec docker backend</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Of course, this is assuming that you already have Docker installed and properly configured. If you don&rsquo;t, make sure you read <a href="https://wiki.archlinux.org/index.php/Docker#Installation"target="_blank" rel="noopener">Arch Wiki&rsquo;s excellent entry on Docker</a> but it&rsquo;s basically installing the Docker package and adding yourself to the docker group.</p>
<p>This is it! Super easy, barely an inconvenience, and runner will do everything your GitLab CI server is doing.</p>
<p>One caveat is that unfortunately, it seems like you <a href="https://gitlab.com/gitlab-org/gitlab-runner/issues/2409"target="_blank" rel="noopener">don&rsquo;t have the ability to save cache or artifacts</a> between job runs (gems, npm packages, etc) so every time this local runner runs, it will start from scratch. This makes it less useful to run all the time, but it&rsquo;s still faster than pushing to the server and waiting in line for your turn in a busy server.</p>
<p>Saved me a couple times already.</p>
]]></content:encoded><category>devops</category><category>containers</category><category>testing</category></item><item><title>My Notes about a Production-grade Ruby on Rails Deployment on Google Cloud Kubernetes Engine</title><link>https://akitaonrails.github.io/en/2018/01/09/my-notes-about-a-production-grade-ruby-on-rails-deployment-on-google-cloud-kubernetes-engine/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2018/01/09/my-notes-about-a-production-grade-ruby-on-rails-deployment-on-google-cloud-kubernetes-engine/</guid><pubDate>Tue, 09 Jan 2018 20:13:00 -0200</pubDate><description>&lt;p&gt;I&amp;rsquo;ve been using Google Cloud with Kubernetes Engine for 2 months and change, from zero to production. Actually, it didn&amp;rsquo;t take me a month to put it all together but it did take an extra month to figure out some nasty rough edges.&lt;/p&gt;
&lt;p&gt;TL;DR: Google is actually doing a pretty good job being a counterbalancer so AWS doesn&amp;rsquo;t slack off. If you already know everything about AWS, I&amp;rsquo;d encourage you to test out Google Cloud. Possibly because of muscle memory, I would still be more comfortable with AWS, but now that I forced myself to suffer the learning process, I am pretty confident with Google Cloud and Kubernetes for most of my scenarios.&lt;/p&gt;</description><content:encoded><![CDATA[<p>I&rsquo;ve been using Google Cloud with Kubernetes Engine for 2 months and change, from zero to production. Actually, it didn&rsquo;t take me a month to put it all together but it did take an extra month to figure out some nasty rough edges.</p>
<p>TL;DR: Google is actually doing a pretty good job being a counterbalancer so AWS doesn&rsquo;t slack off. If you already know everything about AWS, I&rsquo;d encourage you to test out Google Cloud. Possibly because of muscle memory, I would still be more comfortable with AWS, but now that I forced myself to suffer the learning process, I am pretty confident with Google Cloud and Kubernetes for most of my scenarios.</p>
<p>Full disclaimer that I am not an expert, so take what I say with a grain of salt. This is one of those subjects that I am super eager to talk about but I&rsquo;m also very reluctant on the proper choice of words so you don&rsquo;t get the wrong idea about the proposed solutions.</p>
<p>The goal of this exercise is mainly for me to store some snippets and thoughts for future reference. So keep in mind that this is also not a step-by-step tutorial. My first intention was to go that way, but then I realized that it would be almost like writing an entire book, so not this time.</p>
<p>To succeed with something like Google Cloud and Kubernetes you <strong>must</strong> be battle tested in infrastructure. If you never installed server-grade Linux boxes from scratch, if you never did server optimizations, if you&rsquo;re not comfortable with bare server-side components, do not attempt a real production deployment. Your safest bet is still something like Heroku.</p>
<p>You have to be that kinda person that likes to tinker (as you&rsquo;ve probably read me doing in <a href="http://www.akitaonrails.com/linux"target="_blank" rel="noopener">previous</a> blog posts).</p>
<p>I don&rsquo;t know everything, but I know enough. So I only had to figure out which of the pieces would closely fit my needs. You <strong>must</strong> outline your needs before attempting to write your first YAML file. Planning is crucial.</p>
<p>First things first, this is what I wanted/needed:</p>
<ul>
<li>Scalable web application tier, where I could do both rolling updates (for <strong>zero downtime</strong> updates) and automatic and manual horizontal scaling of the servers.</li>
<li>Mountable persistent storage <strong>with</strong> automatic snapshots/backups.</li>
<li>Managed robust database (Postgresql) with automatic backups and <strong>easy replication</strong> to read-only instances.</li>
<li>Managed solution to store secrets (such as Heroku&rsquo;s ENV support). Never store production configuration in the source code.</li>
<li>Docker images support without me having to build custom infrastructure to deploy.</li>
<li>Static external IP addresses for integrations that required a fixed IP.</li>
<li>SSL termination so I could connect to CloudFlare (CDN is mandatory, but not enough, in 2018 we need some level of DDoS protection).</li>
<li>Enough security by default, so everything is - in theory - lockdown unless I decide to open it.</li>
<li>High-availability in different data center regions and zones.</li>
</ul>
<p>It&rsquo;s easy to deploy a simple demo web application. But I didn&rsquo;t want a demo, I wanted a production-grade solution for the long-run. Improvements to my implementation are super welcome, so feel free to comment down below.</p>
<p>Some of the problems for newcomers:</p>
<ul>
<li>The documentation is <em>very</em> extensive, and you will find <em>almost</em> everything - if you know what you are looking for. Also bear in mind that Azure and AWS also implement Kubernetes with some differences, so some documentation doesn&rsquo;t apply to Google Cloud and vice-versa.</li>
<li>There are many features in alpha, beta, and stable stages. The documentation kinda keeps up well, but most tutorials that are just a couple months old may not work as intended anymore (this one included - I am assuming Kubernetes 1.8.4-gke).</li>
<li>There is a whole set of words that apply to concepts you already know but are called differently. Getting used to the vocabulary may get in the way at first.</li>
<li>It feels like you&rsquo;re playing with Lego. Lots of pieces that you can mix and match. It&rsquo;s easy to mess up. This means that you can build a configuration tailored to your needs. But if you just copy and paste from tutorials you <strong>will</strong> get stuck.</li>
<li>You can do <em>almost</em> everything through YAML files and the command line, but it&rsquo;s not trivial to reuse the configuration (for production and staging environments, for example). There are 3rd party tools that deal with parameterizable and reusable YAML bits, but I&rsquo;d do it all by hand first. Never, ever, try automated templates in infrastructure without knowing exactly what they are doing.</li>
<li>You have 2 <em>fat</em> command line tools: <code>gcloud</code> and <code>kubectl</code>, and the confusing part is that they name some things differently even though they are the same &ldquo;things&rdquo;. At least, <code>kubectl</code> is close to <code>docker</code>, if you&rsquo;re familiar with that.</li>
</ul>
<p>Once again, this is <strong>NOT</strong> a step-by-step tutorial. I will annotate a few steps but not everything.</p>
<h3>Scalable Web-Tier (the Web App itself)<span class="hx:absolute hx:-mt-20" id="scalable-web-tier-the-web-app-itself"></span>
    <a href="#scalable-web-tier-the-web-app-itself" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The very first thing you must have is a fully 12-factor compliant web app.</p>
<p>Be it Ruby on Rails, Django, Laravel, Node.js or whatever. It must be a fully shared-nothing app, that does not depend on writing anything to the local filesystem. One where you can easily shut down and start up instances independently. No old-style session in local memory or in local files (I prefer to avoid session affinity). No uploads to the local filesystem (if you must, you will have to mount an external persistent storage), always prefer to send binary streams to managed storage services.</p>
<p>You must have a proper <a href="https://tomanistor.com/blog/cache-bust-that-asset/"target="_blank" rel="noopener">pipeline that outputs cache-busting through fingerprinting assets</a> (and like it or not, Rails still has the best out-of-the-box solution in its Asset Pipeline). You don&rsquo;t want to worry about manually busting caches in CDNs.</p>
<p>Instrument your app, add <a href="https://rpm.newrelic.com"target="_blank" rel="noopener">New Relic RPM</a>, add <a href="https://rollbar.com"target="_blank" rel="noopener">Rollbar</a>.</p>
<p>Again, this is 2018, you don&rsquo;t want to deploy naive code with SQL (or any other input) injection, no unchecked <code>eval</code> around your code, no room for CSRF or XSS, etc. Go ahead, buy the license for <a href="https://brakemanpro.com/"target="_blank" rel="noopener">Brakeman Pro</a> and add it to your CI pipeline. I can wait &hellip;</p>
<p>As this is not a tutorial, I will assume you&rsquo;re more than able to sign up to Google Cloud and find your way to set up a project, configure your region and zone.</p>
<p>It took me a while to wrap my head around the initial structure in Google Cloud:</p>
<ul>
<li>You start with a <a href="https://cloud.google.com/resource-manager/docs/creating-managing-projects"target="_blank" rel="noopener">Project</a>, which is the umbrella for everything your app needs.</li>
<li>Then you create <a href="https://cloud.google.com/kubernetes-engine/docs/concepts/cluster-architecture"target="_blank" rel="noopener">&ldquo;clusters&rdquo;</a>. You can have a production or staging cluster, for example. Or a web cluster and a separated services cluster for non-web stuff, and so on.</li>
<li>A cluster has a <a href="https://cloud.google.com/kubernetes-engine/docs/concepts/cluster-architecture#master"target="_blank" rel="noopener">&ldquo;cluster-master&rdquo;</a>, which is the controller of everything else (the <code>gcloud</code> and <code>kubectl</code> commands talk to its APIs).</li>
<li>A cluster has many <a href="https://cloud.google.com/kubernetes-engine/docs/concepts/cluster-architecture#nodes"target="_blank" rel="noopener">&ldquo;node instances&rdquo;</a>, the proper &ldquo;machines&rdquo; (or, more accurately, VM instances).</li>
<li>Each cluster also has at least one <a href="https://cloud.google.com/kubernetes-engine/docs/concepts/node-pools"target="_blank" rel="noopener">&ldquo;node pool&rdquo;</a> (the &ldquo;default-pool&rdquo;), which is a set of node instances with the same configuration, the same <a href="https://cloud.google.com/compute/docs/machine-types"target="_blank" rel="noopener">&ldquo;machine-type&rdquo;</a>.</li>
<li>Finally, each node instance runs one or more &ldquo;pods&rdquo; which are lightweight containers like LXC. This is where your application actually is.</li>
</ul>
<p>This is an example of <a href="https://cloud.google.com/sdk/gcloud/reference/container/clusters/create"target="_blank" rel="noopener">creating a cluster</a>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>gcloud container clusters create my-web-production \
--enable-cloud-logging \
--enable-cloud-monitoring \
--machine-type n1-standard-4 \
--enable-autoupgrade \
--enable-autoscaling --max-nodes=5 --min-nodes=2 \
--num-nodes 2</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>As I mentioned, it also creates a <code>default-pool</code> with a <a href="https://cloud.google.com/compute/docs/machine-types"target="_blank" rel="noopener">machine-type</a> of <code>n1-standard-4</code>. Choose what combination of CPU/RAM you will need for your particular app beforehand. The type I chose has 4 vCPUs and 15GB of RAM.</p>
<p>By default, it starts with 3 nodes, so I chose 2 at first but auto-scalable to 5 (you can update it later if you need to, but make sure you have room for initial growth). And you can keep adding extra node-pools for differently sized node instances, let&rsquo;s say, for Sidekiq workers to do heavy-duty background processing. Then you should create a separated Node Pool with a different machine-type for its set of node instances, for example:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>gcloud container node-pools create large-pool \
--cluster=my-web-production \
--node-labels=pool=large \
--machine-type=n1-highcpu-8 \
--num-nodes 1</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>This other pool controls 1 node of type <code>n1-highcpu-8</code> which has 8 vCPUs with 7.2 GB of RAM. More CPUs, less memory. You have a category of <code>highmem</code> which is less CPUs with a whole lot more memory. Again, know what you want beforehand.</p>
<p>The important bit here is the <code>--node-labels</code> this is how I will map the deployment to choose between Node Pools (in this case, between the <code>default-pool</code> and the <code>large-pool</code>).</p>
<p>Once you create a cluster, you must issue the following command to fetch its credentials:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>gcloud container clusters get-credentials my-web-production</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>This sets the <code>kubectl</code> command as well. If you have more than one cluster (let&rsquo;s say, one <code>my-web-production</code> and <code>my-web-staging</code>), you must be very careful to always <code>get-credentials</code> for the correct cluster first, otherwise, you may end up running a staging deployment on the production cluster.</p>
<p>Because this is confusing, I modified my ZSH PROMPT to always show which cluster I am dealing with. I adapted from <a href="https://github.com/superbrothers/zsh-kubectl-prompt"target="_blank" rel="noopener">zsh-kubectl-prompt</a>:</p>
<p><img src="https://github.com/superbrothers/zsh-kubectl-prompt/raw/master/images/screenshot001.png" alt="zsh kubectl prompt"  loading="lazy" /></p>
<p>As you will end up having multiple clusters in a big app, I highly recommend you add this PROMPT to your shell.</p>
<p>Now, how do you deploy your application to the pods within those fancy node instances?</p>
<p>You must have a <code>Dockerfile</code> in your application project repository to generate a Docker image. This is one example for a Ruby on Rails application:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>FROM ruby:2.4.3
ENV RAILS_ENV production
ENV SECRET_KEY_BASE xpto
RUN curl -sL https://deb.nodesource.com/setup_8.x | bash -
RUN apt-get update &amp;&amp; apt-get install -y nodejs postgresql-client cron htop vim
ADD Gemfile* /app/
WORKDIR /app
RUN gem update bundler --pre
RUN bundle install --without development test
RUN npm install
ADD . /app
RUN cp config/database.yml.prod.example config/database.yml &amp;&amp; cp config/application.yml.example config/application.yml
RUN RAILS_GROUPS=assets bundle exec rake assets:precompile</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>From the Google Cloud Web Console, you will find a <a href="https://cloud.google.com/container-registry/"target="_blank" rel="noopener">&ldquo;Container Registry&rdquo;</a>, which is a Private Docker Registry.</p>
<p>You must add the remote URL to your local config like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>git remote add gcloud https://source.developers.google.com/p/my-project/r/my-app</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Now you can <code>git push gcloud master</code>. I recommend you also add <a href="https://cloud.google.com/container-builder/docs/running-builds/automate-builds"target="_blank" rel="noopener">triggers</a> to tag your images. I add 2 triggers: one to tag it with <code>latest</code> and another to tag it with a random version number. You will need those later.</p>
<p>Once you add the registry repository as a remote on your git configuration (<code>git remote add</code>) and push to it, it should start building your Docker image with the proper tags you configured with the triggers.</p>
<p>Make sure your Ruby on Rails application doesn&rsquo;t have anything in the initializers that require a connection to the database, as it&rsquo;s not available. This is something you might get stuck with when your Docker build fails because of the <code>assets:precompile</code> task loaded an initializer that accidentally calls a Model - and that triggers <code>ActiveRecord::Base</code> to try to connect.</p>
<p>Also, make sure the Ruby version in the <code>Dockerfile</code> matches the one in <code>Gemfile</code>, otherwise it will also fail.</p>
<p>Notice the weird <code>config/application.yml</code> above? This is from <a href="https://github.com/laserlemon/figaro"target="_blank" rel="noopener">figaro</a>. I also recommend you using something to make it easy to configure ENV variables in your system. I don&rsquo;t like Rails secrets, and it&rsquo;s not exactly friendly to most deployment systems after Heroku made ENV vars ubiquitous. Stick to ENV vars. Kubernetes will also thank you for that.</p>
<p>Now, you can override any ENV variable from the Kubernetes Deployment YAML file. Now it&rsquo;s a good time to show an example of that. You can name it <code>deploy/web.yml</code> or whatever suits your fancy and - of course - check it into your source code repository.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Deployment</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">apps/v1beta1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">web</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">strategy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">RollingUpdate</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">rollingUpdate</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">maxSurge</span><span class="p">:</span><span class="w"> </span><span class="m">1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">maxUnavailable</span><span class="p">:</span><span class="w"> </span><span class="m">1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">minReadySeconds</span><span class="p">:</span><span class="w"> </span><span class="m">10</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">replicas</span><span class="p">:</span><span class="w"> </span><span class="m">2</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">template</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">app</span><span class="p">:</span><span class="w"> </span><span class="l">web</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">containers</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">gcr.io/my-project/my-app:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">my-app</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">imagePullPolicy</span><span class="p">:</span><span class="w"> </span><span class="l">Always</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span>- <span class="nt">containerPort</span><span class="p">:</span><span class="w"> </span><span class="m">4001</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">command</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;passenger&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;start&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;-p&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;4001&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;-e&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;production&#34;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="s2">&#34;--max-pool-size&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;2&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;--min-instances&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;2&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;--no-friendly-error-pages&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="s2">&#34;--max-request-queue-time&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;10&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;--max-request-time&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;10&#34;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="s2">&#34;--pool-idle-time&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;0&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;--memory-limit&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;300&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">env</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;RAILS_LOG_TO_STDOUT&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">value</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;true&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;RAILS_ENV&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">value</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;production&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="c"># ... obviously reduced the many ENV vars for brevity</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;REDIS_URL&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">valueFrom</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="nt">secretKeyRef</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">my-env</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                  </span><span class="nt">key</span><span class="p">:</span><span class="w"> </span><span class="l">REDIS_URL</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;SMTP_USERNAME&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">valueFrom</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="nt">secretKeyRef</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">my-env</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                  </span><span class="nt">key</span><span class="p">:</span><span class="w"> </span><span class="l">SMTP_USERNAME</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;SMTP_PASSWORD&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">valueFrom</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="nt">secretKeyRef</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">my-env</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                  </span><span class="nt">key</span><span class="p">:</span><span class="w"> </span><span class="l">SMTP_PASSWORD</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="c"># ... this part below is mandatory for Cloud SQL</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">DB_HOST</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">value</span><span class="p">:</span><span class="w"> </span><span class="m">127.0.0.1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">DB_PASSWORD</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">valueFrom</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="nt">secretKeyRef</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">cloudsql-db-credentials</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                  </span><span class="nt">key</span><span class="p">:</span><span class="w"> </span><span class="l">password</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">DB_USER</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">valueFrom</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="nt">secretKeyRef</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">cloudsql-db-credentials</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                  </span><span class="nt">key</span><span class="p">:</span><span class="w"> </span><span class="l">username</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">gcr.io/cloudsql-docker/gce-proxy:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">cloudsql-proxy</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">command</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;/cloud_sql_proxy&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;--dir=/cloudsql&#34;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                    </span><span class="s2">&#34;-instances=my-project:us-west1:my-db=tcp:5432&#34;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                    </span><span class="s2">&#34;-credential_file=/secrets/cloudsql/credentials.json&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">volumeMounts</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">cloudsql-instance-credentials</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">mountPath</span><span class="p">:</span><span class="w"> </span><span class="l">/secrets/cloudsql</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">readOnly</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">ssl-certs</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">mountPath</span><span class="p">:</span><span class="w"> </span><span class="l">/etc/ssl/certs</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">cloudsql</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">mountPath</span><span class="p">:</span><span class="w"> </span><span class="l">/cloudsql</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">cloudsql-instance-credentials</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">secret</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">secretName</span><span class="p">:</span><span class="w"> </span><span class="l">cloudsql-instance-credentials</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">ssl-certs</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">hostPath</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="l">/etc/ssl/certs</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">cloudsql</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="l">emptyDir:</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>There is a lot going on here. So let me break it down a bit:</p>
<ul>
<li>The <code>kind</code>, and <code>apiVersion</code> is important, you have to keep an eye on the documentation if those change. This is what is called a <a href="https://kubernetes.io/docs/concepts/workloads/controllers/deployment/"target="_blank" rel="noopener">Deployment</a>. There used to be a Replication Controller (you will find those in old tutorials), but it&rsquo;s no longer in use. The recommendation is to use a <a href="https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/"target="_blank" rel="noopener">ReplicaSet</a>.</li>
<li>Name things correctly, here you have <code>metadata:name</code> with <code>web</code>. Also pay close attention to <code>spec:template:metadata:labels</code> where I am labeling every pod having a label of <code>app: web</code>, you will need this to be able to select those pods later in the Service section down below.</li>
<li>Then I have <code>spec:strategy</code> where we configure the Rolling Update, so if you have 10 pods, it will terminate one, boot up the new one and keep doing that, without never bringing everything down at once.</li>
<li><code>spec:replicas</code> declares how many Pods I want at once. You will have to manually calculate the machine-type of the node-pool then divide how many total CPUs/RAM you have by how much you need for each application instance.</li>
<li>Remember the Docker image we generated above with the &rsquo;latest&rsquo; tag? You refer to it in <code>spec:template:spec:containers:image</code></li>
<li>I am using Passenger with production configuration (check out <a href="https://www.phusionpassenger.com/library/config/reference/"target="_blank" rel="noopener">Phusion&rsquo;s documentation</a>, do not just copy this).</li>
<li>In the <code>spec:template:spec:containers:env</code> section I can override the ENV vars with the real production secrets. And you will notice that I can hard-code values or use this strange contraption:</li>
</ul>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl">- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;SMTP_USERNAME&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">valueFrom</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">secretKeyRef</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">my-env</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">key</span><span class="p">:</span><span class="w"> </span><span class="l">SMTP_USERNAME</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Now, it&rsquo;s referencing a <a href="https://kubernetes.io/docs/concepts/configuration/secret/"target="_blank" rel="noopener">&ldquo;Secret&rdquo;</a> storage that I named &ldquo;my-env&rdquo;. And this is how you create your own:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>kubectl create secret generic my-env \
--from-literal=REDIS_URL=redis://foo.com:18821 \
--from-literal=SMTP_USERNAME=foobar</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Read the documentation as you can load text files instead of declaring everything from the command line.</p>
<p>As I said before, I&rsquo;d rather use a managed service for a database. You can definitely load your own Docker image, but I really don&rsquo;t recommend it. Same goes for other database-like services such as Redis, Mongo. If you&rsquo;re from AWS, <a href="https://cloud.google.com/sql/docs/postgres/"target="_blank" rel="noopener">Google Cloud SQL</a> is like RDS.</p>
<p>After you create your PostgreSQL instance you can&rsquo;t access it directly from the web application. In the end, you have a boilerplate for a second Docker image, a <a href="https://cloud.google.com/sql/docs/mysql/connect-kubernetes-engine"target="_blank" rel="noopener">&ldquo;CloudSQL Proxy&rdquo;</a>.</p>
<p>For that to work you must first create a Service Account:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>gcloud sql users create proxyuser host --instance=my-db --password=abcd1234</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>After you create the PostgreSQL instance it will prompt you to download a JSON credential, so be careful and save it somewhere safe. I don&rsquo;t have to say that you must generate a strong secure password as well. Then you must create extra secrets:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>kubectl create secret generic cloudsql-instance-credentials \
--from-file=credentials.json=/home/myself/downloads/my-db-12345.json

kubectl create secret generic cloudsql-db-credentials \
--from-literal=username=proxyuser --from-literal=password=abcd1234</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>These are referenced in this part of the Deployment:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl">- <span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">gcr.io/cloudsql-docker/gce-proxy:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">cloudsql-proxy</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">command</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;/cloud_sql_proxy&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;--dir=/cloudsql&#34;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="s2">&#34;-instances=my-project:us-west1:my-db=tcp:5432&#34;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="s2">&#34;-credential_file=/secrets/cloudsql/credentials.json&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">volumeMounts</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">cloudsql-instance-credentials</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">mountPath</span><span class="p">:</span><span class="w"> </span><span class="l">/secrets/cloudsql</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">readOnly</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">ssl-certs</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">mountPath</span><span class="p">:</span><span class="w"> </span><span class="l">/etc/ssl/certs</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">cloudsql</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">mountPath</span><span class="p">:</span><span class="w"> </span><span class="l">/cloudsql</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl">- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">cloudsql-instance-credentials</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">secret</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">secretName</span><span class="p">:</span><span class="w"> </span><span class="l">cloudsql-instance-credentials</span><span class="w">
</span></span></span><span class="line"><span class="cl">- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">ssl-certs</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">hostPath</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="l">/etc/ssl/certs</span><span class="w">
</span></span></span><span class="line"><span class="cl">- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">cloudsql</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="l">emptyDir:</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>See that you must add the database name (&ldquo;my-db&rdquo; in this example) in the <code>-instance</code> clause in the command.</p>
<p>And by the way, the <code>gce-proxy:latest</code> refers to version 1.09 at the time when this post was published. But there already was a 1.11 version. That one gave me headaches, dropping connections and adding a super long timeout. So I went back to the 1.09 (latest) and everything worked as expected. So be aware! Not everything that is brand new is good. In infrastructure, you want to stick to stable.</p>
<p>You may also want the option to load a separated CloudSQL instance instead of having it in each pod, so the pods could connect to just one proxy. You may want to read <a href="https://github.com/GoogleCloudPlatform/cloudsql-proxy/issues/49"target="_blank" rel="noopener">this thread</a> on the subject.</p>
<p>It seems that nothing is exposed to anything unless you say so. So we need to expose those pods through what&rsquo;s called a <a href="https://kubernetes.io/docs/concepts/services-networking/service/#type-nodeport"target="_blank" rel="noopener">Node Port Service</a>. Let&rsquo;s create a <code>deploy/web-svc.yaml</code> file as well:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Service</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">web-svc</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">sessionAffinity</span><span class="p">:</span><span class="w"> </span><span class="l">None</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">port</span><span class="p">:</span><span class="w"> </span><span class="m">80</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">targetPort</span><span class="p">:</span><span class="w"> </span><span class="m">4001</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">protocol</span><span class="p">:</span><span class="w"> </span><span class="l">TCP</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">NodePort</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">selector</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">app</span><span class="p">:</span><span class="w"> </span><span class="l">web</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>This is why I highlighted the importance of the <code>spec:template:metadata:labels</code>, so we can use it here in the <code>spec:selector</code> to select the proper pods.</p>
<p>We can now deploy these 2 like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>kubectl create -f deploy/web.yml
kubectl create -f deploy/web-svc.yml</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>And you can see the pods being created with <code>kubectl get pods --watch</code>.</p>
<h2>The Load Balancer<span class="hx:absolute hx:-mt-20" id="the-load-balancer"></span>
    <a href="#the-load-balancer" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Many tutorials will expose those pods directly through a different Service, called <a href="https://kubernetes.io/docs/tasks/access-application-cluster/create-external-load-balancer/"target="_blank" rel="noopener">Load Balancer</a>. I am not so sure how well this behaves under pressure and if it has SSL termination, etc. So I decided to go full blown with an <a href="https://kubernetes.io/docs/concepts/services-networking/ingress/"target="_blank" rel="noopener">Ingress</a> Load Balancer using the <a href="https://www.nginx.com/products/nginx/kubernetes-ingress-controller/"target="_blank" rel="noopener">NGINX Controller</a>, instead.</p>
<p>First of all, I decided to create a separated node-pool for it, for example, like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>gcloud container node-pools create web-load-balancer \
--cluster=my-web-production \
--node-labels=role=load-balancer \
--machine-type=g1-small \
--num-nodes 1 \
--max-nodes 3 --min-nodes=1 \
--enable-autoscaling </code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>As when we created the example <code>large-pool</code> here you must take care of adding <code>--node-labels</code> to make the controller be installed here instead of the <code>default-pool</code>. You will need to know the node instance name, we can do it like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>$ gcloud compute instances list
NAME                                             ZONE        MACHINE_TYPE   PREEMPTIBLE  INTERNAL_IP  EXTERNAL_IP      STATUS
gke-my-web-production-default-pool-123-123       us-west1-a  n1-standard-4               10.128.0.1   123.123.123.12   RUNNING
gke-my-web-production-large-pool-123-123         us-west1-a  n1-highcpu-8                10.128.0.2   50.50.50.50      RUNNING
gke-my-web-production-web-load-balancer-123-123  us-west1-a  g1-small                    10.128.0.3   70.70.70.70      RUNNING</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Let&rsquo;s save it like this for now:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>export LB_INSTANCE_NAME=gke-my-web-production-web-load-balancer-123-123</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>You can manually reserve an external IP and give it a name like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>gcloud compute addresses create ip-web-production \
        --ip-version=IPV4 \
        --global</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>For the sake of the example, let&rsquo;s say that it generated a reserved IP &ldquo;111.111.111.111&rdquo;. Then let&rsquo;s fetch it and save it for now like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>export LB_ADDRESS_IP=$(gcloud compute addresses list | grep &#34;ip-web-production&#34; | awk &#39;{print $3}&#39;)</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Finally, let&rsquo;s hook this address to the load balancer node instance:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>export LB_INSTANCE_NAT=$(gcloud compute instances describe $LB_INSTANCE_NAME | grep -A3 networkInterfaces: | tail -n1 | awk -F&#39;: &#39; &#39;{print $2}&#39;)
gcloud compute instances delete-access-config $LB_INSTANCE_NAME \
    --access-config-name &#34;$LB_INSTANCE_NAT&#34;
gcloud compute instances add-access-config $LB_INSTANCE_NAME \
    --access-config-name &#34;$LB_INSTANCE_NAT&#34; --address $LB_ADDRESS_IP</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Once we do this, we can add the rest of the Ingress Deployment configuration. This will be kinda long but it&rsquo;s mostly boilerplate. Let&rsquo;s start by defining another web application that we will call <code>default-http-backend</code> that will be used to respond to HTTP requests in case our web pods are not available for some reason. Let&rsquo;s call it <code>deploy/default-web.yml</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">extensions/v1beta1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Deployment</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">default-http-backend</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">replicas</span><span class="p">:</span><span class="w"> </span><span class="m">1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">template</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">app</span><span class="p">:</span><span class="w"> </span><span class="l">default-http-backend</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">terminationGracePeriodSeconds</span><span class="p">:</span><span class="w"> </span><span class="m">60</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">containers</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">default-http-backend</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="c"># Any image is permissable as long as:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="c"># 1. It serves a 404 page at /</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="c"># 2. It serves 200 on a /healthz endpoint</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">gcr.io/google_containers/defaultbackend:1.0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">livenessProbe</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">httpGet</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="l">/healthz</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">port</span><span class="p">:</span><span class="w"> </span><span class="m">8080</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">scheme</span><span class="p">:</span><span class="w"> </span><span class="l">HTTP</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">initialDelaySeconds</span><span class="p">:</span><span class="w"> </span><span class="m">30</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">timeoutSeconds</span><span class="p">:</span><span class="w"> </span><span class="m">5</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">containerPort</span><span class="p">:</span><span class="w"> </span><span class="m">8080</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">resources</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">limits</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">cpu</span><span class="p">:</span><span class="w"> </span><span class="l">10m</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">memory</span><span class="p">:</span><span class="w"> </span><span class="l">20Mi</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">requests</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">cpu</span><span class="p">:</span><span class="w"> </span><span class="l">10m</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">memory</span><span class="p">:</span><span class="w"> </span><span class="l">20Mi</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>No need to change anything here, and by now you may be familiar with the Deployment template. Again, you now know that you need to expose it through a NodePort, so let&rsquo;s add a <code>deploy/default-web-svc.yml</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Service</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">default-http-backend</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">selector</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">app</span><span class="p">:</span><span class="w"> </span><span class="l">default-http-backend</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">protocol</span><span class="p">:</span><span class="w"> </span><span class="l">TCP</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">port</span><span class="p">:</span><span class="w"> </span><span class="m">80</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">targetPort</span><span class="p">:</span><span class="w"> </span><span class="m">8080</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">NodePort</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Again, no need to change anything. The next 3 files are the important parts. First, we will create an NGINX Load Balancer, let&rsquo;s call it <code>deploy/nginx.yml</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">extensions/v1beta1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Deployment</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">nginx-ingress-controller</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">replicas</span><span class="p">:</span><span class="w"> </span><span class="m">1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">template</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">k8s-app</span><span class="p">:</span><span class="w"> </span><span class="l">nginx-ingress-lb</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="c"># hostNetwork makes it possible to use ipv6 and to preserve the source IP correctly regardless of docker configuration</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="c"># however, it is not a hard dependency of the nginx-ingress-controller itself and it may cause issues if port 10254 already is taken on the host</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="c"># that said, since hostPort is broken on CNI (https://github.com/kubernetes/kubernetes/issues/31307) we have to use hostNetwork where CNI is used</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">hostNetwork</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">terminationGracePeriodSeconds</span><span class="p">:</span><span class="w"> </span><span class="m">60</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">nodeSelector</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">role</span><span class="p">:</span><span class="w"> </span><span class="l">load-balancer</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">containers</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">args</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="l">/nginx-ingress-controller</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="s2">&#34;--default-backend-service=$(POD_NAMESPACE)/default-http-backend&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="s2">&#34;--default-ssl-certificate=$(POD_NAMESPACE)/cloudflare-secret&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">env</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">POD_NAME</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">valueFrom</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="nt">fieldRef</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                  </span><span class="nt">fieldPath</span><span class="p">:</span><span class="w"> </span><span class="l">metadata.name</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">POD_NAMESPACE</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">valueFrom</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="nt">fieldRef</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                  </span><span class="nt">fieldPath</span><span class="p">:</span><span class="w"> </span><span class="l">metadata.namespace</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;gcr.io/google_containers/nginx-ingress-controller:0.9.0-beta.5&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">imagePullPolicy</span><span class="p">:</span><span class="w"> </span><span class="l">Always</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">livenessProbe</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">httpGet</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="l">/healthz</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">port</span><span class="p">:</span><span class="w"> </span><span class="m">10254</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">scheme</span><span class="p">:</span><span class="w"> </span><span class="l">HTTP</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">initialDelaySeconds</span><span class="p">:</span><span class="w"> </span><span class="m">10</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">timeoutSeconds</span><span class="p">:</span><span class="w"> </span><span class="m">5</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">nginx-ingress-controller</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="nt">containerPort</span><span class="p">:</span><span class="w"> </span><span class="m">80</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">http</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">protocol</span><span class="p">:</span><span class="w"> </span><span class="l">TCP</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="nt">containerPort</span><span class="p">:</span><span class="w"> </span><span class="m">443</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">https</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">protocol</span><span class="p">:</span><span class="w"> </span><span class="l">TCP</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">volumeMounts</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="nt">mountPath</span><span class="p">:</span><span class="w"> </span><span class="l">/etc/nginx-ssl/dhparam</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">tls-dhparam-vol</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">tls-dhparam-vol</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">secret</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">secretName</span><span class="p">:</span><span class="w"> </span><span class="l">tls-dhparam</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Notice the <code>nodeSelector</code> to make the node label we added when we created the new node-pool.</p>
<p>You may want to tinker with the labels, the number of replicas if you need to. But here you will notice that it mounts a volume that I named as <code>tls-dhparam-vol</code>. This is a <a href="https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html#Forward_Secrecy_&amp;_Diffie_Hellman_Ephemeral_Parameters"target="_blank" rel="noopener">Diffie Hellman Ephemeral Parameters</a>. This is how we generate it:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>sudo openssl dhparam -out ~/documents/dhparam.pem 2048

kubectl create secret generic tls-dhparam --from-file=/home/myself/documents/dhparam.pem

kubectl create secret generic tls-dhparam --from-file=/home/myself/documents/dhparam.pem</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Also, notice that I am using version &ldquo;0.9.0-beta_5&rdquo; for the controller image. It works well, no problems so far. But keep an eye on release notes for newer versions as well and do your own testing.</p>
<p>Again, let&rsquo;s expose this Ingress controller through the Load Balancer Service. Let&rsquo;s call it <code>deploy/nginx-svc.yml</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Service</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">nginx-ingress</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">LoadBalancer</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">loadBalancerIP</span><span class="p">:</span><span class="w"> </span><span class="m">111.111.111.111</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">http</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">port</span><span class="p">:</span><span class="w"> </span><span class="m">80</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">targetPort</span><span class="p">:</span><span class="w"> </span><span class="l">http</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">https</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">port</span><span class="p">:</span><span class="w"> </span><span class="m">443</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">targetPort</span><span class="p">:</span><span class="w"> </span><span class="l">https</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">selector</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">k8s-app</span><span class="p">:</span><span class="w"> </span><span class="l">nginx-ingress-lb</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Remember the static external IP we have reserved above and saved in the <code>LB_INGRESS_IP</code> ENV var? This is the one we must put in the <code>spec:loadBalancerIP</code> section. This is also the IP that you will add as an &ldquo;A record&rdquo; in your DNS service (let&rsquo;s say, mapping your &ldquo;<a href="https://www.my-app.com.br"target="_blank" rel="noopener">www.my-app.com.br</a>&rdquo; on CloudFlare).</p>
<p>Finally, we can create the Ingress configuration itself, let&rsquo;s create a <code>deploy/ingress.yml</code> like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">extensions/v1beta1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Ingress</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">ingress</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">annotations</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">kubernetes.io/ingress.class</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;nginx&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">nginx.org/ssl-services</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;web-svc&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">kubernetes.io/ingress.global-static-ip-name</span><span class="p">:</span><span class="w"> </span><span class="l">ip-web-production</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ingress.kubernetes.io/ssl-redirect</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;true&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ingress.kubernetes.io/rewrite-target</span><span class="p">:</span><span class="w"> </span><span class="l">/</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">tls</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">hosts</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">www.my-app.com.br</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">secretName</span><span class="p">:</span><span class="w"> </span><span class="l">cloudflare-secret</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">rules</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">host</span><span class="p">:</span><span class="w"> </span><span class="l">www.my-app.com.br</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">http</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">paths</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="l">/</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">backend</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">serviceName</span><span class="p">:</span><span class="w"> </span><span class="l">web-svc</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">servicePort</span><span class="p">:</span><span class="w"> </span><span class="m">80</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Be careful with the annotations above that hooks everything up. It binds the NodePort service we created for the web pods with the nginx ingress controller and adds SSL termination through that <code>spec:tls:secretName</code>. How do you create that? First, you must purchase an SSL certificate - again, using CloudFlare as the example.</p>
<p>When you finish buying, the provider should give you the secret files to download (keep them safe! a public dropbox folder is not safe!). Then you have to add it to the infrastructure like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>kubectl create secret tls cloudflare-secret \
--key ~/downloads/private.pem \
--cert ~/downloads/fullchain.pem</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Now that we edited a whole bunch of files, we can deploy the entire load balancer stack:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>kubectl create -f deploy/default-web.yml
kubectl create -f deploy/default-web-svc.yml
kubectl create -f deploy/nginx.yml
kubectl create -f deploy/nginx-svc.yml
kubectl create -f deploy/ingress.yml</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>This NGINX Ingress configuration is based off of <a href="https://zihao.me/post/cheap-out-google-container-engine-load-balancer/"target="_blank" rel="noopener">Zihao Zhang&rsquo;s blog post</a>. There are also examples in the <a href="https://github.com/kubernetes-incubator/external-dns/blob/master/docs/tutorials/nginx-ingress.md"target="_blank" rel="noopener">kubernetes incubator repository</a>. You may want to check it out as well.</p>
<p>If you did everything right so far, <code>https://www.my-app-com.br</code> should load your web application. You may want to check for Time to First-Byte (TTFB). You can do it going through CloudFlare like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>curl -vso /dev/null -w &#34;Connect: %{time_connect} \n TTFB: %{time_starttransfer} \n Total time: %{time_total} \n&#34; https://www.my-app.com.br</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Or, if you&rsquo;re having slow TTFB you can bypass CloudFlare doing this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>curl --resolve www.my-app.com.br:443:111.111.111.111 https://www.my-app.com.br -svo /dev/null -k -w &#34;Connect: %{time_connect} \n TTFB: %{time_starttransfer} \n Total time: %{time_total} \n&#34;</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>TTFB should be in the neighborhood of 1 second or less. Anything far and above could mean a problem in your application. You must check your node instance machine types, the number of workers loaded per pod, the CloudSQL proxy version, the NGINX controller version and so on. This is a trial and error procedure as far as I know. Sign up to services such as <a href="https://loader.io"target="_blank" rel="noopener">Loader</a> or even <a href="https://www.webpagetest.org"target="_blank" rel="noopener">Web Page Test</a> for insight.</p>
<h2>Rolling Updates<span class="hx:absolute hx:-mt-20" id="rolling-updates"></span>
    <a href="#rolling-updates" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Now, that everything is up and running, how do we accomplish the Rolling Update I mentioned in the beginning? First you <code>git push</code> to the Container Registry repository and wait for the Docker image to build.</p>
<p>Remember that I said to let a trigger tag the image with a random version number? Let&rsquo;s use it (you can see it from the Build History list in the Container Registry, from the Google Cloud console):</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>kubectl set image deployment web my-app=gcr.io/my-project/my-app:1238471234g123f534f543541gf5 --record</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>You must use the same name and image that is declared in the <code>deploy/web.yml</code> from above. This will start rolling out the update by adding a new pod, then terminating one pod and so on and so forth until all of them are updated, without downtime for your users.</p>
<p>Rolling updates must be carried out carefully. For example, if your new deployment requires a database migration, then you must add a maintenance window (meaning: do it when there is little to no traffic, such as in the middle of the night). So you can run the migrate command like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>kubectl get pods # to get a pod name

kubectl exec -it my-web-12324-121312 /app/bin/rails db:migrate

# you can also bash to a pod like this, but remember that this is an ephemeral container, so files you edit and write there disappear on the next restart:

kubectl exec -it my-web-12324-121312 bash</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>To redeploy everything without resorting to rolling update you must do this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>kubectl delete -f deploy/web.yml &amp;&amp; kubectl apply -f deploy/web.yml</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>You will find a more thorough explanation in <a href="https://tachingchen.com/blog/Kubernetes-Rolling-Update-with-Deployment/"target="_blank" rel="noopener">Ta-Ching&rsquo;s</a> blog post.</p>
<h2>Bonus: Auto Snapshots<span class="hx:absolute hx:-mt-20" id="bonus-auto-snapshots"></span>
    <a href="#bonus-auto-snapshots" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>One item I had in my &ldquo;I wanted/needed&rdquo; list, in the beginning, is the ability to have persistent mountable storage with automatic backups/snapshots. Google Cloud provides half of that for the time being. You can create persistent disks to mount in your pods but it doesn&rsquo;t have a feature to automatically back it up. At least it does have APIs to manually snapshot it.</p>
<p>For this example, let&rsquo;s create a new SSD disk and format it first:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>gcloud compute disks create --size 500GB my-data --type pd-ssd

gcloud compute instances list</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>The last command is so we can copy the name of a node instance. Let&rsquo;s say it&rsquo;s <code>gke-my-web-app-default-pool-123-123</code>. We will attach the <code>my-data</code> disk to it:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>gcloud compute instances attach-disk gke-my-web-app-default-pool-123-123 --disk my-data --device-name my-data

gcloud compute ssh gke-my-web-app-default-pool-123-123</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>The last command ssh&rsquo;s in the instance. We can list the attached disks with <code>sudo lsblk</code> and you will see the 500GB disk, probably, as <code>/dev/sdb</code>, but make sure that&rsquo;s correct because we will format it!</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>sudo mkfs.ext4 -m 0 -F -E lazy_itable_init=0,lazy_journal_init=0,discard /dev/sdb</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Now we can exit from the SSH session and detach the disk:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>gcloud compute instances detach-disk gke-my-web-app-default-pool-123-123 --disk my-data</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>You can mount this disk in your pods by adding the following to your deployment yaml:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>spec:
  containers:
    - image: ...
      name: my-app
      volumeMounts:
        - name: my-data
          mountPath: /data
          # readOnly: true
   # ...
   volumes:
     - name: my-data
       gcePersistentDisk:
         pdName: my-data
         fsType: ext4</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Now, let&rsquo;s create a CronJob deployment file as <code>deploy/auto-snapshot.yml</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">batch/v1beta1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">CronJob</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">auto-snapshot</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">schedule</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;0 4 * * *&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">concurrencyPolicy</span><span class="p">:</span><span class="w"> </span><span class="l">Forbid</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">jobTemplate</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">template</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">restartPolicy</span><span class="p">:</span><span class="w"> </span><span class="l">OnFailure</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">containers</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">auto-snapshot</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">grugnog/google-cloud-auto-snapshot</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">command</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;/opt/entrypoint.sh&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">env</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;GOOGLE_CLOUD_PROJECT&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">value</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;my-project&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;GOOGLE_APPLICATION_CREDENTIALS&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">value</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;/credential/credential.json&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">volumeMounts</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span>- <span class="nt">mountPath</span><span class="p">:</span><span class="w"> </span><span class="l">/credential</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">editor-credential</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">editor-credential</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">secret</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="nt">secretName</span><span class="p">:</span><span class="w"> </span><span class="l">editor-credential</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>As we already did before, you will need to create another Service Account with editor permissions in the &ldquo;IAM &amp; admin&rdquo; section of the Google Cloud console, then download the JSON credential, and finally upload it like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>kubectl create secret generic editor-credential \
--from-file=credential.json=/home/myself/download/my-project-1212121.json</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Also notice that, as a normal cron job, there is a schedule parameter that you might want to change. In the example, &ldquo;0 4 * * *&rdquo; means that it will run the snapshot every day at 4 AM.</p>
<p>Check out the <a href="https://github.com/grugnog/google-cloud-auto-snapshot"target="_blank" rel="noopener">original repository</a> of this solution for more details.</p>
<p>And this should be it for now!</p>
<p>As I said, in the beginning, this is not a complete procedure, just highlights of some of the important parts. If you&rsquo;re new to Kubernetes you just read about Deployment, Service, Ingress, but you have ReplicaSet, DaemonSet, and much more to play with.</p>
<p>I think this is also already too long to add a <a href="https://cloud.google.com/sql/docs/mysql/high-availability"target="_blank" rel="noopener">multi-region High Availability</a> setup explanation, so let&rsquo;s leave it at that.</p>
<p>Any corrections or suggestions are more than welcome as I am still in the learning process, and there are a ton of things that I still don&rsquo;t know myself.</p>
]]></content:encoded><category>rails</category><category>cloud</category><category>containers</category><category>devops</category></item><item><title>Choosing MatterMost over Rocket.chat and Slack</title><link>https://akitaonrails.github.io/en/2016/08/13/choosing-mattermost-over-rocket-chat-and-slack/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2016/08/13/choosing-mattermost-over-rocket-chat-and-slack/</guid><pubDate>Sat, 13 Aug 2016 22:41:00 -0300</pubDate><description>&lt;p&gt;I am a technologist, basically a nerd. So when I obsess over technical stuff, no matter how small, I just can&amp;rsquo;t sleep calmly until I find reasonable closure.&lt;/p&gt;
&lt;p&gt;My team, my clients, we&amp;rsquo;ve been happily using Slack for more than 2 years, as I believe was true for many teams around the world. Although no one ever complained, I was always annoyed by the small things. First of all, as everything of value, it costs. Either I pay USD 6.67/month/user or I live with the restrictions of the free plan. And as most other teams, I lived with the restrictions for as long as I could.&lt;/p&gt;</description><content:encoded><![CDATA[<p>I am a technologist, basically a nerd. So when I obsess over technical stuff, no matter how small, I just can&rsquo;t sleep calmly until I find reasonable closure.</p>
<p>My team, my clients, we&rsquo;ve been happily using Slack for more than 2 years, as I believe was true for many teams around the world. Although no one ever complained, I was always annoyed by the small things. First of all, as everything of value, it costs. Either I pay USD 6.67/month/user or I live with the restrictions of the free plan. And as most other teams, I lived with the restrictions for as long as I could.</p>
<p>For example, to get rid of the warnings to upgrade because we hit the upload limits, I created a small tool called <a href="https://github.com/akitaonrails/slack_cleanup"target="_blank" rel="noopener">slack_cleanup</a> (first in Elixir, and then in <a href="https://github.com/akitaonrails/cr_slack_cleanup"target="_blank" rel="noopener">Crystal</a> just for exercise). This helped for a while.</p>
<p>Our team kept growing, steadily, frequently, as well as clients. The more users we add, the more conversations, the faster we hit the restrictions. History gets lost more frequently, we need to clean up uploads more often. It gets old very fast.</p>
<p>One thing I value above everything is knowledge. As a small example, I keep multiple backups of all my emails, all my projects, all my assets that I produced in the last 20 years. Heck, I have a 6 TB, Raid-5, Drobo system right in my home desk and 3 extra 1TB external drives for backup. I&rsquo;ve lost very little over the years.</p>
<p>It really annoys me when I lose information.</p>
<ul>
<li>
<p>Gmail Business, DropBox, AWS S3 buckets, being external services, don&rsquo;t worry me because I keep copies of everything offline. So if those accounts get busted all of a sudden, I have multiple copies.</p>
</li>
<li>
<p>GitHub annoyed me a bit because although I have multiple copies of the repositories, I would lose all the Pull Request and Issues history. That&rsquo;s one reason I moved to my own GitLab and helped tweak the import process to grab that Pull Request history.</p>
</li>
<li>
<p>Slack annoys me a lot for the aforementioned reasons, which is why last week we tried MatterMost first, and then we deployed Rocket.chat.</p>
</li>
</ul>
<p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/558/big_8_13_16__22_20.png" alt="Mattermost"  loading="lazy" /></p>
<h3>There, and Back Again<span class="hx:absolute hx:-mt-20" id="there-and-back-again"></span>
    <a href="#there-and-back-again" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>We moved from Slack to Rocket.chat and a couple of days later we are moving again, now to Mattermost.</p>
<p>Yes, this was cumbersome. My team was not very happy about leaving Slack. Slack really is slick, full-featured, good-looking, well-rounded, a proper web product for this generation. Any alternative should be at least almost as good-looking, and at least have bug-free features, including webhooks.</p>
<p>Mattermost fits the bill almost perfectly but the open sourced Team Edition lacked one important feature for me: the ability to disallow normal members to rename and/or delete private groups. Yes, we expect grown-ups to behave, but when you have remote teams, remote clients, external users with no commitment to the company, it&rsquo;s a hassle.</p>
<p>Yes, I could and I probably should use the paid Enterprise Edition. But for just that small feature, it felt too expensive. So that triggered me to let it go and install Rocket.chat instead. I moved my entire team there (almost 50 people, because I didn&rsquo;t move the clients yet). That would be the end of the story.</p>
<p>But Rocket.chat has a complex infrastructure to deal with (you must have at the very least 3 boxes or pay extra for a proper Mongodb SaaS). Actually, <a href="http://www.akitaonrails.com/en/2016/08/09/moving-away-from-slack-into-rocket-chat-good-enough"target="_blank" rel="noopener">in my previous post</a> I explained why you MEAN guys should not be careless dealing with Mongodb. In a nutshell: Mongodb was not meant to run in a single instance, you must have a replica set. If you have a single instance Mongodb, you&rsquo;re doing it wrong.</p>
<p>And the most problematic: the client-side is just too heavy. It frequently spikes out the CPU in not so powerful machines. It&rsquo;s noticeably and measurably slower to navigate its UI, compared to Slack. MatterMost&rsquo;s UI was much faster and way more responsive.</p>
<p>I was almost willing to overlook the lack of a proper test suite. I was willing to try to live with Meteor and CoffeeScript. I was willing to deal with the complex MongoDB maintenance.</p>
<p>But slow responsiveness across many of the members of my team is a no-go, a big show-stopper. We turned the beta videochat feature off (as it always triggers CPU spikes across all users), but many members still had a bad experience with a UI that was too slow and a resource hog.</p>
<p>The React-based, ES6-written, properly structured - with good enough client-side test suites - MatterMost was a more suitable candidate. So I decided to really think about the original problem and I came up with a simple solution: <a href="http://www.akitaonrails.com/en/2016/08/12/hackeando-o-mattermost-team-edition"target="_blank" rel="noopener">add a simple PLPGSQL function</a> to be triggered whenever someone tried to delete a channel. Sure enough, it worked. And that prompted me to call my team again and propose this new change: I believe everybody was on-board as MatterMost was way faster on their machines.</p>
<p>I know I am sounding really harsh towards Rocket.chat, and it&rsquo;s really not my intention. If we didn&rsquo;t have any other options, we would still move to Rocket.chat. But as Mattermost proved to be the better choice, it was a no-brainer.</p>
<h3>MatterMost Install<span class="hx:absolute hx:-mt-20" id="mattermost-install"></span>
    <a href="#mattermost-install" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>As always, I will not bore you with instructions that are already available online. If you want to install everything manually, follow <a href="https://docs.mattermost.com/install/prod-ubuntu.html"target="_blank" rel="noopener">this tutorial</a>, but a better option would be <a href="https://docs.mattermost.com/install/prod-docker.html"target="_blank" rel="noopener">the Docker-based deployment</a>. You can even install/upgrade it <a href="http://docs.gitlab.com/omnibus/gitlab-mattermost/"target="_blank" rel="noopener">together with GitLab</a>.</p>
<p>Make sure you have NGINX in front of the server and that you have both a properly registered domain or sub-domain, and that you have SSL - use Let&rsquo;s Encrypt.</p>
<p>Because I want to keep tweaking and experimenting with the codebase in a live installation, I installed everything manually and I have this directory structure:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>-rw-r--r--  1 mattermost mattermost     6504 Aug 13 20:11 config.json
drwxrwxr-x  5 mattermost mattermost     4096 Aug 13 20:30 data
lrwxrwxrwx  1 mattermost mattermost       33 Aug 13 20:13 mattermost -&gt; mattermost-team-3.3.0-linux-amd64
drwxr-xr-x  9 mattermost mattermost     4096 Aug 13 20:28 mattermost-team-3.2.0-linux-amd64
-rw-rw-r--  1 mattermost mattermost 19968308 Jul 14 16:37 mattermost-team-3.2.0-linux-amd64.tar.gz
drwxr-xr-x 11 mattermost mattermost     4096 Aug 13 20:28 mattermost-team-3.3.0-linux-amd64
-rw-rw-r--  1 mattermost mattermost 20241448 Aug 12 20:41 mattermost-team-3.3.0-linux-amd64.tar.gz
drwxrwxr-x  3 mattermost mattermost     4096 Aug 13 20:23 platform-dev-3.3.0
-rw-r--r--  1 mattermost mattermost 18060203 Aug 13 20:22 platform-dev.tar.gz</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>So I have a restricted, sudo user <code>mattermost</code> and I have a main <code>mattermost</code> directory pointing to the binary packages you will find in the <a href="https://www.mattermost.org/download/"target="_blank" rel="noopener">official download page</a>.</p>
<p>Notice that I have a copy of <code>mattermost/config/config.json</code> in the home directory. I leave it there so every time I download a new version and redo the symlink to <code>mattermost</code>, I can just do:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>rm -Rf ~/mattermost/config/config.json
cd ~/mattermost/config
ln -s ~/mattermost/config.json</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Also make sure you change at least the following in the config:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>...
&#34;FileSettings&#34;: {
    &#34;MaxFileSize&#34;: 83886080,
    &#34;DriverName&#34;: &#34;local&#34;,
    &#34;Directory&#34;: &#34;/home/mattermost/data&#34;,
...</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>If you want, you can set file uploads to go to some S3 bucket you have, and just fill in the AWS details. But if you choose to have them locally, change the directory to somewhere outside of the <code>mattermost</code> folder, as on every upgrade you will change the folder. With both AWS EC2 or Digital Ocean you can always choose to add a secondary volume that can outlive the virtual boxes, so even if you get to a point where you have hundreds of concurrent users and you want to scale horizontally, you can have all your boxes pointing to a shared volume (AWS EFS, for example).</p>
<p>Speaking of which, in this configuration, upgrading would be like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>sudo service mattermost stop
wget https://releases.mattermost.com/x.y.z/mattermost-team-x.y.z-linux-amd64.tar.gz
tar xvfz mattermost-team-x.y.z-linux-amd64.tar.gz
mv mattermost mattermost-team-x.y.z-linux-amd64
ln -s mattermost-team-x.y.z-linux-amd64 mattermost
rm -Rf mattermost/config/config.json
rm -Rf mattermost/data
cd mattermost/config
ln -s ~/config.json
cd ..
ln -s ~/data
sudo service mattermost start</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>The reason I wanted to have it this way is because I can tweak the code and manually push the changes.</p>
<p>For your development machine you should follow <a href="https://docs.mattermost.com/developer/developer-setup.html"target="_blank" rel="noopener">this instruction</a>. If you&rsquo;re in OS X and you choose to use Docker Toolbox, remember that you don&rsquo;t need VirtualBox anymore as it will use OS X&rsquo;s native hypervisor now. On my machine, I had to add <code>dockerhost</code> manually in my <code>/etc/hosts</code> because <code>boot2docker</code> was failing to get my ip address.</p>
<p>Then you can just clone the code from Github:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>mkdir mattermost
cd mattermost
git clone https://github.com/mattermost/platform
git checkout -b v3.3.0 v3.3.0</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Remember to always checkout the correct stable version (v3.3.0 as of the time when I originally posted this article) that you have installed in your server. Again, I will not bore you with what&rsquo;s already documented in the links above, but you must have Go 1.6(.3), Docker, Docker-Composer, Docker-Machine all installed already.</p>
<p>You can execute <code>make run</code> and after it finishes (and as usual, npm will make sure it takes a very long time) you can open <code>http://localhost:8065</code> to play with it locally.</p>
<p>Most importantly: you can tweak the React JSX components from <code>platform/webapp/components</code>, for example, the <code>channel_header.jsx</code> and add stuff like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="err">#</span> <span class="nx">line</span> <span class="mi">493</span> <span class="nx">from</span> <span class="nx">v3</span><span class="p">.</span><span class="mf">3.0</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span><span class="p">(</span><span class="nx">isAdmin</span> <span class="o">||</span> <span class="nx">isSystemAdmin</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">dropdownContents</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="o">&lt;</span><span class="nx">li</span>
</span></span><span class="line"><span class="cl">            <span class="nx">key</span><span class="o">=</span><span class="s1">&#39;rename_channel&#39;</span>
</span></span><span class="line"><span class="cl">            <span class="nx">role</span><span class="o">=</span><span class="s1">&#39;presentation&#39;</span>
</span></span><span class="line"><span class="cl">        <span class="o">&gt;</span>
</span></span><span class="line"><span class="cl">            <span class="o">&lt;</span><span class="nx">a</span>
</span></span><span class="line"><span class="cl">                <span class="nx">role</span><span class="o">=</span><span class="s1">&#39;menuitem&#39;</span>
</span></span><span class="line"><span class="cl">                <span class="nx">href</span><span class="o">=</span><span class="s1">&#39;#&#39;</span>
</span></span><span class="line"><span class="cl">                <span class="nx">onClick</span><span class="o">=</span><span class="p">{</span><span class="k">this</span><span class="p">.</span><span class="nx">showRenameChannelModal</span><span class="p">}</span>
</span></span><span class="line"><span class="cl">            <span class="o">&gt;</span>
</span></span><span class="line"><span class="cl">                <span class="o">&lt;</span><span class="nx">FormattedMessage</span>
</span></span><span class="line"><span class="cl">                    <span class="nx">id</span><span class="o">=</span><span class="s1">&#39;channel_header.rename&#39;</span>
</span></span><span class="line"><span class="cl">                    <span class="nx">defaultMessage</span><span class="o">=</span><span class="s1">&#39;Rename {term}...&#39;</span>
</span></span><span class="line"><span class="cl">                    <span class="nx">values</span><span class="o">=</span><span class="p">{{</span>
</span></span><span class="line"><span class="cl">                        <span class="nx">term</span><span class="o">:</span> <span class="p">(</span><span class="nx">channelTerm</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">                    <span class="p">}}</span>
</span></span><span class="line"><span class="cl">                <span class="o">/&gt;</span>
</span></span><span class="line"><span class="cl">            <span class="o">&lt;</span><span class="err">/a&gt;</span>
</span></span><span class="line"><span class="cl">        <span class="o">&lt;</span><span class="err">/li&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">ChannelStore</span><span class="p">.</span><span class="nx">isDefault</span><span class="p">(</span><span class="nx">channel</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nx">dropdownContents</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="nx">deleteOption</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>And you know what this will do? Remove the &ldquo;Rename Group&rdquo; and &ldquo;Delete Group&rdquo; options from the channel menu if the user is not a system admin. Now how do you put this in your server?</p>
<p>First of all, you have to edit the <code>Makefile</code> at this section:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>@# Make osx package
@# Copy binary
cp $(GOPATH)/bin/darwin_amd64/platform $(DIST_PATH)/bin
@# Package
tar -C dist -czf $(DIST_PATH)-$(BUILD_TYPE_NAME)-osx-amd64.tar.gz mattermost
@# Cleanup
rm -f $(DIST_PATH)/bin/platform

@# Make windows package
@# Copy binary
cp $(GOPATH)/bin/windows_amd64/platform.exe $(DIST_PATH)/bin
@# Package
tar -C dist -czf $(DIST_PATH)-$(BUILD_TYPE_NAME)-windows-amd64.tar.gz mattermost
@# Cleanup
rm -f $(DIST_PATH)/bin/platform.exe

@# Make linux package
@# Copy binary
cp $(GOPATH)/bin/platform $(DIST_PATH)/bin
@# Package
tar -C dist -czf $(DIST_PATH)-$(BUILD_TYPE_NAME)-linux-amd64.tar.gz mattermost
@# Don&#39;t cleanup linux package so dev machines will have an unziped linux package avalilable
@#rm -f $(DIST_PATH)/bin/platform</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Notice that the developer that made this file is probably using Linux. That&rsquo;s because this file builds the Linux-ELF binary as <code>bin/platform</code>, while the OS X Mach-O binary will be at <code>bin/darwin_amd64/platform</code>. Now, if like me you&rsquo;re on OS X, you have to invert this and make the Linux version point to <code>bin/linux_amd64/platform</code>, otherwise the packages will have the wrong binary.</p>
<p>Then you will notice that there is this section right at the bottom:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>setup-mac:
    echo $$(boot2docker ip 2&gt; /dev/null) dockerhost | sudo tee -a /etc/hosts</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>If you&rsquo;re on the newest versions of Docker-Toolbox, you don&rsquo;t need boot2docker anymore, it will use docker-machine instead. So this line doesn&rsquo;t work and the easiest way to make <code>make test</code> run is to change the <code>dockerhost</code> to <code>localhost</code> in the <code>config/config.json</code>&rsquo;s PostgreSQL configuration.</p>
<p>If the tests are all passing, you can now run <code>make package</code> to make all packages, then you can send the <code>dist/mattermost-team-edition-linux-amd64.tar.gz</code> to your server and uncompress to the correct location.</p>
<p>As a sidenote for Go noobies like myself, I had my <code>GOPATH</code> pointed to <code>/Users/akitaonrails/.go</code> and my project was at <code>/Users/akitaonrails/codeminer42/mattermost/platform</code>. I was told that this is incorrect and the source of my many hours of frustration. The project MUST BE INSIDE THE GOPATH.</p>
<p>So I changed my <code>GOPATH</code> to <code>/Users/akitaonrails/Sites/go</code> and the project to <code>/Users/akitaonrails/Sites/go/src/github.com/mattermost/platform</code> and now it works. I would probably have symlinked the project path to be inside the GOPATH as well. But now everything compiles and runs just fine.</p>
<p>This is how I will keep experimenting for the time being, until I feel that I am comfortable enough to automate the whole process later. This should work just fine for my team for the time being.</p>
<p>Of course, the snippet above is nothing but a <strong>dirty hack</strong>, don&rsquo;t try to contribute like this. A proper implementation would require me to at least create a new setting into the <code>config.json</code>, for example <code>TeamSettings.MembersCanManageChannel</code> to <code>false</code>, and change the <code>api/context.go</code> around line 347 to be something like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">func</span> <span class="p">(</span><span class="n">c</span> <span class="o">*</span><span class="no">Context</span><span class="p">)</span> <span class="no">HasPermissionsToTeam</span><span class="p">(</span><span class="n">teamId</span> <span class="n">string</span><span class="p">,</span> <span class="n">where</span> <span class="n">string</span><span class="p">)</span> <span class="n">bool</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">c</span><span class="o">.</span><span class="n">IsSystemAdmin</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="kp">true</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="o">!</span><span class="n">utils</span><span class="o">.</span><span class="n">cfg</span><span class="o">.</span><span class="n">TeamSettings</span><span class="o">.</span><span class="n">MembersCanManageChannel</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="kp">false</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="o">...</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Then change the <code>webapp/channel_header.jsx</code> component in the React front-end (as well as proper unit tests to <code>webapp/tests/client_channel.test.jsx</code>), make sure the <code>make test</code> passes, and then finally create a feature request to the core team.</p>
<p>But the idea here was just to show that it was not so difficult to solve the show-stopper for my particular scenario, both using the <a href="http://www.akitaonrails.com/en/2016/08/12/hackeando-o-mattermost-team-edition"target="_blank" rel="noopener">SQL trigger</a> to safeguard the database and the hack to fix the Web UI.</p>
<h3>Conclusion<span class="hx:absolute hx:-mt-20" id="conclusion"></span>
    <a href="#conclusion" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>But what about the most important feature of all? RightGIF support? There is nothing as simple as a rightgif slack command just yet. Fortunately you can compensate most missing niceties like this by installing a <a href="https://www.npmjs.com/package/hubot-mattermost"target="_blank" rel="noopener">Hubot server</a>, and linking it to a user so you can chat with the bot and make it do things for you (set an alarm, fetch a gif, translate text, etc). As a caveat, the hubot adapter requires the use of the <a href="https://github.com/loafoe/mattermost-client"target="_blank" rel="noopener">mattermost-client</a> which must be synced with the server platform version releases to work properly, so be careful when you&rsquo;re upgrading.</p>
<p>Overall, Mattermost is a great choice. It&rsquo;s not for amateurs either, the development environment requires you to know your Docker stuff. It requires you to know proper Go-lang configuration. It requires you to follow <a href="https://docs.mattermost.com/developer/contribution-guide.html"target="_blank" rel="noopener">proper procedures to contribute</a>, as they should. The project itself is a single codebase divided into roughly 2 applications, a Go-based HTTP API and a React-based front-end to consume the APIs. Everything about the project is automated through the proper usage of Docker images (for mysql, postgresql, openldap instances) and Makefiles to run the test suite, create packages, etc.</p>
<p>And it has some conveniences from Slack that Rocket.chat doesn&rsquo;t have yet such as a simple shortcut to switch channels (Cmd-K), the ability to reply to messages and organize them as threads, proper and more complete Markdown support. Overall, the features are well-rounded, and not half-baked. What is there is solid and works, it&rsquo;s always bad to have half-finished features.</p>
<p>With these hacks in mind I can strongly recommend that you use Mattermost. And as I said in previous posts, it&rsquo;s not just a matter of cost. If you&rsquo;re a small team, without internal developers or someone that can maintain your own installation, you should definitely pay Mattermost for the Enterprise support, it&rsquo;s affordable and way cheaper than Slack.</p>
<p>For now, it&rsquo;s the better choice, both in terms of overall fit-and-finish, well-rounded features, simple and responsive UI, good code quality and overall care on the engineering.</p>
]]></content:encoded><category>reviews</category><category>containers</category></item></channel></rss>