<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/tags/containers/</link><description>Blog do Fabio Akita do Canal do YouTube 'Akitando' falando sobre tecnologia, carreira e coisas geek.</description><generator>Hugo -- gohugo.io</generator><language>pt-BR</language><lastBuildDate>Mon, 19 Oct 2026 23:20:48 +0000</lastBuildDate><atom:link href="https://akitaonrails.github.io/tags/containers/index.xml" rel="self" type="application/rss+xml"/><item><title>AI-Jail: update de segurança, Docker vira opt-in</title><link>https://akitaonrails.github.io/2026/07/25/ai-jail-update-seguranca-docker-opt-in/</link><guid isPermaLink="true">https://akitaonrails.github.io/2026/07/25/ai-jail-update-seguranca-docker-opt-in/</guid><pubDate>Sat, 25 Jul 2026 13:00:00 -0300</pubDate><description>&lt;p&gt;Hoje de manhã abriram um issue importante no repositório do &lt;a href="https://github.com/akitaonrails/ai-jail"target="_blank" rel="noopener"&gt;ai-jail&lt;/a&gt;: o &lt;a href="https://github.com/akitaonrails/ai-jail/issues/88"target="_blank" rel="noopener"&gt;issue #88&lt;/a&gt;, reportado pelo &lt;a href="https://github.com/mdindoffer"target="_blank" rel="noopener"&gt;@mdindoffer&lt;/a&gt;, com o título &amp;ldquo;Sandbox escape via a docker socket passthrough (effective host root)&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;O report procedia, e a correção já está disponível na &lt;strong&gt;v1.16.0&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;Atualize pra v1.16.0&lt;span class="hx:absolute hx:-mt-20" id="atualize-pra-v1160"&gt;&lt;/span&gt;
&lt;a href="#atualize-pra-v1160" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Se você usa o ai-jail, o update é o de sempre:&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;O que mudou na v1.16.0&lt;span class="hx:absolute hx:-mt-20" id="o-que-mudou-na-v1160"&gt;&lt;/span&gt;
&lt;a href="#o-que-mudou-na-v1160" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Até a v1.15.x, o ai-jail montava o socket do Docker dentro da jaula automaticamente, sempre que &lt;code&gt;/var/run/docker.sock&lt;/code&gt; existia no host. Read-write. Sem aviso, sem flag, sem pedir opinião. Eu documentei isso no README como &amp;ldquo;favorece usabilidade&amp;rdquo;, e era verdade: agente de coding frequentemente precisa rodar um &lt;code&gt;docker compose&lt;/code&gt; pra subir banco de testes, e o passthrough automático poupava configuração.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Hoje de manhã abriram um issue importante no repositório do <a href="https://github.com/akitaonrails/ai-jail"target="_blank" rel="noopener">ai-jail</a>: o <a href="https://github.com/akitaonrails/ai-jail/issues/88"target="_blank" rel="noopener">issue #88</a>, reportado pelo <a href="https://github.com/mdindoffer"target="_blank" rel="noopener">@mdindoffer</a>, com o título &ldquo;Sandbox escape via a docker socket passthrough (effective host root)&rdquo;.</p>
<p>O report procedia, e a correção já está disponível na <strong>v1.16.0</strong>.</p>
<h2>Atualize pra v1.16.0<span class="hx:absolute hx:-mt-20" id="atualize-pra-v1160"></span>
    <a href="#atualize-pra-v1160" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Se você usa o ai-jail, o update é o de sempre:</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>O que mudou na v1.16.0<span class="hx:absolute hx:-mt-20" id="o-que-mudou-na-v1160"></span>
    <a href="#o-que-mudou-na-v1160" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Até a v1.15.x, o ai-jail montava o socket do Docker dentro da jaula automaticamente, sempre que <code>/var/run/docker.sock</code> existia no host. Read-write. Sem aviso, sem flag, sem pedir opinião. Eu documentei isso no README como &ldquo;favorece usabilidade&rdquo;, e era verdade: agente de coding frequentemente precisa rodar um <code>docker compose</code> pra subir banco de testes, e o passthrough automático poupava configuração.</p>
<p>A partir da v1.16.0 o comportamento inverteu:</p>
<ul>
<li>O passthrough do socket ficou <strong>desligado por padrão</strong>. Só entra se você pedir explicitamente com a flag <code>--docker</code> ou com <code>no_docker = false</code> no <code>.ai-jail</code>.</li>
<li>Quando você liga e existe um socket no host, o ai-jail imprime um aviso na largada deixando claro que aquilo equivale a dar root no host pro processo dentro da jaula.</li>
<li>O <code>ai-jail status</code> agora mostra Docker como <code>disabled (default)</code>, pra ninguém achar que ligou sem querer.</li>
<li>Nos modos <code>--lockdown</code> e browser profile o socket nunca entra, como já era antes.</li>
</ul>
<p>Mudança de comportamento, sim, e de propósito. Se o seu workflow depende de Docker dentro da jaula (o meu depende em alguns projetos), o opt-in é uma linha no <code>.ai-jail</code> do projeto:</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>O nome do campo ficou feio (<code>no_docker = false</code> pra ligar, eu sei), mas configs antigas continuam parseando igual e o <code>--no-docker</code> / <code>no_docker = true</code> funcionam como antes. Só o default mudou. Os detalhes estão nas <a href="https://github.com/akitaonrails/ai-jail/blob/master/releases/v1.16.0.md"target="_blank" rel="noopener">release notes da v1.16.0</a>.</p>
<h2>O que o issue #88 mostrou<span class="hx:absolute hx:-mt-20" id="o-que-o-issue-88-mostrou"></span>
    <a href="#o-que-o-issue-88-mostrou" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O report do mdindoffer é daqueles que todo mantenedor quer receber: resumo preciso, repro em poucas linhas, proposta de correção. A essência:</p>
<p>O ai-jail montava o socket <strong>cru</strong> do Docker dentro da sandbox, read-write. O daemon do Docker roda como root no host. Logo, um agente dentro da jaula podia rodar isto:</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>E pronto. Leitura de qualquer arquivo do host, como root. Isso derrota de uma vez só o <code>$HOME</code> em tmpfs, o <code>--mask</code>, o <code>--deny-path</code> e o Landlock, porque a ação deixa de acontecer dentro da sandbox: acontece no daemon, que vive fora e acima de qualquer namespace que o bwrap criou. O agente nem precisa escapar da jaula quando a jaula tem uma porta que dá direto na sala de máquinas.</p>
<p>Em retrospecto, deixar isso ligado por padrão foi uma falha de design. Eu sabia que o passthrough era &ldquo;perigoso&rdquo; no abstrato, tanto que escrevi isso no README. O que eu não tinha internalizado: perigoso desse jeito, com esse default, já é vulnerabilidade.</p>
<p>O mdindoffer ainda propôs o caminho de hardening definitivo: em vez de montar o socket cru, interpor um proxy filtrado (estilo o <a href="https://github.com/wollomatic/socket-proxy"target="_blank" rel="noopener">wollomatic/socket-proxy</a>) que só aceita bind mounts de caminhos que o agente já pode escrever dentro da jaula. Está no radar pra uma versão futura. Pra fechar o buraco agora, opt-in com aviso explícito resolve o default, que era onde o problema morava.</p>
<h2>A classe de vulnerabilidade: docker.sock é root<span class="hx:absolute hx:-mt-20" id="a-classe-de-vulnerabilidade-dockersock-é-root"></span>
    <a href="#a-classe-de-vulnerabilidade-dockersock-%c3%a9-root" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Essa não é a primeira vez que eu esbarro nessa história, e aposto que também não é a sua. &ldquo;Quem tem acesso ao socket do Docker tem root no host&rdquo; é um dos clássicos da segurança de containers, documentado pela própria Docker na página de <a href="https://docs.docker.com/engine/security/"target="_blank" rel="noopener">daemon attack surface</a>: só usuários confiáveis devem controlar o daemon, porque o Docker permite compartilhar qualquer diretório do host com um container, sem restrição nenhuma de acesso.</p>
<p>O motivo técnico é simples. O <code>dockerd</code> é um daemon que roda como root e obedece comandos que chegam pela API no socket Unix <code>/var/run/docker.sock</code>. A CLI <code>docker</code> é só um cliente dessa API. Quando você pede <code>docker run -v /:/host</code>, quem cria o container e monta o filesystem inteiro do host dentro dele é o daemon, com privilégio total. E processo dentro de container roda como uid 0 por padrão, que o kernel enxerga como uid 0 de verdade (salvo user namespace remapping, que quase ninguém liga). A conta fecha: acesso de escrita no socket equivale a root no host. O grupo <code>docker</code> é sudo sem senha com outro nome.</p>
<h2>A demonstração, na sua máquina<span class="hx:absolute hx:-mt-20" id="a-demonstração-na-sua-máquina"></span>
    <a href="#a-demonstra%c3%a7%c3%a3o-na-sua-m%c3%a1quina" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Se você tem Docker instalado e seu usuário no grupo <code>docker</code>, reproduz agora. Sem sudo, sem explorar bug nenhum:</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"># confirme que você é um usuário comum</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"># tente ler o /etc/shadow diretamente: negado, como esperado</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"># agora peça pro daemon fazer isso por você</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>E se quiser o pacote completo, uma shell de root no seu próprio 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>Nenhum exploit, nenhum 0-day. Você usou a API oficial, do jeito documentado, e saiu de usuário comum pra root em um comando. Era exatamente isso que um agente dentro do ai-jail conseguia fazer até hoje, mesmo com todas as camadas (bwrap, Landlock, seccomp, rlimits) ligadas.</p>
<h2>Por que isso ainda existe?<span class="hx:absolute hx:-mt-20" id="por-que-isso-ainda-existe"></span>
    <a href="#por-que-isso-ainda-existe" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Se todo mundo sabe disso há mais de uma década, por que ninguém &ldquo;consertou&rdquo;? A resposta curta: isso é arquitetura, e arquitetura não se resolve com patch.</p>
<ul>
<li>O daemon root com API todo-poderosa foi o design que fez o Docker simples de operar. Autorização granular por requisição existe na forma de <a href="https://docs.docker.com/engine/extend/plugins_authorization/"target="_blank" rel="noopener">authorization plugins</a>, mas é opt-in, chata de configurar, e eu conto nos dedos os setups que já vi usando.</li>
<li>O <a href="https://docs.docker.com/engine/security/userns-remap/"target="_blank" rel="noopener">userns-remap</a> existe desde o Docker 1.10 e mapeia o root do container pra um usuário sem privilégio no host. Vem desligado de fábrica, porque quebra compatibilidade com imagens e volumes que assumem uid 0.</li>
<li>O <a href="https://docs.docker.com/engine/security/rootless/"target="_blank" rel="noopener">rootless mode</a> roda o daemon inteiro como o seu usuário, com o socket em <code>$XDG_RUNTIME_DIR/docker.sock</code>. Funciona, mas tem restrições de rede e storage, e a internet inteira de tutoriais assume o daemon root no caminho clássico.</li>
<li>O <a href="https://podman.io/"target="_blank" rel="noopener">Podman</a> nasceu rootless e sem daemon justamente por causa dessa crítica. Tem uma seção inteira sobre ele logo abaixo.</li>
</ul>
<p>Resumindo: isso vai continuar existindo. Toda ferramenta que monta <code>/var/run/docker.sock</code> dentro de um ambiente &ldquo;pra conveniência&rdquo; abre o mesmo buraco, consciente ou não. CI que monta o socket pra build de imagem, IDE remota, code-server, sandbox de agente de IA (oi, eu), plugin de painel web. O ajuste fica sempre do lado de quem monta o ambiente.</p>
<h2>Curiosidade: isso é tudo, menos novo<span class="hx:absolute hx:-mt-20" id="curiosidade-isso-é-tudo-menos-novo"></span>
    <a href="#curiosidade-isso-%c3%a9-tudo-menos-novo" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Se você me acompanha há algum tempo, essa história toda deve ter dado déjà vu. Em 2023 eu gravei o <a href="/2023/03/02/akitando-139-entendendo-como-containers-funcionam/">[Akitando #139] - Entendendo Como Containers Funcionam</a>, onde eu explico o que um container realmente é: um processo comum do Linux, limitado por cgroups, enganado por namespaces, com as capabilities cortadas. Sem mágica e sem máquina virtual. E olha o que já estava na lista de links daquele episódio: um walkthrough de <a href="https://flast101.github.io/docker-privesc/"target="_blank" rel="noopener">privilege escalation via Docker</a>, demonstrando exatamente o truque do <code>docker run -v /:/host</code>. O buraco que o issue #88 explorou dentro do ai-jail é o mesmo que eu já apontava num video de 2023, e que já era manjado muito antes disso.</p>
<p>Ninguém sabe disso melhor que a Red Hat. O <a href="https://podman.io/"target="_blank" rel="noopener">Podman</a> nasceu lá em 2018 como resposta direta a essa arquitetura: sem daemon central, rootless por padrão. Cada container vira filho direto do seu usuário, via fork-exec, sem um processo todo-poderoso rodando como root intermediando nada. O &ldquo;docker.sock é root&rdquo; simplesmente não existe nesse modelo, porque não existe nem docker.sock, nem daemon, nem root.</p>
<p>E antes que você pergunte: sim, eu uso Podman em algumas coisas e recomendo. Mas ele também não é solução perfeita, e vale entender o porquê.</p>
<p>O que o Podman faz bem:</p>
<ul>
<li><strong>A arquitetura certa.</strong> Daemonless e rootless desde o dia zero. A classe inteira de vulnerabilidade deste artigo perde o sentido.</li>
<li><strong>Compatibilidade de CLI.</strong> <code>alias docker=podman</code> cobre a maioria esmagadora dos comandos do dia a dia. Build, run, push, pull: mesmos comandos, mesmas flags.</li>
<li><strong>Integração com systemd.</strong> Os <a href="https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html"target="_blank" rel="noopener">Quadlets</a> são, na minha opinião, o jeito mais limpo de rodar container como serviço no Linux. O Docker nunca chegou perto disso.</li>
</ul>
<p>E onde ele tropeça:</p>
<ul>
<li><strong>O socket de compatibilidade não é 100%.</strong> O Podman oferece um socket compatível com a API do Docker (o <code>podman.socket</code>), e muita ferramenta funciona em cima dele. Mas &ldquo;muita&rdquo; não é &ldquo;toda&rdquo;: Testcontainers, alguns plugins de IDE, ferramentas de CI mais exóticas tropeçam em diferenças de comportamento. Funciona até o dia em que não funciona, e aí você perde uma tarde debugando.</li>
<li><strong>Compose é cidadão de segunda classe.</strong> O <code>docker compose</code> oficial até conversa com o socket do Podman, e existe o <code>podman-compose</code>, mas nenhum dos dois tem a mesma redondeza do par original. Projeto com compose complicado é onde a migração costuma enroscar.</li>
<li><strong>Rootless tem preço.</strong> A rede rootless (slirp4netns, e hoje o pasta) tem limitações: sem ping por padrão, IP de origem esquisito, throughput menor. Imagem que assume uid 0 e volume com permissão errada pedem ajuste. Nada grave, mas é atrito.</li>
<li><strong>Docker Desktop é um produto.</strong> No macOS e no Windows, o Docker Desktop entrega uma experiência polida que o Podman Desktop ainda está alcançando. Pra muita gente, esse é o único contato com containers que existe.</li>
</ul>
<p>Somando tudo, você chega na resposta de por que o mundo continua no Docker: inércia de ecossistema. Todo tutorial, todo CI, toda imagem de exemplo, todo <code>docker run</code> colado de Stack Overflow assume o daemon root no caminho clássico. O Docker virou o nome da categoria, tipo Bombril. Migrar pro Podman é tecnicamente fácil e politicamente caro: é você contra o conhecimento acumulado da internet inteira.</p>
<p>Um detalhe que importa pra quem usa sandbox: montar o socket do Podman rootless dentro de uma jaula é bem menos catastrófico que montar o do Docker. O &ldquo;daemon&rdquo; equivalente roda como o seu usuário, então um agente mal-intencionado ganharia os seus privilégios, não root. Continua ruim (dá pra sobrescrever o seu <code>~/.ssh</code> com eles, por exemplo), mas é outro campeonato. Mesmo assim, o default do ai-jail continua valendo: não montar socket nenhum, de runtime nenhum. Opt-in é opt-in.</p>
<h2>Boas práticas pra isso não morder você<span class="hx:absolute hx:-mt-20" id="boas-práticas-pra-isso-não-morder-você"></span>
    <a href="#boas-pr%c3%a1ticas-pra-isso-n%c3%a3o-morder-voc%c3%aa" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>A lista que eu aplico e recomendo:</p>
<ol>
<li><strong>Trate o grupo <code>docker</code> como sudo sem senha.</strong> Antes de adicionar qualquer usuário ou serviço nele, pergunte se você daria sudo irrestrito pra aquilo. É a mesma coisa.</li>
<li><strong>Nunca monte <code>/var/run/docker.sock</code> em ambientes não confiáveis.</strong> Agente de IA, job de CI que roda código de pull request de estranho, container de terceiro. Faça um <code>grep -r docker.sock</code> nos seus docker-compose, manifests de CI e configs de ferramentas. Aparece em mais lugares do que você lembra.</li>
<li><strong>Precisou expor pra algo semiconfiável? Use um socket proxy.</strong> O <a href="https://github.com/wollomatic/socket-proxy"target="_blank" rel="noopener">wollomatic/socket-proxy</a> e o <a href="https://github.com/Tecnativa/docker-socket-proxy"target="_blank" rel="noopener">docker-socket-proxy da Tecnativa</a> ficam entre o cliente e o daemon com allowlist de endpoints e bloqueio de bind mounts arbitrários. Read-only por padrão, você liga só o que precisa.</li>
<li><strong>Prefira rootless sempre que possível.</strong> Podman rootless no Linux é o caminho mais limpo; o rootless mode do próprio Docker é a segunda opção. O daemon deixa de ser root e essa classe inteira de problema perde a mordida.</li>
<li><strong>Em CI, construa imagem sem daemon privilegiado.</strong> <a href="https://github.com/GoogleContainerTools/kaniko"target="_blank" rel="noopener">Kaniko</a> e <a href="https://buildah.io/"target="_blank" rel="noopener">Buildah</a> constroem imagens rootless, sem precisar montar socket nenhum no job.</li>
<li><strong>Não conseguiu rootless? Ligue o userns-remap.</strong> Custa uma tarde de testes com seus volumes e compra isolamento real entre o root do container e o root do host.</li>
<li><strong>No ai-jail, deixe o default trabalhar por você.</strong> Docker desligado, e <code>--docker</code> só nos projetos onde você confia no workload como confiaria num sudo. Regra prática: se você não daria <code>sudo</code> cego pro agente naquele diretório, também não dê <code>--docker</code>.</li>
</ol>
<h2>Conclusão<span class="hx:absolute hx:-mt-20" id="conclusão"></span>
    <a href="#conclus%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Uma sandbox bem construída perde boa parte do valor se tiver uma porta dos fundos aberta por conveniência. O bwrap, o Landlock, o seccomp e os rlimits do ai-jail continuam fazendo o trabalho deles, mas nenhum deles enxerga o que acontece quando o processo lá dentro pede gentilmente pro daemon root do host montar o filesystem inteiro num container. Camada de segurança que você não audita vira decoração.</p>
<p>Meu agradecimento público ao <a href="https://github.com/mdindoffer"target="_blank" rel="noopener">@mdindoffer</a>: report limpo, repro mínima, severidade correta e ainda por cima proposta de solução. É assim que se reporta vulnerabilidade em projeto open source.</p>
<p>Se você quer o contexto completo de como eu uso sandbox no dia a dia, escrevi sobre isso em <a href="/2026/07/11/como-me-precaver-pros-meus-agentes-nao-apagarem-minhas-coisas/">Como me precaver pros meus agentes não apagarem minhas coisas?</a>. A história do ai-jail está em <a href="/2026/01/10/ai-agents-garantindo-a-protecao-do-seu-sistema/">AI Agents: Garantindo a Proteção do seu Sistema</a> e na <a href="/2026/03/01/ai-jail-sandbox-para-agentes-de-ia-de-shell-script-a-ferramenta-real/">reescrita em Rust</a>.</p>
<p>Agora vai lá e roda o upgrade.</p>
]]></content:encoded><category>ai-jail</category><category>containers</category><category>seguranca</category></item><item><title>Distrobox de Emulação com Claude Code</title><link>https://akitaonrails.github.io/2026/04/11/distrobox-de-emulacao-com-claude-code/</link><guid isPermaLink="true">https://akitaonrails.github.io/2026/04/11/distrobox-de-emulacao-com-claude-code/</guid><pubDate>Sat, 11 Apr 2026 18:00:00 -0300</pubDate><description>&lt;p&gt;Eu gosto de videogame velho desde antes de muita gente aqui nascer. Meu primeiro contato foi ainda na era do Atari, no começo dos anos 80. Depois vieram os micros de 8 bits, os arcades dos anos 90, os consoles de 16, 32, 64 bits, e eu continuei acompanhando tudo. Nostalgia, pra mim, não é pasta bonitinha no Instagram. É acervo mesmo. ROM, BIOS, dumps, patches, DLC, firmware, save. Ao longo dos anos fui guardando tudo no meu NAS. Hoje tenho terabytes disso em &lt;code&gt;/mnt/terachad/Emulators&lt;/code&gt;.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Eu gosto de videogame velho desde antes de muita gente aqui nascer. Meu primeiro contato foi ainda na era do Atari, no começo dos anos 80. Depois vieram os micros de 8 bits, os arcades dos anos 90, os consoles de 16, 32, 64 bits, e eu continuei acompanhando tudo. Nostalgia, pra mim, não é pasta bonitinha no Instagram. É acervo mesmo. ROM, BIOS, dumps, patches, DLC, firmware, save. Ao longo dos anos fui guardando tudo no meu NAS. Hoje tenho terabytes disso em <code>/mnt/terachad/Emulators</code>.</p>
<p>No meu canal eu inclusive usei jogos antigos pra ensinar fundamentos de computação. No <a href="https://www.youtube.com/watch?v=hYJ3dvHjeOE&amp;pp=ygUUYWtpdGFuZG8gc3VwZXIgbWFyaW8%3D"target="_blank" rel="noopener">Akitando sobre Super Mario e computadores antigos</a> eu explico 6502, mapa de memória, PPU, limitações de hardware e por que aqueles jogos eram programados daquele jeito. Se você nunca viu, recomendo assistir:</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>O problema é o de sempre: a cada poucos anos eu resolvo montar uma máquina nova de emulação, baixo de novo os emuladores principais da vez, e lá vou eu repetir o mesmo ritual masoquista. <code>PCSX2</code>, <code>RPCS3</code>, <code>Eden</code>, <code>Azahar</code>, <code>Dolphin</code>, <code>RetroArch</code>, <code>Flycast</code>, <code>shadPS4</code> e por aí vai. Cada um com suas manias. Cada um com sua forma particular de te fazer perder horas.</p>
<p>Em teoria, montar tudo isso deveria ser divertido. Na prática, leva dias. E não é exagero. <code>Dolphin</code> ainda consegue ser um dos piores porque controle de GameCube e Wii não tem nada de padrão. <code>RPCS3</code> exige tuning por jogo. <code>Eden</code> precisa de DLC, update e cheats no lugar certo. <code>shadPS4</code> é um festival de tentativa e erro. Quando eu finalmente terminava de configurar tudo, eu já não queria mais jogar porcaria nenhuma. Eu só queria fechar os menus e ir fazer outra coisa.</p>
<p>Eu já cheguei a dizer que talvez a graça estivesse justamente no tuning. Não acho mais isso. Depois de repetir esse processo vezes demais, eu cansei. Eu quero jogar os jogos, não preencher formulário escondido em GUI de emulador.</p>
<h2>O problema não era Linux<span class="hx:absolute hx:-mt-20" id="o-problema-não-era-linux"></span>
    <a href="#o-problema-n%c3%a3o-era-linux" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Eu já resolvi esse tipo de coisa de várias formas no passado. Anos atrás eu rodava Linux no host e um Windows virtualizado com GPU passthrough pra jogar dentro da VM. Na época era o que fazia sentido. Eu fiz um vídeo inteiro sobre isso:</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>Mas isso foi antes do trabalho monumental da Valve, do Proton, do Wine moderno e de toda a comunidade open source que empurrou o desktop Linux pra um lugar muito melhor. Hoje dá pra evitar Windows na maior parte dos casos. No outro vídeo abaixo eu explico a evolução de CPU, GPU, OpenGL, Vulkan e por que chegamos nesse ponto:</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>Meu mini-PC dedicado a games com RTX 4090 continua sendo minha principal máquina de Steam, especialmente agora que montei um <a href="/2026/04/01/meu-cockpit-de-sim-racing-formula-fx1/">cockpit decente de sim racing</a>. Nele eu uso <a href="https://www.emudeck.com/"target="_blank" rel="noopener">EmuDeck</a>, que nasceu justamente pra automatizar instalação e configuração de emuladores no Steam Deck, depois passou a suportar Windows também, e ajuda bastante a reduzir a complicação de montar esse tipo de ambiente. Mas meu desktop principal também é forte demais pra ficar subutilizado. Ele tem uma RTX 5090, e hoje eu uso essa GPU principalmente pra testar LLMs e benchmarking, como contei em <a href="/2026/04/05/testando-llms-open-source-e-comerciais-quem-consegue-bater-o-claude-opus/">Testando LLMs Open Source e Comerciais</a>. Seria um desperdício não usar isso pra gaming também.</p>
<p>Só que no Linux eu queria outra coisa. Eu não queria uma caixa-preta fazendo tudo por mim sem eu saber exatamente o que estava sendo alterado, principalmente no meu PC principal, que é minha máquina de trabalho. Eu queria um setup meu, feito &ldquo;na mão&rdquo; no sentido certo da expressão: não clicar GUI por GUI, mas entender o que está sendo configurado, manter os arquivos sob meu controle, e conseguir reconstruir tudo do jeito que eu gosto. Eu sei que o pessoal do NixOS vai pular aqui pra dizer &ldquo;é só usar Nix&rdquo; — explico mais abaixo por que descartei essa rota. Também não queria transformar a minha máquina de trabalho num carnaval de pacote de emulador, tema GTK, wrapper, launcher, runtime esquisito e config enterrada em <code>~/.config</code>. Nem queria dual boot ou VM. Em 2026, a melhor resposta pra isso, na minha opinião, é <a href="https://distrobox.it/"target="_blank" rel="noopener">Distrobox</a>.</p>
<p>Pelo que o próprio projeto explica, Distrobox é um wrapper em cima de <code>podman</code>, <code>docker</code> ou <code>lilipod</code> pra criar containers fortemente integrados ao host, com acesso a <code>HOME</code>, Wayland/X11, áudio, dispositivos USB, storage externo e GPU. Ou seja: exatamente o tipo de isolamento pragmático que eu queria. Não é uma fronteira de segurança, e o próprio site deixa isso claro. Não use pensando em sandbox de alta segurança. Use pensando em separar ambientes sem ficar pagando o preço de uma VM.</p>
<p>E antes que alguém repita a confusão de sempre: container não é máquina virtual. Eu explico isso com calma neste outro episódio:</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>O setup<span class="hx:absolute hx:-mt-20" id="o-setup"></span>
    <a href="#o-setup" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>A ideia é simples: um Arch Linux vanilla dentro de um Distrobox chamado <code>gaming</code>, com os caminhos do NAS montados em read-only, a biblioteca Steam acessível onde faz sentido, e todos os emuladores instalados lá dentro. Como o container tem acesso a GPU NVIDIA, áudio, USB e demais periféricos, eu separo trabalho de jogo na mesma máquina sem penalidade prática de performance.</p>
<p>O bootstrap inicial é mais ou menos isso:</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>Só esse começo já tem pegadinha. O <code>archlinux:latest</code> dentro de Distrobox vem sem <code>[multilib]</code> no <code>pacman.conf</code>, com um <code>sudoers</code> mal configurado, e com o velho problema do <code>--nvidia</code>: as libs do driver do host entram bind-mounted read-only, então qualquer instalação que dependa de <code>nvidia-utils</code> quebra com conflito de arquivo. Só isso já é o tipo de dor de cabeça que, alguns anos atrás, me faria abrir meia dúzia de abas, mais uma penca de terminais, e gastar uma noite inteira em tentativa e erro.</p>
<p>Desta vez eu fiz diferente.</p>
<h2>Claude Code como assistente de infraestrutura<span class="hx:absolute hx:-mt-20" id="claude-code-como-assistente-de-infraestrutura"></span>
    <a href="#claude-code-como-assistente-de-infraestrutura" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Eu venho ficando cada vez mais confortável em usar Claude Code como meu assistente de infraestrutura nas minhas máquinas pessoais. Eu não faria isso cegamente em servidor de cliente. Mas em máquina minha, em ambiente que eu posso destruir e reconstruir quantas vezes eu quiser, faz todo sentido. Escrevi sobre isso no artigo <a href="/2026/03/31/migrando-meu-home-server-com-claude-code/">Migrando meu Home Server com Claude Code</a>, quando usei Claude pra instalar e configurar openSUSE MicroOS, Docker, NFS, firewall, hardening e o resto todo sem eu precisar ficar lembrando comando chato de shell.</p>
<p>Então pensei: por que não fazer a mesma coisa aqui?</p>
<p>Foi exatamente o que eu fiz. Comecei com prompts bem objetivos, sempre pedindo duas coisas ao mesmo tempo: fazer o trabalho e documentar o suficiente pra eu poder reconstruir tudo depois. O histórico mais importante ficou em <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>Vale um esclarecimento: aqueles prompts no GitHub não são as minhas mensagens cruas, do jeito que saíram na hora. Depois que tudo funcionou, eu pedi pro próprio Claude reescrever os prompts de forma organizada e detalhada, só pra fins de documentação. Os prompts originais eram bem mais simples e bem menos específicos. Eu descrevia o objetivo e deixava o Claude descobrir sozinho paths, arquivos de configuração, formatos e comandos necessários.</p>
<p>O primeiro prompt criou a box com <code>--nvidia</code>, <code>--home</code> separado e os mounts corretos. O segundo resolveu os três problemas clássicos do Arch dentro de Distrobox: <code>sudo</code>, <code>multilib</code> e o dummy package de <code>nvidia-utils</code>. O terceiro instalou a base de gaming, inclusive um detalhe que eu teria esquecido fácil se estivesse fazendo na mão: <code>pipewire-pulse</code>, necessário pra vários emuladores não ficarem mudos nem perderem sincronia.</p>
<p>O ponto não é o texto em si. O ponto é o método. Eu não preciso lembrar a ordem exata, nem as opções precisas, nem onde está a documentação de cada detalhe. Eu descrevo o objetivo e deixo o agente carregar o piano. Fico no papel de tech lead: observando, revisando, corrigindo direção quando precisa, e mandando continuar.</p>
<h2>O projeto no GitHub<span class="hx:absolute hx:-mt-20" id="o-projeto-no-github"></span>
    <a href="#o-projeto-no-github" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O setup vive em <a href="https://github.com/akitaonrails/distrobox-gaming"target="_blank" rel="noopener">akitaonrails/distrobox-gaming</a>, estruturado como um projeto Ansible. 17 roles cobrem desde criação da box, bootstrap de pacotes, seed de configs, instalação de DLC, configuração de controle, até verificação pós-setup. Os playbooks principais são:</p>
<ul>
<li><code>site.yml</code>: setup completo do zero</li>
<li><code>reset-configs.yml</code>: reset só das configs (útil quando você quer zerar tuning e redeployar)</li>
<li><code>backup.yml</code> / <code>restore.yml</code>: snapshot e restauração do container inteiro via <code>podman commit</code></li>
<li><code>refresh-shadps4.yml</code>: atualização standalone do shadPS4</li>
<li><code>install-xenia.yml</code>: instalação opcional do Xenia Manager (Wine prefix)</li>
</ul>
<p>O passo a passo completo está em <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>. As decisões de pacote (por que AUR em vez de Flatpak, por exemplo) estão em <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>Por que não Nix?<span class="hx:absolute hx:-mt-20" id="por-que-não-nix"></span>
    <a href="#por-que-n%c3%a3o-nix" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Alguém vai perguntar. &ldquo;Você tá recriando ambiente reproduzível. Por que não usar NixOS, ou pelo menos <code>nix-shell</code>/flakes?&rdquo;</p>
<p>Eu considerei e descartei. O ecossistema de emuladores que eu preciso vive no AUR. Pacotes <code>-bin</code> do AUR são wrappers de AppImage que empacotam o binário oficial do upstream sem compilar nada. <code>rpcs3-bin</code>, <code>pcsx2-latest-bin</code>, <code>eden-bin</code>, <code>duckstation-qt-bin</code>, <code>shadps4-bin</code> — esses binários acompanham o upstream praticamente no dia do lançamento. No Nixpkgs, muitos desses emuladores ficam semanas ou meses atrás do release, e alguns nem existem. Criar um overlay Nix pra cada emulador que eu preciso, manter, e lidar com os patches de compatibilidade quando upstream muda, é trabalho que eu não quero ter.</p>
<p>Tem também o modelo mental. Eu não quero aprender a linguagem Nix, o sistema de derivações e o modelo de avaliação pra montar um ambiente de games pessoal. Ansible eu já conheço, as roles são pastas com tasks em YAML, e o debug é <code>ansible-playbook -vvv</code>. Quando dá errado, eu leio a mensagem de erro, abro o task que falhou, e sei exatamente onde mexer. Se eu precisasse do mesmo nível de reprodutibilidade em escala, num cluster, com rollback atômico, aí sim Nix teria argumento. Pra um distrobox com emuladores na minha máquina pessoal, Ansible resolve com muito menos atrito.</p>
<p>Flatpak também foi testado e descartado. O <code>bwrap</code> (bubblewrap) do Flatpak não aninha direito dentro das mount namespaces do Docker, então apps Flatpak simplesmente não rodam dentro de distrobox. A alternativa seria instalar Flatpak no host, mas aí você volta ao problema de poluir a máquina de trabalho com pacotes de gaming. Além disso, as versões Flatpak de emuladores ficam bem atrás do AUR — o <code>pcsx2-latest-bin</code> do AUR tracka o AppImage 2.7.x, enquanto o Flathub ainda distribui a v2.6.3.</p>
<h2>Plataformas cobertas<span class="hx:absolute hx:-mt-20" id="plataformas-cobertas"></span>
    <a href="#plataformas-cobertas" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O setup cobre 12 plataformas hoje:</p>
<table>
  <thead>
      <tr>
          <th>Plataforma</th>
          <th>Emulador</th>
          <th>Renderer</th>
          <th>Destaque</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>Per-game configs curados via API</td>
      </tr>
      <tr>
          <td>PS4</td>
          <td>shadPS4</td>
          <td>Vulkan</td>
          <td>Driveclub em foco (experimental)</td>
      </tr>
      <tr>
          <td>PS Vita</td>
          <td>Vita3K</td>
          <td>—</td>
          <td>Seeding padrão</td>
      </tr>
      <tr>
          <td>GameCube</td>
          <td>Dolphin</td>
          <td>Vulkan</td>
          <td>Perfis 8BitDo Ultimate 2</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>DLC + cheats Atmosphere</td>
      </tr>
      <tr>
          <td>Dreamcast</td>
          <td>Flycast</td>
          <td>Vulkan, 2880p</td>
          <td>Wrapper hires, 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 entra por cima pra cobrir o resto (NES, SNES, Genesis, N64, arcade, etc.), com 21 cores do buildbot e 8 asset packs (databases, shaders, cheats, overlays, autoconfig).</p>
<p>Frontend é <a href="https://es-de.org/"target="_blank" rel="noopener">ES-DE</a>. As definições dos sistemas customizados ficam em <code>ansible/group_vars/all/esde.yml</code> e cada emulador entra com um wrapper que já sobe com as variáveis certas de GPU (voltarei nisso).</p>
<h2>GPU hardforce pra NVIDIA<span class="hx:absolute hx:-mt-20" id="gpu-hardforce-pra-nvidia"></span>
    <a href="#gpu-hardforce-pra-nvidia" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Num sistema híbrido com NVIDIA dGPU + AMD iGPU (meu caso no desktop), Vulkan às vezes escolhe o iGPU sozinho e o resultado é emulador rodando a 15 fps enquanto a 5090 fica parada. A solução é forçar o ICD Vulkan da NVIDIA em todos os launchers.</p>
<p>Cada entrada de desktop rendereizada pelo Ansible exporta <code>VK_ICD_FILENAMES</code> apontando pro ICD da NVIDIA, e o wrapper do ES-DE faz a mesma coisa antes de lançar qualquer core. Em combinação com a flag <code>--nvidia</code> na criação da box, que bind-monta os drivers do host em read-only, o resultado é previsível: Vulkan sempre na dGPU NVIDIA, independente do estado do sistema na hora.</p>
<p>Isso vale principalmente pra quem tem setup híbrido. Num desktop com só uma GPU, o ICD filtering é placebo, mas também não atrapalha.</p>
<h2>PS2: PCSX2 com tuning por jogo<span class="hx:absolute hx:-mt-20" id="ps2-pcsx2-com-tuning-por-jogo"></span>
    <a href="#ps2-pcsx2-com-tuning-por-jogo" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>A linha Gran Turismo é meu vício declarado. Cada versão precisa de tweak específico. O setup do PCSX2 hoje tem configs globais razoáveis (Vulkan, 4x SSAA, widescreen 16:9, Xbox-style bindings, FXAA + PCRTC antiblur, 16x anisotropic, deinterlace Automatic) e per-game INIs pros títulos que eu realmente jogo:</p>
<ul>
<li><strong>Gran Turismo 3 A-spec</strong> (SCUS-97102 + bundle PBPX-95503): widescreen pnach, pack de retextura opcional (desabilitado por default porque NFS causa flicker em cutscenes de showcase de carro)</li>
<li><strong>Gran Turismo 4 USA</strong> (SCUS-97328): HD HUD do Silentwarior112, pack completo de retextura, patches do Silent pra gatilho e câmera, widescreen pnach</li>
<li><strong>Gran Turismo 4 Spec II</strong> (SCUS-97436, CRC <code>4CE521F2</code>): caso especial — os HD packs de GT4 vanilla são linkados via symlink porque o Spec II tem mesma estrutura mas CRC diferente. Widescreen pnach renomeado pra bater com o CRC. Deinterlace mode 8 (Adaptive TFF), ShadeBoost tuning (+10 saturação, +3 brilho, +2 contraste) pra corrigir ghosting de pause-interlace e saturação anêmica do upscale</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>Os pnach files do Silent e da comunidade são baixados automaticamente pelo role <code>pcsx2_textures</code>, que também faz o deploy do pack de cheats do NAS e instala os HD packs do Spec II via symlink. Widescreen é ligado via <code>gamesettings</code> por CRC (PCSX2 identifica jogo por CRC antes de aplicar o pnach).</p>
<p>Sobre versão do PCSX2: o <code>pcsx2-latest-bin</code> do AUR tracka a linha 2.7.x, que é onde todas as features modernas estão (texture replacement, FXAA, PCRTC antiblur). O <code>pcsx2</code> padrão dos repos oficiais do Arch ainda está na 2.6.3. A diferença é mais de 250 commits de correção e feature novo.</p>
<h2>PS3: RPCS3 com per-game configs curados<span class="hx:absolute hx:-mt-20" id="ps3-rpcs3-com-per-game-configs-curados"></span>
    <a href="#ps3-rpcs3-com-per-game-configs-curados" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>RPCS3 tem um detalhe chato: configs por jogo ficam em <code>~/.config/rpcs3/custom_configs/config_&lt;SERIAL&gt;.yml</code>. O formato do arquivo tem versão, e se você gerar um YAML manualmente com versão errada, o RPCS3 rejeita silenciosamente e volta pro default. Isso custou tempo pra descobrir.</p>
<p>A role <code>rpcs3_per_game_configs</code> resolve isso assim:</p>
<ol>
<li>Query o <a href="https://rpcs3.net/compatibility"target="_blank" rel="noopener">compatibility DB da comunidade RPCS3</a> pra pegar os presets recomendados por título</li>
<li>Aplica overrides curados por cima pra títulos que eu sei que precisam de ajuste específico (tipicamente GT5 e GT6)</li>
<li>Salva o YAML com a filename convention exata (<code>config_&lt;SERIAL&gt;.yml</code>, prefixo <code>config_</code> obrigatório)</li>
</ol>
<p>Na linha Gran Turismo:</p>
<ul>
<li><strong>GT5</strong> (BCUS98114, BCES00569): Resolution Scale 300%, Shader Precision Ultra, Force High Precision Z, SPU XFloat Accurate, Multithreaded RSX. Combinação que mata o dithering típico do RSX sem quebrar a GT series safety config (WCB/RCB off).</li>
<li><strong>GT6</strong> (BCES01893 + variantes regionais): caso especial, <strong>versão travada em v1.05</strong>. Patches 1.06+ regridem com superfícies pretas em carros no menu garagem. O script <code>extract_ps3_dlc.py</code> tem per-title PATCH ceiling justamente pra pinar o GT6 no 1.05 mesmo quando o PSN cuspe patches mais novos. Além disso, Force CPU Blit ligado é mandatório (sem ele, flicker de tela inteira). O trade-off é que o retrovisor fica permanentemente preto — ligar WCB restauraria o retrovisor mas downgradaria pra 720p nativo. Prefiro perder o retrovisor.</li>
</ul>
<p>Mais detalhes desses dois em <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> e <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>Update checker de PS3<span class="hx:absolute hx:-mt-20" id="update-checker-de-ps3"></span>
    <a href="#update-checker-de-ps3" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Pra não ter que abrir a GUI do RPCS3 e ficar clicando em <code>Download Game Updates</code> jogo por jogo, criei um script em Python: <code>check_ps3_updates.py</code>. Ele percorre os jogos instalados em <code>dev_hdd0/game</code>, parseia o <code>PARAM.SFO</code> pra pegar a versão atual de cada título, consulta o servidor de updates da PSN (<code>a0.ww.np.dl.playstation.net</code>) e compara com o cache local de patches. <code>--list</code> mostra o diff; <code>--download</code> baixa o que falta. <code>--max-version</code> respeita os teto por título (ex: 1.05 pro GT6).</p>
<p>O primeiro scan na minha biblioteca achou 51 de 72 jogos com update disponível, 69 patches faltando localmente, uns 24 GB no total. Só o GT6 sozinho tinha 16 patches faltando, da 1.07 até 1.22, que eu descartei pelos motivos acima.</p>
<p>Os patches chegam como pacotes PSN tipo 0x0001 sem criptografia, então o próprio <code>extract_ps3_dlc.py</code> consegue instalar depois. A chamada padrão:</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>Acompanhar versão de DLC de PS3 manualmente, jogo por jogo no RPCS3, é inviável. Vira lista de tarefas eterna e você só descobre que tinha patch 1.22 quando o jogo crasha.</p>
<h2>PS1: DuckStation com per-game<span class="hx:absolute hx:-mt-20" id="ps1-duckstation-com-per-game"></span>
    <a href="#ps1-duckstation-com-per-game" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>A trilogia Gran Turismo começa no PS1, e a qualidade ali só sobe se você ligar as coisas certas:</p>
<ul>
<li><strong>Gran Turismo</strong> (SCUS-94949): built-in WidescreenHack do DuckStation (não existe cheat pra esse jogo), PGXP ligado</li>
<li><strong>Gran Turismo 2 Simulation</strong> (SCUS-94455): widescreen cheat + 8 MB RAM cheat (fix de áudio em track crowded), filter JINC2</li>
<li><strong>Gran Turismo 2 Arcade</strong> (SCUS-94488): mesmo tratamento do Simulation</li>
</ul>
<p>Global defaults: Vulkan, 8x upscale, JINC2 como filter default.</p>
<p>Os cheats pro GT2 saem de um repositório de widescreen fixes e são ligados automaticamente na pasta de cheats do DuckStation.</p>
<h2>Switch: Eden com DLC, update e cheats<span class="hx:absolute hx:-mt-20" id="switch-eden-com-dlc-update-e-cheats"></span>
    <a href="#switch-eden-com-dlc-update-e-cheats" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>No Switch, <code>eden-bin</code> é o emulador. A integração automatiza três coisas que antes eu fazia na mão:</p>
<ol>
<li>
<p><strong>Install de DLC e update</strong>: o role <code>install_dlcs</code> aponta pro diretório de dumps no NAS, identifica os arquivos <code>.nsp</code>/<code>.nsz</code>/<code>.xci</code>/<code>.xcz</code>, e instala tudo em <code>~/.local/share/eden/nand/user/Contents/registered/</code>. Se o dump está bagunçado (patches e DLC misturados em flat folder), o script <code>reorganize_switch_nsps.py</code> sorteia tudo por Title ID antes.</p>
</li>
<li>
<p><strong>Cheats Atmosphere</strong>: os cheats em formato Atmosphere vão symlinkados pro load path do Eden (<code>~/.local/share/eden/load/&lt;TITLE_ID&gt;/cheats/</code>). O role <code>switch_cheats</code> faz isso por todos os títulos cobertos pelo pack do NAS.</p>
</li>
<li>
<p><strong>Update checker</strong>: <code>check_switch_updates.py</code> lista títulos com update disponível vs o que tá instalado localmente. Útil pra não perder patch importante de jogo que sai atualização ainda.</p>
</li>
</ol>
<p>Atenção ao detalhe que me queimou: <code>QT_STYLE_OVERRIDE</code> precisa ser desarmado antes de lançar o <code>eden-bin</code>, senão ele conflita com Kvantum e quebra a UI. O wrapper já faz o <code>unset</code> antes do <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 ainda é território áspero no Linux. Não tem emulador nativo decente. A melhor rota hoje é <a href="https://xenia.jp/"target="_blank" rel="noopener">Xenia</a> rodando via Wine, gerenciado pelo <a href="https://github.com/xenia-manager/xenia-manager"target="_blank" rel="noopener">Xenia Manager</a>.</p>
<p>Isso é opt-in no meu setup. O playbook <code>install-xenia.yml</code> cria um Wine prefix dedicado, baixa o Xenia Manager, e registra os launchers. Não faz parte do <code>site.yml</code> principal porque nem todo mundo quer Wine no meio do caminho.</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>Forza antigo ainda é o rei do simcade de geração passada. Rodar FM2/3/4 no Xenia requer mods da comunidade Project Forza Plus: patches de compatibilidade, mods de performance, instalação de title updates específicos. Documentei o processo em <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 e 4<span class="hx:absolute hx:-mt-20" id="project-gotham-racing-3-e-4"></span>
    <a href="#project-gotham-racing-3-e-4" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>PGR3 roda razoavelmente bem no Xenia Canary. PGR4 precisa de ajuste específico: <code>render_target_path_vulkan = &quot;fsi&quot;</code> no config, senão algumas races quebram com artefato visual. Documentei o setup em <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>. Áudio do PGR4 no NVIDIA ainda tem problema de XMA decoding que gera garbage intermitente. Tem issue aberta no xenia-canary, sem resolução up até agora.</p>
<h3>Title Updates em batch<span class="hx:absolute hx:-mt-20" id="title-updates-em-batch"></span>
    <a href="#title-updates-em-batch" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Title Update pra jogo de Xbox 360 era distribuído pela Xbox Live, que já foi desligada. A alternativa é o archive.org, que tem o catálogo completo preservado. Escrevi <code>scripts/download-xbox360-tus.py</code> pra automatizar:</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>O script precisa do CLI <code>internetarchive</code> autenticado (<code>ia configure</code> uma vez, guarda o token). Ele scaneia a pasta de jogos, faz match contra o manifest do archive.org (cacheado localmente), prioriza por região (USA &gt; World &gt; Europe &gt; Japan), e baixa via <code>ia download --checksum</code> com retry automático. Os .zip resultantes vão pro Xenia Manager via <code>Manage → Install Content</code>, que extrai pro diretório correto (<code>000B0000/</code>).</p>
<p>O setup completo tá em <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 no distrobox<span class="hx:absolute hx:-mt-20" id="steam-no-distrobox"></span>
    <a href="#steam-no-distrobox" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Desde que o Proton virou cidadão de primeira no Linux, faz sentido ter Steam também no gaming box. Adicionei o pacote <code>steam</code> no Ansible (que puxa multilib e dependências 32-bit), montei <code>/mnt/data/steam</code> em read-write pra a biblioteca persistir fora do container, e criei launcher no host.</p>
<p>Na prática, isso me permite rodar jogos Steam via Proton lado a lado com emulação, sem precisar alternar ambiente. O mini-PC com RTX 4090 continua sendo a máquina principal de Steam no cockpit de sim racing. O desktop é fallback e ambiente de teste.</p>
<h2>Shell e controle por controle<span class="hx:absolute hx:-mt-20" id="shell-e-controle-por-controle"></span>
    <a href="#shell-e-controle-por-controle" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><h3>Shell mínimo dentro da box<span class="hx:absolute hx:-mt-20" id="shell-mínimo-dentro-da-box"></span>
    <a href="#shell-m%c3%adnimo-dentro-da-box" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Quando eu entro no gaming box via <code>distrobox enter gaming</code>, eu quero prompt decente mesmo pra troubleshooting rápido. Instalei <code>zsh</code> + <code>starship</code> com config mínima, zero plugins exóticos. Não é meu setup de dev full, é só &ldquo;prompt que mostra path e git branch sem me dar vergonha&rdquo;.</p>
<h3>Dolphin e o inferno do controle moderno<span class="hx:absolute hx:-mt-20" id="dolphin-e-o-inferno-do-controle-moderno"></span>
    <a href="#dolphin-e-o-inferno-do-controle-moderno" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p><code>Dolphin</code> sempre foi o rei do atrito manual. Se você usa um controle mais moderno, como meu 8BitDo Ultimate 2, precisa lembrar como eu gosto do mapeamento de GameCube, como adapto o Wii Remote, quando usar perfil de Nunchuk, quando trocar pro Classic Controller. Eu não tenho a menor paciência pra reconstruir isso na mão toda vez. Os perfis ficam prontos em <code>config/emulator-overrides/dolphin/Profiles/</code> e são copiados pelo role <code>seed_configs</code>. Detalhes em <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>Alguns tropeços que sobraram<span class="hx:absolute hx:-mt-20" id="alguns-tropeços-que-sobraram"></span>
    <a href="#alguns-trope%c3%a7os-que-sobraram" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Nem tudo virou magia. Emulação sempre tem casca de banana.</p>
<p><code>Flycast</code>, por exemplo, tem uma armadilha irritante. O arquivo <code>emu.cfg</code> usa chaves <code>rend.*</code> dentro da seção <code>[config]</code>. Se você cria uma seção <code>[rend]</code> separada, parece certo, mas o emulador simplesmente ignora e depois reescreve tudo com default medíocre. A correção virou wrapper dedicado em <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>Detalhes em <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> ainda está longe de ser um caso encerrado. O setup atual é focado em <code>Driveclub</code>, com config espelhada, patch XML e links dos <code>sys_modules</code> do firmware 11.00. E isso não foi por acaso: eu gosto muito de <code>Driveclub</code>. Aliás, é praticamente o único jogo ainda preso no PS4 que eu realmente queria conseguir rodar direito. Eu já vi vários vídeos no YouTube mostrando o jogo funcionando, mas até agora eu mesmo não consegui fazer isso rodar de forma satisfatória no Linux. Documentado em <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>. A parte boa é que agora eu não dependo mais de memória muscular pra lembrar como lançar, com qual patch, em que pasta, com quais módulos. A parte ruim é que emulação de PS4 ainda é emulação de PS4. Não existe script que faça upstream amadurecer mais rápido. Se alguém souber como fechar essa configuração no Linux, olhe o projeto no GitHub e mande um PR.</p>
<p>Ridge Racer V tem um flickering de textura de carro documentado nas issues do PCSX2 (#3639, #13729). O renderer Hardware é aceitável pra jogo de corrida onde você não fica admirando parado o modelo. Software renderer resolve mas mata performance. Escolhi aceitar o flicker.</p>
<h2>O ganho real não é só automação<span class="hx:absolute hx:-mt-20" id="o-ganho-real-não-é-só-automação"></span>
    <a href="#o-ganho-real-n%c3%a3o-%c3%a9-s%c3%b3-automa%c3%a7%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Seria fácil resumir isso como &ldquo;olha que legal, usei IA pra automatizar shell script&rdquo;. Não é isso.</p>
<p>O ganho real é mais chato e mais importante: eu parei de gastar energia mental com trabalho braçal. Não precisei mais lembrar comando raro. Não precisei manter dezenas de abas com wiki, issue, fórum e README. Não precisei deixar meia dúzia de terminais espalhados dando <code>tail</code> em log enquanto tento lembrar que opção escondida de GUI o emulador insiste em regravar no primeiro boot. Eu passei a trabalhar em conversa.</p>
<p>Eu digo pro agente o objetivo. Ele procura os arquivos, lê os logs, encontra o formato certo do config, compara defaults, propõe correções, escreve script, valida paths, verifica UID/GID, confere symlink quebrado, gera wrapper, exporta launcher. Eu continuo responsável pelas decisões, claro. Mas parei de ser digitador de comando raro.</p>
<p>Esse é o ponto que mais me interessa no uso de coding agents em Linux. Eles reduzem dramaticamente a fricção de entrada. Muita gente desiste do desktop Linux não porque o sistema seja incapaz, mas porque a curva de tuning historicamente foi irritante demais. Ter um assistente capaz de ler docs, cruzar config, propor automação e executar com supervisão muda esse jogo.</p>
<h2>Se você quiser reproduzir<span class="hx:absolute hx:-mt-20" id="se-você-quiser-reproduzir"></span>
    <a href="#se-voc%c3%aa-quiser-reproduzir" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O repo foi publicado pra isso:</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"># ajuste paths da sua máquina</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 é 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>Leia o <a href="https://github.com/akitaonrails/distrobox-gaming/blob/master/README.md"target="_blank" rel="noopener"><code>README.md</code></a> e o <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> antes. Eu não distribuo ROM, BIOS, firmware nem chaves. O repo só detecta, linka e configura o que você já tem na sua própria máquina.</p>
<h2>Se você preferir a rota com Claude Code<span class="hx:absolute hx:-mt-20" id="se-você-preferir-a-rota-com-claude-code"></span>
    <a href="#se-voc%c3%aa-preferir-a-rota-com-claude-code" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Minha recomendação é simples:</p>
<ol>
<li>Comece pelo objetivo, não pelo comando. &ldquo;Quero um distrobox Arch com GPU NVIDIA, home separado, ROMs read-only e Steam rw&rdquo; é melhor do que despejar meia linha de shell sem contexto.</li>
<li>Peça sempre pra documentar o que está fazendo. Se der certo, promova o resultado a script ou role. Se não documentar, você acabou de fabricar uma gambiarra descartável.</li>
<li>Trabalhe em fases. Criação da box. Bootstrap. Config de emuladores. Export de launchers. Verificação. Foi exatamente assim que eu quebrei o problema.</li>
<li>Peça verificações objetivas. <code>command -v</code>, existência de arquivos, symlinks quebrados, UID/GID, path de ROM, áudio, GPU. A melhor automação é a que falha cedo.</li>
<li>Não use agente como papagaio de comando. Use como assistente técnico. Você continua revisando as decisões e mandando ajustar o rumo.</li>
</ol>
<p>Pra mim, esse foi o ganho. Eu saí do zero pra uma máquina de emulação muito mais completa sem precisar customizar tudo na unha, em GUI, uma janela por vez. E, dessa vez, terminei com energia pra fazer o que eu queria desde o começo.</p>
<p>Jogar.</p>
]]></content:encoded><category>emulacao</category><category>containers</category><category>automacao</category></item><item><title>Migrando meu Home Server com Claude Code | openSUSE MicroOS</title><link>https://akitaonrails.github.io/2026/03/31/migrando-meu-home-server-com-claude-code/</link><guid isPermaLink="true">https://akitaonrails.github.io/2026/03/31/migrando-meu-home-server-com-claude-code/</guid><pubDate>Tue, 31 Mar 2026 16:00:00 -0300</pubDate><description>&lt;p&gt;Meu home server antigo era uma bagunça. Um Intel NUC com Ubuntu Server que eu fui remendando ao longo de dois anos. Containers com caminhos hardcoded, volumes montados em locais aleatórios (&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 espalhados sem padrão nenhum. Funcionava, mas se eu perdesse o disco, levaria dias pra reconstruir tudo de memória.&lt;/p&gt;
&lt;p&gt;Com o &lt;a href="https://akitaonrails.github.io/2026/03/31/review-minisforum-ms-s1-max-amd-ai-max-395/"&gt;novo Minisforum MS-S1 Max&lt;/a&gt; que comprei, decidi fazer a migração direito. E decidi usar Claude Code desde o início pra acelerar o processo. É um servidor caseiro, só eu uso, o risco de fazer besteira é baixo. Mas se fosse um servidor de produção real, eu jamais faria isso sem review humano rigoroso em cada passo.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Meu home server antigo era uma bagunça. Um Intel NUC com Ubuntu Server que eu fui remendando ao longo de dois anos. Containers com caminhos hardcoded, volumes montados em locais aleatórios (<code>/home/akitaonrails/docker/</code>, <code>/home/akitaonrails/sonarr/</code>, <code>/mnt/terachad/</code>), docker-compose files espalhados sem padrão nenhum. Funcionava, mas se eu perdesse o disco, levaria dias pra reconstruir tudo de memória.</p>
<p>Com o <a href="/2026/03/31/review-minisforum-ms-s1-max-amd-ai-max-395/">novo Minisforum MS-S1 Max</a> que comprei, decidi fazer a migração direito. E decidi usar Claude Code desde o início pra acelerar o processo. É um servidor caseiro, só eu uso, o risco de fazer besteira é baixo. Mas se fosse um servidor de produção real, eu jamais faria isso sem review humano rigoroso em cada passo.</p>
<p>O que segue é o relato da migração e o guia pra quem quiser replicar. Se um dia eu precisar reconstruir do zero, esse post é a documentação.</p>
<h2>Escolha do sistema operacional<span class="hx:absolute hx:-mt-20" id="escolha-do-sistema-operacional"></span>
    <a href="#escolha-do-sistema-operacional" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><h3>Por que não Ubuntu Server de novo<span class="hx:absolute hx:-mt-20" id="por-que-não-ubuntu-server-de-novo"></span>
    <a href="#por-que-n%c3%a3o-ubuntu-server-de-novo" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Eu usei Ubuntu Server no NUC por praticidade. Mas <code>do-release-upgrade</code> é uma roleta russa. Toda vez que o Ubuntu lança versão nova, a atualização é um risco real de quebrar coisas. Pacotes mudam, configs são sobrescritas, dependências conflitam. Pra um servidor que precisa estar sempre rodando, isso é inaceitável.</p>
<h3>Por que não Arch Linux<span class="hx:absolute hx:-mt-20" id="por-que-não-arch-linux"></span>
    <a href="#por-que-n%c3%a3o-arch-linux" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Eu uso Arch no desktop e gosto. Mas Arch é uma rolling release sem nenhuma garantia de estabilidade. Pra um desktop onde eu posso parar e resolver problemas, ótimo. Pra um servidor headless que roda 49 containers Docker e precisa funcionar depois de cada reboot, não.</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>As duas opções modernas pra servidor de containers são Fedora CoreOS e openSUSE MicroOS. As duas são sistemas imutáveis: o root filesystem é read-only, atualizações são atômicas (ou aplicam inteiro ou não aplicam nada), e rollback é instantâneo.</p>
<p>A diferença: Fedora CoreOS usa Ignition (configuração declarativa antes do primeiro boot) e é projetada pra ser provisionada automaticamente. MicroOS usa <code>transactional-update</code> e permite uso interativo normal. Pra um home server onde eu quero SSH e mexer manualmente quando precisar, MicroOS se encaixa melhor.</p>
<h3>O que torna MicroOS diferente<span class="hx:absolute hx:-mt-20" id="o-que-torna-microos-diferente"></span>
    <a href="#o-que-torna-microos-diferente" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O conceito de sistema imutável muda como você opera o servidor:</p>
<p>Toda instalação de pacote ou edição de <code>/etc</code> passa por <code>transactional-update</code>, que cria um snapshot btrfs novo, aplica a mudança nesse snapshot, e no reboot seguinte o sistema boota no snapshot atualizado. Se a mudança quebrar alguma coisa, você faz <code>transactional-update rollback</code> e volta pro snapshot anterior em segundos.</p>
<p>Atualizações são automáticas e diárias. O <code>transactional-update.timer</code> baixa patches, cria snapshot, e o <code>rebootmgr</code> reinicia numa janela configurada (no meu caso, entre 4h e 5h30 da manhã). Se a atualização quebra o boot, o GRUB automaticamente volta pro snapshot anterior.</p>
<p>SELinux vem enforcing por padrão. Isso causou 90% dos problemas durante a migração, mas é a configuração certa pra segurança.</p>
<h2>Setup inicial<span class="hx:absolute hx:-mt-20" id="setup-inicial"></span>
    <a href="#setup-inicial" 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 alocados como VRAM via BIOS (UMA Frame Buffer Size)</li>
<li>NVMe de 2TB (sistema + Docker)</li>
<li>Rede cabeada 2.5Gbps</li>
<li>Synology DS1821+ NAS em 192.168.0.21 (NFS)</li>
</ul>
<h3>Primeiros passos<span class="hx:absolute hx:-mt-20" id="primeiros-passos"></span>
    <a href="#primeiros-passos" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Instalação do MicroOS é padrão. Depois:</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"># Criar usuário com UID que bate com o NAS (pra NFS funcionar sem problemas de permissão)</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"># Configurar sudo (dentro de 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"># dentro: adicionar akitaonrails ao 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>NFS do Synology<span class="hx:absolute hx:-mt-20" id="nfs-do-synology"></span>
    <a href="#nfs-do-synology" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O NAS Synology exporta <code>/volume1/TERACHAD</code> via NFS. O ponto de montagem no MicroOS é <code>/var/mnt/terachad</code> (não <code>/mnt/</code>, que fica no root imutável).</p>
<p>No <code>/etc/fstab</code> (aplicado via 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>Detalhes que importam: <code>nfsvers=4.1</code> porque 4.2 não funcionou com o Synology. <code>rsize=262144,wsize=262144</code> (256KB buffers) foi a maior melhoria de performance NFS. <code>hard</code> em vez de <code>nofail</code> pra que o mount retente indefinidamente se o NAS desconectar temporariamente.</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>Esse passo deu trabalho. O Radeon 8060S do AI Max+ 395 é gfx1151, que o ROCm não suporta oficialmente. Foram necessários três passos, e os três são obrigatórios:</p>
<ol>
<li>BIOS: setar UMA Frame Buffer Size pra 96GB</li>
<li>Kernel: adicionar <code>amdttm.pages_limit=25165824 amdttm.page_pool_size=25165824</code> em <code>/etc/kernel/cmdline</code></li>
<li>Docker: usar <code>HSA_OVERRIDE_GFX_VERSION=11.5.1</code> em todo container ROCm</li>
</ol>
<p>Sem o passo 2, o ROCm só vê 15.5GB mesmo com a alocação do BIOS. Os números são 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"># FORA do 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>Verificação:</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 no MicroOS<span class="hx:absolute hx:-mt-20" id="docker-no-microos"></span>
    <a href="#docker-no-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 e login pra o grupo pegar</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Instalar docker-compose standalone (o pacote openSUSE não inclui)</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> faz containers sobreviverem restart do daemon Docker. <code>userland-proxy: false</code> usa iptables direto em vez de processos proxy (menos overhead). <code>selinux-enabled: true</code> é obrigatório no MicroOS.</p>
<h2>SELinux e Docker: a maior fonte de problemas<span class="hx:absolute hx:-mt-20" id="selinux-e-docker-a-maior-fonte-de-problemas"></span>
    <a href="#selinux-e-docker-a-maior-fonte-de-problemas" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Isso merece uma seção inteira porque foi responsável por 90% dos bugs durante a migração.</p>
<p>No MicroOS com SELinux enforcing, todo container que escreve em diretório bind-mounted do host precisa de tratamento especial. Existem duas abordagens: o sufixo <code>:Z</code> nos volumes e a opção <code>security_opt: label:disable</code>.</p>
<h3>NUNCA use <code>:Z</code>. Use <code>security_opt: label:disable</code>.<span class="hx:absolute hx:-mt-20" id="nunca-use-z-use-security_opt-labeldisable"></span>
    <a href="#nunca-use-z-use-security_opt-labeldisable" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O <code>:Z</code> diz pro Docker relabeling o diretório do host com o contexto SELinux do container. Parece a coisa certa. Na prática:</p>
<ul>
<li>Bancos SQLite quebram. O relabeling muda o contexto do arquivo e o SQLite pode recusar abrir o WAL journal.</li>
<li>Mounts NFS ignoram <code>:Z</code> silenciosamente. O NFS não suporta xattrs do SELinux. O kernel ignora o flag sem erro, mas o container continua sem permissão.</li>
<li>Mounts <code>:ro,Z</code> tentam relabeling mesmo sendo read-only, o que falha em NFS e pode corromper contexto em paths locais.</li>
</ul>
<p>A solução correta pra todo container nesse sistema:</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">meuservico</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"># desliga enforcement SELinux pra esse 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"># SEM :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"># SEM :Z mesmo em :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> desliga enforcement de labels SELinux apenas pra aquele container, não pro sistema inteiro. Combinado com o isolamento de rede e processos do Docker, é seguro pra home server.</p>
<h2>A migração dos stacks<span class="hx:absolute hx:-mt-20" id="a-migração-dos-stacks"></span>
    <a href="#a-migra%c3%a7%c3%a3o-dos-stacks" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Todos os stacks Docker foram reorganizados em <code>/var/opt/docker/&lt;stack&gt;/docker-compose.yml</code>. No servidor antigo, estavam espalhados em <code>/home/akitaonrails/docker/</code>, <code>/home/akitaonrails/&lt;servico&gt;/</code>, sem padrão.</p>
<h3>Substituições aplicadas em todos os compose files<span class="hx:absolute hx:-mt-20" id="substituições-aplicadas-em-todos-os-compose-files"></span>
    <a href="#substitui%c3%a7%c3%b5es-aplicadas-em-todos-os-compose-files" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><table>
  <thead>
      <tr>
          <th>Antes</th>
          <th>Depois</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;servico&gt;/</code></td>
          <td><code>/var/opt/docker/&lt;stack&gt;/&lt;servico&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>Stack de media (Plex, Radarr, Sonarr, etc.)<span class="hx:absolute hx:-mt-20" id="stack-de-media-plex-radarr-sonarr-etc"></span>
    <a href="#stack-de-media-plex-radarr-sonarr-etc" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O stack de media é o mais complexo. Plex precisa de IP próprio na LAN (macvlan) pro streaming direto funcionar. O 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>No compose, o Plex precisa estar em duas redes: a macvlan (pro IP 192.168.0.6) e a bridge default (pra outros containers como Seerr conseguirem se comunicar):</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"># obrigatório — sem isso, Seerr não enxerga o 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>Detalhe que quase me quebrou: o Plex guarda paths absolutos no banco de dados. Se o volume interno do container mudou de <code>/media</code> pra <code>/data</code>, o Plex não encontra mais nada. Tem que usar exatamente o mesmo mount target do compose antigo.</p>
<h3>Ollama com ROCm<span class="hx:absolute hx:-mt-20" id="ollama-com-rocm"></span>
    <a href="#ollama-com-rocm" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Stack novo, não existia no servidor anterior:</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> ativa flash attention. <code>OLLAMA_KV_CACHE_TYPE=q8_0</code> usa KV cache em 8-bit, cortando bandwidth necessária por token pela metade. São otimizações de performance gratuitas.</p>
<h3>Monitoramento (Grafana + Prometheus)<span class="hx:absolute hx:-mt-20" id="monitoramento-grafana--prometheus"></span>
    <a href="#monitoramento-grafana--prometheus" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O Grafana usa named volume (<code>grafana_data</code>) que NÃO é incluído em backups normais de filesystem. Foi o motivo pelo qual perdi todos os dashboards na primeira tentativa. A solução é backup explícito do 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"># No servidor antigo:</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"># Transferir e restaurar no novo:</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>Mesma coisa pro Portainer (<code>portainer_data</code>). Qualquer volume definido no bloco <code>volumes:</code> do compose sem host path precisa desse tratamento.</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>Eu uso <a href="https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/"target="_blank" rel="noopener">Cloudflare Tunnel</a> pra acessar todos os serviços fora de casa sem abrir portas no roteador. A migração foi a mais fácil: copiar o arquivo de credenciais JSON do túnel e o <code>config.yml</code>, atualizar os IPs de <code>.145</code> pra <code>.90</code>, e subir o container. O túnel mantém o mesmo ID, não precisa recriar DNS.</p>
<p>Os hostnames ficam em <code>config.yml</code>: portainer, grafana, plex, seerr, qbittorrent, syncthing, radarr, sonarr, bazarr, prowlarr, vault, gitea, kavita, e outros. Tudo acessível via <code>https://&lt;servico&gt;.example.com</code> de qualquer lugar.</p>
<h3>Gitea (registry de imagens)<span class="hx:absolute hx:-mt-20" id="gitea-registry-de-imagens"></span>
    <a href="#gitea-registry-de-imagens" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O <a href="https://github.com/go-gitea/gitea"target="_blank" rel="noopener">Gitea</a> funciona como registry Docker privado na porta 3007. Os projetos Frank FBI, Frank Mega, Frank Yomik e Mila têm imagens Docker que são buildadas e pushadas pro Gitea. Pra funcionar, o <code>daemon.json</code> do Docker precisa de:</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>O SSH do Gitea deu problema na migração: o app.ini antigo tinha <code>SSH_LISTEN_PORT=22</code>, mas o entrypoint do container também inicia sshd na porta 22. Conflito. Solução: <code>GITEA__server__SSH_LISTEN_PORT=2222</code> como variável de ambiente no compose.</p>
<h3>Todos os 49 containers rodando<span class="hx:absolute hx:-mt-20" id="todos-os-49-containers-rodando"></span>
    <a href="#todos-os-49-containers-rodando" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O servidor migrado roda 49 containers em 15 stacks. O stack de media sozinho tem 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>). Os projetos pessoais (<a href="https://github.com/akitaonrails/frank_fbi"target="_blank" rel="noopener">Frank FBI</a>, <a href="/2026/02/21/vibe-code-fiz-um-clone-do-mega-em-rails-em-1-dia-pro-meu-home-server/">Frank Mega</a>, <a href="https://github.com/akitaonrails/FrankYomik"target="_blank" rel="noopener">Frank Yomik</a>, Mila) somam mais 11. Monitoramento com <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 e <a href="https://github.com/google/cadvisor"target="_blank" rel="noopener">cAdvisor</a>. Utilitários como <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> como registry Docker privado. <a href="https://github.com/immich-app/immich"target="_blank" rel="noopener">Immich</a> como Google Photos self-hosted. <a href="https://github.com/oae/kaizoku"target="_blank" rel="noopener">Kaizoku</a> pra manga com <a href="https://github.com/Kareadita/Kavita"target="_blank" rel="noopener">Kavita</a> como reader. <a href="https://github.com/ollama/ollama"target="_blank" rel="noopener">Ollama</a> com ROCm. E o <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> indexando a blockchain do NAS.</p>
<h2>Backups: duas camadas<span class="hx:absolute hx:-mt-20" id="backups-duas-camadas"></span>
    <a href="#backups-duas-camadas" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><h3>Camada 1: snapshots btrfs locais (snapper)<span class="hx:absolute hx:-mt-20" id="camada-1-snapshots-btrfs-locais-snapper"></span>
    <a href="#camada-1-snapshots-btrfs-locais-snapper" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O <code>/var</code> vive numa partição btrfs de 3.7TB. O snapper cria snapshots automáticos: 7 diários + 1 semanal. São crash-consistent, não application-consistent (postgres pode ficar levemente inconsistente se tiver write pesado no momento do snapshot).</p>
<p>Pra recuperar um arquivo apagado acidentalmente:</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>Pra rollback completo de um stack:</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>Camada 2: restic pro NAS (off-machine)<span class="hx:absolute hx:-mt-20" id="camada-2-restic-pro-nas-off-machine"></span>
    <a href="#camada-2-restic-pro-nas-off-machine" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O <a href="https://github.com/restic/restic"target="_blank" rel="noopener">restic</a> roda toda noite às 3h, faz backup incremental pra <code>/var/mnt/terachad/homelab-backups/</code>. Retenção: 7 diários + 4 semanais. Deduplicação por conteúdo, então Plex config (19GB) e Gitea repos (12GB) transferem apenas deltas.</p>
<p>Antes do restic rodar, um <code>pg_dump</code> exporta os bancos postgres (Immich, Kaizoku). Os dumps vão pra <code>/tmp/homelab-db-dumps/</code> e são incluídos no backup.</p>
<p>O que NÃO é incluído no backup (re-downloadável): blockchain Bitcoin (785GB no NAS), imagens Docker (re-pullable), modelos Ollama (re-downloadable), cache do HuggingFace/EasyOCR, transcoding temporário do Plex.</p>
<p>Diretórios grandes e re-downloadáveis foram convertidos em subvolumes btrfs pra que o snapper os ignore: <code>/var/lib/ollama</code> e <code>/var/opt/docker/bitcoin/fulcrum/fulc2_db</code>.</p>
<h2>Tuning de performance<span class="hx:absolute hx:-mt-20" id="tuning-de-performance"></span>
    <a href="#tuning-de-performance" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><h3>btrfs com compressão zstd<span class="hx:absolute hx:-mt-20" id="btrfs-com-compressão-zstd"></span>
    <a href="#btrfs-com-compress%c3%a3o-zstd" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Adicionei <code>compress=zstd:1</code> no fstab pra partição <code>/var</code>. O zstd nível 1 tem overhead de CPU quase zero em NVMe e comprime bem metadata Docker, JSON configs e logs. Dados incompressíveis (SQLite, postgres) são ignorados automaticamente pelo 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>Com ~30GB de RAM disponível pro sistema (96GB vão pra VRAM), swap comprimido em memória ajuda. O zram cria um dispositivo de swap de ~15GB (ram/2) com compressão zstd, muito mais rápido que swap em disco.</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 em diretórios de banco de dados<span class="hx:absolute hx:-mt-20" id="btrfs-nodatacow-em-diretórios-de-banco-de-dados"></span>
    <a href="#btrfs-nodatacow-em-diret%c3%b3rios-de-banco-de-dados" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Copy-on-write + escrita aleatória de banco de dados = write amplification. Desabilitei CoW nos diretórios que guardam SQLite e 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 em modo performance<span class="hx:absolute hx:-mt-20" id="cpu-em-modo-performance"></span>
    <a href="#cpu-em-modo-performance" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Num servidor headless, não faz sentido economizar energia:</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>Persistido 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>Problema que descobri: o Docker vem com <code>KillMode=process</code>, que significa que no shutdown do sistema, o systemd mata só o dockerd e deixa todos os <code>containerd-shim</code> (um por container, ~49 no meu caso) órfãos. O systemd-shutdown precisa caçar eles um a um depois que o journal já parou, causando um hang silencioso de vários minutos.</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>Os problemas que encontramos<span class="hx:absolute hx:-mt-20" id="os-problemas-que-encontramos"></span>
    <a href="#os-problemas-que-encontramos" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Essa é a tabela de problemas reais que tivemos durante a migração. Se você está planejando algo parecido, leia antes de começar:</p>
<table>
  <thead>
      <tr>
          <th>Problema</th>
          <th>Causa</th>
          <th>Solução</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>ROCm vê só 15.5GB de VRAM</td>
          <td>Kernel TTM limita pages mesmo com BIOS em 96GB</td>
          <td>Adicionar <code>amdttm.pages_limit=25165824</code> no kernel cmdline</td>
      </tr>
      <tr>
          <td>Todos containers: permission denied em volumes</td>
          <td>SELinux <code>container_t</code> não escreve em paths sem label</td>
          <td><code>security_opt: label:disable</code> em todo serviço</td>
      </tr>
      <tr>
          <td>NFS com <code>:Z</code> falha silenciosamente</td>
          <td>NFS não suporta xattr do SELinux</td>
          <td>Nunca usar <code>:Z</code> em paths NFS</td>
      </tr>
      <tr>
          <td>SQLite quebra com <code>:Z</code></td>
          <td>Relabeling muda contexto, WAL mode falha</td>
          <td>Remover <code>:Z</code>, usar <code>label:disable</code></td>
      </tr>
      <tr>
          <td>Radarr/Sonarr mostraram tela de setup</td>
          <td>Backup em <code>appdata/config/</code> mas compose montava <code>appdata/</code></td>
          <td>Corrigir: <code>appdata/config:/config</code></td>
      </tr>
      <tr>
          <td>Grafana perdeu dashboards</td>
          <td>Named volume não incluído no backup de filesystem</td>
          <td>Backup explícito do named volume</td>
      </tr>
      <tr>
          <td>Plex não encontra mídia</td>
          <td>Path interno mudou de <code>/media</code> pra <code>/data</code></td>
          <td>Restaurar path original no compose</td>
      </tr>
      <tr>
          <td>Seerr não conecta no Plex</td>
          <td>macvlan isolada da bridge network</td>
          <td>Adicionar <code>default: {}</code> nas networks do Plex</td>
      </tr>
      <tr>
          <td>Fulcrum crash: &ldquo;option -b missing&rdquo;</td>
          <td>Env vars não suportadas pela imagem</td>
          <td>Usar flags CLI em <code>command:</code></td>
      </tr>
      <tr>
          <td>bitcoind rejeita RPC</td>
          <td>Bind em <code>::1</code> por padrão</td>
          <td>Adicionar <code>-rpcbind=0.0.0.0 -rpcallowip=172.0.0.0/8</code></td>
      </tr>
      <tr>
          <td>sdbootutil warning no transactional shell</td>
          <td>Deve rodar fora da transação</td>
          <td>Executar <code>sdbootutil update-all-entries</code> no shell normal</td>
      </tr>
      <tr>
          <td>Watchtower permission denied em docker.sock</td>
          <td>SELinux bloqueia acesso ao socket</td>
          <td><code>label:disable</code></td>
      </tr>
      <tr>
          <td>Gitea SSH crash</td>
          <td>Conflito: entrypoint sshd porta 22 + app porta 22</td>
          <td><code>GITEA__server__SSH_LISTEN_PORT=2222</code></td>
      </tr>
      <tr>
          <td>docker-compose não instalado com Docker</td>
          <td>Pacote openSUSE só instala o daemon</td>
          <td>Instalar binário standalone manualmente</td>
      </tr>
  </tbody>
</table>
<h2>O que dizer pro Claude Code antes de começar<span class="hx:absolute hx:-mt-20" id="o-que-dizer-pro-claude-code-antes-de-começar"></span>
    <a href="#o-que-dizer-pro-claude-code-antes-de-come%c3%a7ar" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Se eu fosse refazer a migração do zero, daria estas instruções pro Claude Code na primeira mensagem. Na ordem de importância:</p>
<p>Diga que SELinux está enforcing e que ele NÃO deve usar <code>:Z</code> em nenhum volume Docker, e sim <code>security_opt: label:disable</code> em todo serviço. Diga que <code>/var/mnt/terachad/</code> é mount NFS e que <code>:Z</code> nunca deve aparecer em paths NFS. Diga pra sempre olhar o compose original antes de reescrever e só mudar IPs, paths e nomes de container, sem inventar novos layouts de volume. Avise que named volumes precisam de backup explícito (Grafana, Portainer). Explique que Plex roda em macvlan e precisa de <code>default: {}</code> nas networks. Informe que a GPU é gfx1151, não suportada oficialmente, e que precisa de UMA 96GB no BIOS + kernel TTM params + <code>HSA_OVERRIDE_GFX_VERSION=11.5.1</code>. E diga que Bitcoin/Fulcrum não processam variáveis de ambiente, tudo vai como argumento no <code>command:</code>.</p>
<p>Essas instruções teriam evitado 80% dos problemas que encontramos.</p>
<h2>Layout final do servidor<span class="hx:absolute hx:-mt-20" id="layout-final-do-servidor"></span>
    <a href="#layout-final-do-servidor" 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/      (túnel Cloudflare)
├── frank_fbi/        (análise de fraude por email)
├── frank_mega/       (clone do Mega)
├── frank_yomik/      (tradução de mangás)
├── gitea/            (registry Docker)
├── immich/           (Google Photos self-hosted)
├── kaizoku/          (manga downloader &#43; reader)
├── media/            (Plex &#43; *arr stack)
├── mila/             (bot Discord)
├── monitor/          (Grafana &#43; Prometheus)
├── ollama/           (LLM local com ROCm)
├── rip/              (HandBrake)
└── utils/            (Portainer, Vaultwarden, Syncthing, etc.)

/var/mnt/terachad/    (NFS do Synology)
├── Bitcoin/data/     (blockchain, 785GB)
├── Downloads/        (torrents &#43; nzbget)
├── Videos/           (Radarr movies &#43; Sonarr series)
└── Ollama/models/    (overflow de modelos se disco local encher)</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>Aviso sobre usar IA pra administrar servidor<span class="hx:absolute hx:-mt-20" id="aviso-sobre-usar-ia-pra-administrar-servidor"></span>
    <a href="#aviso-sobre-usar-ia-pra-administrar-servidor" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Eu usei Claude Code pra acelerar a migração. Criou compose files, escreveu scripts de backup, configurou firewall, diagnosticou problemas de SELinux. Funcionou bem pro meu caso: home server, só eu uso, e eu estava revisando cada passo.</p>
<p>Mas tem armadilhas. O Claude não sabe que <code>:Z</code> quebra SQLite a menos que você diga. Ele não sabe que o Fulcrum não aceita env vars a menos que já tenha visto o Dockerfile. Ele vai inventar layouts de volume &ldquo;melhores&rdquo; que quebram o Plex porque o Plex guarda paths absolutos no banco de dados.</p>
<p>Se fosse produção real: não faça isso sem review. Cada compose file que o Claude gerar, leia inteiro antes de aplicar. Cada comando destrutivo (rollback, delete, recreate), confirme manualmente. E tenha backups testados antes de começar. O Claude é bom pra gerar a primeira versão e diagnosticar erros, mas as decisões de arquitetura e as validações de segurança são suas.</p>
<p>Os posts anteriores sobre o home server que podem dar contexto adicional:</p>
<ul>
<li><a href="/2024/04/03/meu-netflix-pessoal-com-docker-compose/">Meu &ldquo;Netflix Pessoal&rdquo; com Docker Compose</a></li>
<li><a href="/2025/09/09/acessando-meu-home-server-com-dominio-de-verdade/">Acessando meu Home Server com domínio de verdade</a></li>
<li><a href="/2025/09/10/protegendo-seu-home-server-com-cloudflare-zero-trust/">Protegendo seu Home Server com Cloudflare Zero Trust</a></li>
<li><a href="/2025/09/10/instalando-grafana-no-meu-home-server/">Instalando Grafana no meu Home Server</a></li>
<li><a href="/2025/09/10/omarchy-2-0-bitwarden-self-hosted-vaultwarden/">Vaultwarden self-hosted</a></li>
</ul>
]]></content:encoded><category>homelab</category><category>containers</category><category>devops</category><category>seguranca</category></item><item><title>Omarchy 2.0 - Bitwarden Self-Hosted / VaultWarden</title><link>https://akitaonrails.github.io/2025/09/10/omarchy-2-0-bitwarden-self-hosted-vaultwarden/</link><guid isPermaLink="true">https://akitaonrails.github.io/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;Continuando a &lt;a href="https://akitaonrails.github.io/tags/omarchy/"&gt;série de Omarchy&lt;/a&gt; e fazendo cross-over com a série de &lt;a href="https://akitaonrails.github.io/tags/homeserver"&gt;home server&lt;/a&gt;, vamos falar sobre Gerenciador de Senhas e 2FA. Quem me acompanha que eu falo frequentemente sobre segurança digital pessoal, a importância de prestar atenção em links pra não cair em golpes, phishing, nem instalar malware sem querer.&lt;/p&gt;
&lt;p&gt;Mais importante: ter uma senha forte, aleatória, exclusiva e diferente para &lt;strong&gt;CADA&lt;/strong&gt; site, app ou serviço que usa. Nunca, jamais, reusar uma senha. Por que? Porque todo dia um desses sites é invadido e as senhas são vazadas. Se você procurar por você mesmo no site &lt;a href="https://haveibeenpwned.com/"target="_blank" rel="noopener"&gt;Have I Been Pwned&lt;/a&gt; certamente vai achar contas suas que já vazaram. E se você compartilha essa mesma senha com 10 outros sites, alguém já deve ter entrado na sua conta a essa altura.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Continuando a <a href="/tags/omarchy/">série de Omarchy</a> e fazendo cross-over com a série de <a href="/tags/homeserver">home server</a>, vamos falar sobre Gerenciador de Senhas e 2FA. Quem me acompanha que eu falo frequentemente sobre segurança digital pessoal, a importância de prestar atenção em links pra não cair em golpes, phishing, nem instalar malware sem querer.</p>
<p>Mais importante: ter uma senha forte, aleatória, exclusiva e diferente para <strong>CADA</strong> site, app ou serviço que usa. Nunca, jamais, reusar uma senha. Por que? Porque todo dia um desses sites é invadido e as senhas são vazadas. Se você procurar por você mesmo no site <a href="https://haveibeenpwned.com/"target="_blank" rel="noopener">Have I Been Pwned</a> certamente vai achar contas suas que já vazaram. E se você compartilha essa mesma senha com 10 outros sites, alguém já deve ter entrado na sua conta a essa altura.</p>
<p>Assista meu video sobre segurança:</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>Ter preguiça de usar um gerenciador de senhas é só uma desculpa idiota de gente preguiçosa. E um programador sequer tem direito de usar essa desculpa.</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>Quem acompanha o DHH desde os primórdios de quando Macs eram a melhor plataforma pra desenvolvedores, lembra que ele sempre foi fã do 1Password, um dos que funcionava melhor em iPhones.</p>
<p>Eu migrei de volta pra Android e Linux muitos anos atrás, em 2015. Eu usava <strong>1Password</strong>, mas em Android o app era tenebroso de feio de ruim de usar. Por isso desisti dele e fiquei migrando entre vários outros apps. Seja Google Authenticator, seja Lastpass (que não recomendo).</p>
<p>Sei que tem diversos outros open source que as pessoas preferem, mas eu acabei caindo no Bitwarden, que é <strong>BONITO</strong> tanto em Android como a extension pra navegadores. Além disso ele tem componentes abertos e permite <strong>self-hosting</strong>.</p>
<p>Mais do que isso, ele não dificulta que você exporte todos os dados da sua conta e importe em outro lugar. Diferente de apps como o Twilio Authy, que te bloqueia de tudo e por isso deixei de usar.</p>
<p>Bitwarden tem gerador seguro de senhas aleatórias, tem suporte a <strong>2FA TOTP</strong>, dá pra armazenar anotações (tipo mini textos) seguras e muito mais. Os apps integram com biometria do celular, então é bem conveniente de usar.</p>
<p>Ainda mantenho meu Bitwarden dentro do <a href="https://www.samsung.com/global/sustainability/popup/popup_doc/AYUqoB6qDDMAIx_C/"target="_blank" rel="noopener">Secure Vault</a> da Samsung, o <strong>Samsung Knox Vault</strong>. Então são 2 níveis de segurança. E quando preciso andar em algum lugar suspeito, não levo meu celular principal, só um burner-phone sem nada de importante logado.</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>Olhem como é bonito. Eu gosto dele e uso já faz alguns anos. Minhas senhas variam entre 21 a 42 caracteres aleatórios, um diferente pra cada site. Eu atualizei todas as senhas de tudo mais de uma vez nos últimos anos, e todas as contas tem 2FA/TOTP cadastrado no Bitwarden também, assim consegui sair do Authy.</p>
<h3>Instalando VaultWarden Self-Hosted<span class="hx:absolute hx:-mt-20" id="instalando-vaultwarden-self-hosted"></span>
    <a href="#instalando-vaultwarden-self-hosted" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Mais importante, chegou a hora de hostear meu próprio servidor de Bitwarden, o <a href="https://github.com/dani-garcia/vaultwarden"target="_blank" rel="noopener"><strong>vaultwarden</strong></a> que é uma alternativa open source, código aberto no GitHub, então você mesmo pode fuçar pra aprender ou até contribuir com ele. Novamente, subo tudo com Docker Compose no meu Home Server, então só precisei adicionar este trecho no meu arquivo:</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>Importante:</p>
<ul>
<li>Crie um <code>ADMIN_TOKEN</code> longo e aleatório. Use o próprio Bitwarden pra isso, mantenha secreto.</li>
<li>Mude <code>SIGNUPS_ALLOWED</code> pra <code>true</code> somente uma vez, entre no site local e crie sua nova conta. Depois mude no arquivo pra <code>false</code>, reinicie o container. Dessa forma ninguém mais poderá criar nenhuma nova conta.</li>
<li>É recomendado configurar um GMAIL SMTP pra mandar emails de notificações. Pra isso precisa criar uma senha de aplicativo no Google.</li>
</ul>
<p>Pra criar uma senha exclusivamente pro vaultwarden, acesse <a href="https://myaccount.google.com/apppasswords"target="_blank" rel="noopener">Google App Passwords</a> e gere uma nova senha secreta exclusiva:</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>Ele só vai mostrar essa senha uma única vez. Copie e cole direto no yaml do docker compose. Configurando e-mail bonitinho, toda vez que alguém fizer login, ele manda um e-mail notificando. Isso é bom pra ter certeza que não tem ninguém suspeito mexendo no seu servidor:</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>Agora só preciso subir o 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>Roteando túnel do Cloudflared<span class="hx:absolute hx:-mt-20" id="roteando-túnel-do-cloudflared"></span>
    <a href="#roteando-t%c3%banel-do-cloudflared" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Como já mostrei no <a href="https://akitaonrails.com/2025/09/09/acessando-meu-home-server-com-dominio-de-verdade/"target="_blank" rel="noopener">post de Cloudflared</a> agora quero poder acessar remotamente, via túnel seguro, usando uma URL pública como <code>https://bitwarden.fabioakita.dev</code>. Pra fazer isso temos que editar aquele arquivo <code>/etc/cloudflared/config.yml</code> e adicionar no final dele assim:</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>Depois temos que reiniciar o serviço e também adicionar a rota manualmente:</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>E pronto! Talvez demore até 5 minutos pros DNS sincronizarem e tudo mais, mas daí é só entrar no seu navegador e vai ver isto:</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>Se aquele <code>SIGNUPS_ALLOWED</code> estiver <code>true</code> também vai ter um link pra &ldquo;Criar nova conta&rdquo;. No meu caso eu já fiz isso e desliquei, então só tem login mesmo.</p>
<h3>Importando seus Dados<span class="hx:absolute hx:-mt-20" id="importando-seus-dados"></span>
    <a href="#importando-seus-dados" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Antes de continuar, vamos abrir a extension do Bitwarden - que continua logado na versão cloud em bitwarden.com e exportar todos os meus dados:</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>Basta se autenticar, daí na barra inferior de ícones vai ter &ldquo;Settings&rdquo;, depois &ldquo;Vault Options&rdquo; e, finalmente &ldquo;Export vault&rdquo;. Deixe o padrão no formato JSON mesmo (vai ser um <strong>JSON encriptado</strong>) e faça download desse arquivo pra algum lugar seguro.</p>
<p>Agora podemos logar de volta na nossa versão self-hosted em <code>https://bitwarden.fabioakita.dev</code> e seguir pelo mesmo menu, mas escolher &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>E pronto! Agora minha versão self-hosted está <strong>idêntica</strong> à versão cloud que vinha usando até agora.</p>
<h3>Logando no novo servidor self-hosted<span class="hx:absolute hx:-mt-20" id="logando-no-novo-servidor-self-hosted"></span>
    <a href="#logando-no-novo-servidor-self-hosted" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Agora basta deslogar tanto do App mobile quanto da extension do navegador. Daí na tela de login vai ter a seguinte opção:</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>Na extension web a janela não deixa ver essa opção, use o scroll do mouse e scrole até o fim da janela e vai ter essa opção pequena. Escolha &ldquo;self-hosted&rdquo; e vai abrir uma janela pra digitar a URL <code>https://bitwarden.fabioakita.dev</code> e pronto!</p>
<p>A partir daqui o uso é exatamente o mesmo de antes. A interface é a mesma, as opções são as mesmas. Configure <strong>biometria</strong>, PIN, timeout pra lock e tudo mais pra deixar do jeito que você gosta.</p>
<p>Faça a mesma coisa no app mobile e pronto!</p>
<p>Nessa nova conta que criamos no servidor self-hosted, não esqueça de habilitar 2FA também, que é a opção &ldquo;Authenticator App&rdquo; em &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>Pra todas as outras contas, podemos registrar o 2FA dentro do Bitwarden. Mas pra conta master do próprio Bitwarden/Vaultwarden, precisamos usar um segundo app só pra isso.</p>
<p>Não sei se é o melhor, mas eu tenho gostado de usar o <a href="https://getaegis.app/"target="_blank" rel="noopener">Aegis Authenticator</a> no meu 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>É open source, o código está no <a href="https://github.com/beemdevelopment/aegis"target="_blank" rel="noopener">GitHub</a>, então parece minimamente confiável. Também deixo ele dentro do Samsung Knox Vault. Só preciso dele quando preciso re-logar no Bitwarden/Vaultwarden. Pra logar nos Google, Steam ou qualquer outra conta, o 2FA de cada uma está dentro do Vaultwarden mesmo:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20250910200449_2fa.png" alt="2FA"  loading="lazy" /></p>
<h3>Conclusão<span class="hx:absolute hx:-mt-20" id="conclusão"></span>
    <a href="#conclus%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Agora só faltou mesmo me logar com minha conta antiga em <strong>bitwarden.com</strong>, que é a versão &ldquo;oficial&rdquo; no cloud e deletar minha conta, pra garantir que ninguém mais tenha acesso.</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>E pronto, <strong>PERMANENTEMENTE APAGADO</strong>! Não existe mais minhas contas num servidor qualquer que eu não controlo, mesmo que fosse encriptado.</p>
<p>Estou bem contente com esta solução. Agora meu Vaultwarden está num container, os dados do home server tem backup automático pro meu NAS, tudo é encriptado e só tem acesso via túnel seguro da Cloudflare. O risco é bem baixo e ninguém tem chance de ter acesso aos meus dados, porque eles não estão mais no &ldquo;cloud&rdquo;.</p>
<p>Meus dados são somente meus.</p>
]]></content:encoded><category>omarchy</category><category>homelab</category><category>containers</category><category>seguranca</category></item><item><title>Instalando Grafana no meu Home Server</title><link>https://akitaonrails.github.io/2025/09/10/instalando-grafana-no-meu-home-server/</link><guid isPermaLink="true">https://akitaonrails.github.io/2025/09/10/instalando-grafana-no-meu-home-server/</guid><pubDate>Wed, 10 Sep 2025 16:00:00 -0300</pubDate><description>&lt;p&gt;Este post é pra continuar complementando minha &lt;a href="https://akitaonrails.github.io/tags/homeserver"&gt;série sobre meu Home Server&lt;/a&gt;, de novo, pra servir de anotação pra mim mesmo no futuro. Como já expliquei, meu home server é um mini-PC Intel NUC que eu só acesso com SSH. Então eu queria uma forma de monitorar o uso dos recursos do sistema sem ter que manualmente logar via SSH e abrir BTOP ou NTOP.&lt;/p&gt;
&lt;p&gt;Fiquei com preguiça de instalar Grafana na época, mas resolvi fazer isso agora, e foi bem mais fácil do que eu esperava, por isso resolvi compartilhar como fiz.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Este post é pra continuar complementando minha <a href="/tags/homeserver">série sobre meu Home Server</a>, de novo, pra servir de anotação pra mim mesmo no futuro. Como já expliquei, meu home server é um mini-PC Intel NUC que eu só acesso com SSH. Então eu queria uma forma de monitorar o uso dos recursos do sistema sem ter que manualmente logar via SSH e abrir BTOP ou NTOP.</p>
<p>Fiquei com preguiça de instalar Grafana na época, mas resolvi fazer isso agora, e foi bem mais fácil do que eu esperava, por isso resolvi compartilhar como fiz.</p>
<p>A idéia é subir <a href="https://github.com/google/cadvisor"target="_blank" rel="noopener">cAdvisor</a> que faz monitoramento de recursos de containers e <a href="https://github.com/prometheus/prometheus"target="_blank" rel="noopener">Prometheus</a> que faz scraping de dados de recursos do sistema e consolidar os dados dos dois no <a href="https://grafana.com/"target="_blank" rel="noopener">Grafana</a> que é uma aplicação web que permite montar dashboards gráficos pra monitorar seus sistemas. Se fizer direito, no final vai ter uma tela assim:</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>Instalação<span class="hx:absolute hx:-mt-20" id="instalação"></span>
    <a href="#instala%c3%a7%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Pra subir no meu home server é muito simples, eu faço tudo com Docker Compose, então:</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>Tem que prestar atenção nos mapeamentos de diretório local. Se colocar <code>/etc/grafana/prometheus.yml</code> por exemplo, tem que mudar esta linha:</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>Falando nele, eis o <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>Finalmente <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>Cuidado também com mapeamento de portas, o recomendado é fazer assim:</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>Isso porque cAdvisor e Prometheus só precisam estar acessíveis pro Grafana, dentro do home server, mas não tem porque dar bind em <code>0.0.0.0</code> e estar acessível pra fora do servidor. Aí é mais uma escolha. Como eu estou numa LAN caseira, e não tem túneis apontando pra esses serviços, pra mim tanto faz.</p>
<p>Se por acaso, como eu, também já tiver algum serviço pendurado na porta 8080, o cAdvisor vai falhar. Nesse caso remapeie as portas e reconfigure o Prometheus, ou só tire a linha de porta. Não precisa ter o cAdvisor exposto pra fora.</p>
<p>Outro detalhe é que por padrão Grafana sobe na porta 3000 mas eu mudei pra 3001 porque já tinha outro serviço nessa porta. Então, quando subir, eu posso acessar via <code>http://192.168.0.200:3001</code>.</p>
<p>Como expliquei no <a href="https://akitaonrails.com/2025/09/09/acessando-meu-home-server-com-dominio-de-verdade/"target="_blank" rel="noopener">artigo de Cloudflared</a> eu roteio esse endereço pro Cloudflare pra ter um domínio bonito como <code>https://grafana.fabioakita.dev</code> pra acessar tanto localmente quanto remotamente, se quiser. Depois leiam esses outros artigos pra entender como.</p>
<p>E pronto, agora é só subir os 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>E acabou, super simples, eu deveria ter feito isso antes.</p>
<p>Como podem ver arquivo de Docker Compose, quando entrar no site pela primeira vez, o login vai ser com <code>admin</code> senha <code>change-me</code>, obviamente, a primeira coisa a fazer é trocar a senha:</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>Agora praticamente acabou. Podemos checar se o Prometheus está funcionando e integrado. Basta ir no menu &ldquo;Connections&rdquo; e &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 consegue consolidar dados de dezenas de fontes diferentes, Prometheus é só um deles. Em uma infra maior e mais complicada, essa lista estaria enorme.</p>
<p>Pra montar Dashboards, dá pra ir do zero e montar manualmente widget a widget, mas é mais fácil importar dashboards que já existem. Pra isso é só clicar na lupa lá no topo, na caixa de procura e escolher a opção &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>Nessa página dá pra ou importar um arquivo JSON, ou copiar e colocar o JSON ou pesquisar pelo ID que já existe cadastrado no site 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>De cara, eu aprendi que existem 3 IDs que são úteis pra mim:</p>
<ul>
<li><strong>1860 - Node Exporter Full</strong> - que é a foto de tela que está no começo deste artigo e tem várias métricas do sistema como CPU, memória, disco, rede, etc. É o mais útil pra mim.</li>
<li><strong>13484 - Docker/cAdvisor Exporter</strong> - que são as métricas de CPU, memória, mas dividido por cada container rodando no servidor.</li>
<li><strong>3662 - Prometheus 2.0 overview</strong> - que é um resumo geral de saúde</li>
</ul>
<p>Se gostar desses ou de outros dashboard, depois é só favoritar com um &ldquo;pin&rdquo; e pronto.</p>
<p>Por exemplo, este é o dashboard do cAdvisor:</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>Eu ainda não explorei outros dashboards. Se tiver recomendações de IDs pra dar import, não deixe de mandar nos comentários abaixo!</p>
<p>Com isso eu tenho uma visão rápida e bonita do uso de recursos do meu home server e consigo ver rapidamente se tem algo de estranho acontecendo. Não parece que esses serviços consumam muitos recursos então posso deixar rodando junto com os demais containers.</p>
]]></content:encoded><category>homelab</category><category>containers</category><category>devops</category></item><item><title>Acessando meu Home Server com Domínio de Verdade</title><link>https://akitaonrails.github.io/2025/09/09/acessando-meu-home-server-com-dominio-de-verdade/</link><guid isPermaLink="true">https://akitaonrails.github.io/2025/09/09/acessando-meu-home-server-com-dominio-de-verdade/</guid><pubDate>Tue, 09 Sep 2025 17:00:00 -0300</pubDate><description>&lt;p&gt;Agora uma dica avançada que complementa meu artigo sobre meu &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;. Em resumo, além do meu desktop PC, que agora roda Omarchy, eu tenho um mini-PC (um simples Intel NUC, não precisa ser muito potente), exclusivamente pra rodar containers Docker.&lt;/p&gt;
&lt;p&gt;Eu tenho um bom Synology DS1821+ que também tem suporte pra rodar Docker, mas eu prefiro deixar o NAS somente pra funções de NAS e não arriscar comprometer meus dados com outras coisas rodando nele. Não é um &amp;ldquo;problema&amp;rdquo; ter um servidor só que faz tudo, mas já que eu posso, prefiro separar as coisas.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Agora uma dica avançada que complementa meu artigo sobre meu <a href="https://akitaonrails.com/2024/04/03/meu-netflix-pessoal-com-docker-compose/"target="_blank" rel="noopener">Home Server</a>. Em resumo, além do meu desktop PC, que agora roda Omarchy, eu tenho um mini-PC (um simples Intel NUC, não precisa ser muito potente), exclusivamente pra rodar containers Docker.</p>
<p>Eu tenho um bom Synology DS1821+ que também tem suporte pra rodar Docker, mas eu prefiro deixar o NAS somente pra funções de NAS e não arriscar comprometer meus dados com outras coisas rodando nele. Não é um &ldquo;problema&rdquo; ter um servidor só que faz tudo, mas já que eu posso, prefiro separar as coisas.</p>
<p>Esse Intel NUC roda um Ubuntu Server LTS headless (sem desktop manager), que eu uso só acessando via SSH (como expliquei no <a href="https://akitaonrails.com/2025/09/09/omarchy-2-0-entendendo-ssh-e-yubikeys/"target="_blank" rel="noopener">post anterior sobre SSH</a>). Nele rodo coisas como Portainer, Plex, Sonarr, Bazarr, qBitTorrent e muito mais.</p>
<p>Da forma como está eu só adicionei uma entrada no <code>/etc/hosts</code> do meu desktop assim:</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>A partir de agora todos os meus domínios e endereços IPs vão ser falsos, obviamente.</strong></p>
<p>Enfim, dessa forma eu posso abrir meu navegador e entrar em <code>http://plex.lan:32400</code> pra acessar meu Plex, por exemplo. Mas isso tem três desvantagens:</p>
<ol>
<li>Só no meu desktop consigo usar esses hostnames, do meu notebook precisaria editar o arquivo hosts também. Além disso preciso saber as portas pra cada container.</li>
<li>Não tenho SSL pra nada, então toda autenticação vai em texto aberto. Não é um problema muito grande porque o tráfego é somente interno na minha LAN local. Não tenho acessado de fora de casa.</li>
<li>Se por acaso eu quiser acessar de fora, nada disso funciona.</li>
</ol>
<p>Existem dezenas de formas de resolver esse problema, soluções que envolvem WireGuard, ou soluções comerciais como TailScale ou coisas assim. Mas tem uma solução razoavelmente simples que resolve todos os problemas de uma só vez: <strong>Cloudflare Tunnels</strong>.</p>
<p>A idéia é criar túneis seguros via Cloudflare, pra ter acesso aos meus serviços na minha LAN interna <strong>SEM</strong> precisar expor portas pra fora do meu roteador ou coisas assim. Minha rede permanece isolada e inalterada.</p>
<h3>1. Registrando Domínio<span class="hx:absolute hx:-mt-20" id="1-registrando-domínio"></span>
    <a href="#1-registrando-dom%c3%adnio" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>A primeira recomendação é registrar um domínio de verdade. Fazendo isso é possível ter certificados SSL de verdade que a própria Cloudflare gerencia e renova automaticamente.</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>Eu tenho alguns domínios registrados e não vou revelar qual uso pro meu Home Server. Vou usar esse vazio <strong>fabioakita.dev</strong> como exemplo só pra este post.</p>
<p>Domínios são baratos, faixa de USD 12 por ano, dependendo do nome que escolher. Escolha um curto e fácil de digitar.</p>
<h3>2. Instalando Cloudflared<span class="hx:absolute hx:-mt-20" id="2-instalando-cloudflared"></span>
    <a href="#2-instalando-cloudflared" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Com o domínio criado, agora precisamos instalar o serviço <a href="https://pkg.cloudflare.com/index.html"target="_blank" rel="noopener"><strong>cloudflared</strong></a> no Home Server. Ele é quem vai se conectar aos servidores da Cloudflare a abrir o túnel pra dentro do meu servidor local. Clique no link pra instruções de como instalar, mas num Ubuntu Server 22.04 é assim:</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>Agora precisamos fazer login:</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"># adicione tunel homelab</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"># crie diretório pra configurações</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 as mesmas credenciais que usa pra logar no website da Cloudflare. Se tudo der certo, no final ele vai criar um arquivo em <code>/root/.cloudflared/xxxxx-yyyy-zzzz...json</code>, são suas credenciais, assim como uma chave privada SSH, não jogue num pendrive ou num drive de cloud da vida. Guarde muito bem. Vamos mover:</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>Finalmente, precisamos criar um arquivo de configurações <code>/etc/cloudflared/config.yml</code> com todos os hostnames que queremos pra cada um dos containers, apontando pras portas corretas. Vou dar um trecho de exemplo da minha config, modificado com IPs falsos e o domínio falso:</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>Como podem ver, é um arquivo extremamente simples. Acho que qualquer um consegue entender o que isso faz. Modifique pras suas próprias necessidades.</p>
<p>Depois de salvar o arquivo e voltar pra linha de comando, precisa manualmente rodar este comando pra cada 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>Mas isso é muito trabalhoso, com esse tanto de hostname, achei mais fácil criar um script <code>/etc/cloudflared/add.sh</code> assim:</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>Agora é só rodar: <code>sudo sh /etc/cloudflared/add.sh</code>, <strong>só uma vez!</strong> Então garanta que seu arquivo de config está correto antes!</p>
<p>Finalmente, podemos configurar o serviço pra reiniciar sozinho em todo 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. Acessando seus serviços via túnel<span class="hx:absolute hx:-mt-20" id="3-acessando-seus-serviços-via-túnel"></span>
    <a href="#3-acessando-seus-servi%c3%a7os-via-t%c3%banel" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Se nada deu errado, já acabou! Já pode ir direto pro seu navegador e testar:</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>Olha só que beleza. Note na barra de URL que digitei <code>https://immich.fabioakita.dev</code> e acessou normalmente. Aliás, se não conhecia o <a href="https://immich.app/"target="_blank" rel="noopener">Immich</a> funciona idêntico a um Google Photos, só que sob seu total controle! Tem app pra smartphone e sincroniza bonitinho.</p>
<p>Mais importante, note que está com <strong>HTTPS</strong> que foi provido pelo Cloudflare pro meu domínio inteiro. Quando fizer login, vai ser via <strong>túnel encriptado</strong>. E mais do que isso: mesmo se eu estiver via 5G, fora de casa, no meu celular, vou poder acessar minhas fotos usando esse endereço!</p>
<p>Do meu celular vai abrir um túnel seguro pra Cloudflare, e aquele serviço <code>cloudflared</code> no meu home server está também com um túnel seguro aberto pra Cloudflare, daí ele faz uma ponte entre os dois túneis e pronto! Tenho acesso remoto sem abrir meu servidor direto pra internet! O melhor dos dois mundos!</p>
<p>Não se esqueça de configurar todos os seus serviços com senhas fortes (eu gero elas via Bitwarden, mas qualquer um server 1Password, o que for):</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 pra vlans<span class="hx:absolute hx:-mt-20" id="4-bridge-pra-vlans"></span>
    <a href="#4-bridge-pra-vlans" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Eu não lembro exatamente o motivo, mas só meu container de Plex deu mais trabalho. Isso porque ele usa uma VLAN diferente das demais. Novamente, não lembro porque fiz isso na época, mas está assim:</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>Por causa disso o <code>cloudflared</code> não consegue rotear pra esse container. A forma de consertar isso é criando uma ponte dessa vlan pra minha 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>Obviamente, trocar <code>enp3s0</code> pelo nome correto da sua ethernet. Cheque com <code>ip addr</code> ou <code>ip link</code> por exemplo. Só de rodar os comandos acima, se eu for no navegador e digitar <code>https://plex.fabioakita.dev</code> já deve conseguir acessar. Pra tornar isso permanente, precisamos do 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>E agora, só reiniciar os serviços:</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>O &ldquo;correto&rdquo; seria meu Plex estar configurado com <code>network_mode: host</code>mas, de novo, não lembro porque não estava e não estava a fim de desconfigurar tudo pra descobrir. Caso alguém tenha configurações similares, é assim que bypassa.</p>
<p>Falando em Plex, eu já tinha ouvido falar que tem que tomar cuidado com streaming via cloudflared, não sei se tem limites ou custos extras. Achei <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">este artigo</a> que fala mais sobre isso mas não parei pra ver a fundo. A única coisa que fiz extra, no dashboard da Cloudflare, foi adicionar uma regra pra não fazer <strong>CACHING</strong> do subdomínio <code>plex</code>, pra evitar custos adicionais.</p>
<h3>Conclusão<span class="hx:absolute hx:-mt-20" id="conclusão"></span>
    <a href="#conclus%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Como podem ver, é extremamente fácil ter um home server, isolar tudo com Docker e depois expor seus containers via túnel seguro pra acessar remotamente. Nada disso significa segurança instantânea. Garanta que seus containers sempre estejam atualizados usando serviços como <a href="https://github.com/containrrr/watchtower"target="_blank" rel="noopener">Watchtower</a>, garanta que cada serviço tenha senhas fortes aleatórias e únicas - não use a mesma senha pra tudo. Garanta que seu firewall esteja bem configurado e ativo, mesmo numa LAN.</p>
<p>Muitos routers de Wi-Fi modernos permitem criar uma VLAN separada pra Wi-Fi pra visitas. Assim eles podem usar seu Wi-Fi mas não enxergam seus computadores, que vão estar logicamente isoladas em outra VLAN. Cheque a documentação do seu roteador. Eu uso uma <strong>TP-Link BE1900 Tri-Band Wi-Fi 7</strong>, mas pra maioria das pessoas ela é cara demais e excessiva demais. Pesquise bem. E não adianta ficar me perguntando de outros modelos: eu não testei todos pra saber as diferenças.</p>
<p>Se você viu nomes como Radarr, Sonarr e não sabe o que isso significa, leia o <a href="https://wiki.servarr.com/"target="_blank" rel="noopener">Wiki do Servarr</a> que consolida todas as documentações desses serviços num único lugar. É a infraestrutura que permite você ter sua própria &ldquo;Netflix Particular&rdquo;.</p>
<p>Também não perguntem porque eu uso Plex e não Jellyfin. É escolha pessoal, escolha o que é melhor pra você. Eu gosto de Plex, já usei Jellyfin, e não pretendo mudar.</p>
<p>Eu odeio a idéia de pagar assinaturas pra tudo e não ter propriedade de nada. Acho dinheiro jogado fora. Eu prefiro <strong>TER</strong> as minhas próprias coisas sob <strong>MEU CONTROLE</strong>. É mais caro, é mais trabalhoso. Conveniência demais significa que você não tem controle de nada e sempre depende dos outros.</p>
<p>Eu odeio depender dos outros.</p>
]]></content:encoded><category>homelab</category><category>redes</category><category>containers</category></item><item><title>Omarchy 2.0 - Mise pra organizar ambientes de desenvolvimento</title><link>https://akitaonrails.github.io/2025/09/07/omarchy-2-0-mise-pra-organizar-ambientes-de-desenvolvimento/</link><guid isPermaLink="true">https://akitaonrails.github.io/2025/09/07/omarchy-2-0-mise-pra-organizar-ambientes-de-desenvolvimento/</guid><pubDate>Sun, 07 Sep 2025 16:40:00 -0300</pubDate><description>&lt;p&gt;Continuando meus posts sobre &lt;a href="https://www.akitaonrails.com/tags/omarchy"target="_blank" rel="noopener"&gt;Omarchy&lt;/a&gt; mais um post introdutório, mas agora sobre &lt;a href="https://mise.jdx.dev/getting-started.html"target="_blank" rel="noopener"&gt;Mise-en-place&lt;/a&gt; que já vem pré-instalado. No Omarchy, se digitar &amp;ldquo;Super+Alt+Espaço&amp;rdquo; vai abrir o menu principal. Escolha &amp;ldquo;Install&amp;rdquo; e depois &amp;ldquo;Development&amp;rdquo; e vai aparecer várias linguagens/frameworks que pode instalar, como Ruby on Rails ou Go.&lt;/p&gt;
&lt;p&gt;Mas não precisa usar estes menus. Vamos entender.&lt;/p&gt;
&lt;p&gt;Num Arch Linux ou Ubuntu da vida, um amador instalaria ruby ou python fazendo algo assim:&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;pre&gt;&lt;code&gt;# arch
yay -S ruby python3
# ubuntu
apt install ruby python3&lt;/code&gt;&lt;/pre&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;p&gt;&lt;strong&gt;Este é o jeito ERRADO.&lt;/strong&gt;&lt;/p&gt;</description><content:encoded><![CDATA[<p>Continuando meus posts sobre <a href="https://www.akitaonrails.com/tags/omarchy"target="_blank" rel="noopener">Omarchy</a> mais um post introdutório, mas agora sobre <a href="https://mise.jdx.dev/getting-started.html"target="_blank" rel="noopener">Mise-en-place</a> que já vem pré-instalado. No Omarchy, se digitar &ldquo;Super+Alt+Espaço&rdquo; vai abrir o menu principal. Escolha &ldquo;Install&rdquo; e depois &ldquo;Development&rdquo; e vai aparecer várias linguagens/frameworks que pode instalar, como Ruby on Rails ou Go.</p>
<p>Mas não precisa usar estes menus. Vamos entender.</p>
<p>Num Arch Linux ou Ubuntu da vida, um amador instalaria ruby ou python fazendo algo assim:</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>Este é o jeito ERRADO.</strong></p>
<p>Esses comandos vão instalar a versão mais nova de cada linguagens e toda vez que atualizar o sistema com <code>yay -Syu</code> ou <code>apt upgrade</code>, vai puxar as versões mais novas ainda.</p>
<p>Só que se você desenvolve projetos de verdade, <strong>não quer que as versões mudem</strong>.</p>
<p>A versão do deploy no servidor no cloud e a versão na sua máquina local <strong>TEM QUE SER AS MESMAS</strong>.</p>
<p>Se está usando Ruby v3.0.7 e Node.js 22.19.0, não pode ficar refém que quando atualizar seu sistema, do nada vai aparecer Ruby 3.4.5 e Node 24.7.0. Tudo vai começar a quebrar e você vai ficar super confuso sem saber porque. Esse é o erro mais primário que um júnior comete.</p>
<p>Pra evitar isso, cada projeto que está trabalhando PRECISA ESTAR TRAVADO EM VERSÕES FIXAS.</p>
<p>O jeito mais fácil de fazer isso em 2025 é usando <strong>MISE</strong>. Como falei, ele já vem instalado e ativado no Omarchy, mas se estiver usando outra distro basta instalar Mise manualmente:</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>Adapte isso pra ZSH, Fish ou outro shell que estiver usando, leia a <a href="https://mise.jdx.dev/installing-mise.html"target="_blank" rel="noopener">documentação</a></p>
<p>Agora navegue pro diretório do seu projeto, por exemplo:</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>E trave as versões:</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>Isso vai criar o arquivo <code>.tool-versions</code> que você deve adicionar no git do seu repositório:</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>E pronto, toda vez que fizer <code>cd</code> pro diretório, o Mise vai ativar exatamente a versão correta pra esse projeto. Veja:</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>Note que, se estiver usando o prompt <a href="https://akitaonrails.com/2025/09/07/omarchy-2-0-zsh-configs/"target="_blank" rel="noopener">Starship</a> como expliquei no post anterior, com um preset como Pure Prompt, ele mostra no prompt as versões de cada linguagem pra que fique fácil de ver se estiver numa versão errada.</p>
<p>No caso, se leu com atenção, já deve ter notado que <strong>SIM, ESTOU NA VERSÃO ERRADA</strong> do Node.js. Olhem como no prompt ele fala que estou usando Node 24.7.0 mas no arquivo <code>.tool-versions</code> ele pede 14.21.3.</p>
<p>O motivo de estar &ldquo;missing&rdquo; é porque esta máquina é recém-instalada, mas o projeto é velho.</p>
<p>Então vamos checar:</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>Veja como na listagem de <code>mise list</code> ele fala que 14.21.3 está &ldquo;missing&rdquo;. Então vamos instalar:</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>Com o comando <code>mise use</code> podemos pedir pra instalar e usar uma versão em particular. Se já não estiver instalado, ele vai instalar. Note como depois disso o prompt do Starship mudou pra refletir que agora estamos usando 14.21.3. Esse é o jeito certo.</p>
<p>Se não estiver necessariamente num diretório de projeto com versões travadas, ainda assim é possível rodar comandos usando uma versão específica, assim:</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>Digamos que queira gerar um projeto de Rails numa versão específica de Ruby:</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>Mesma coisa vale pra generators em Node, como Next.js ou qualquer outro framework.</p>
<p>Não chequei se o LazyVim no Omarchy já vem configurado assim, mas caso não venha, pro NeoVim suportar Mise precisa adicionar isso:</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>Basta colocar isso em <code>~/.config/nvim/lua/config/mise.lua</code> que ele deve carregar o path correto. Pra integrar com outros editores, cheque <a href="https://mise.jdx.dev/ide-integration.html"target="_blank" rel="noopener">esta documentação</a>. Tem um MONTE de opções avançadas que é bom ler a respeito na <a href="https://mise.jdx.dev/dev-tools/"target="_blank" rel="noopener">seção de Dev-Tools</a> mas o básico é o que listei aqui.</p>
<h2>E bancos de dados??<span class="hx:absolute hx:-mt-20" id="e-bancos-de-dados"></span>
    <a href="#e-bancos-de-dados" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O mesmo vale pra bancos de dados. Cada projeto deve estar travado numa versão específica idêntica à que está rodando no servidor de produção e nunca deve depender dos pacotes do sistema operacional. Dá pra usar mise pra isso mas o mais recomendado é usar Docker. Cada projeto <strong>DEVE</strong> ter um <code>docker-compose.yml</code> pra subir somente os bancos de dados. Não precisa subir containers pra linguagens, pra isso é melhor usar Mise.</p>
<p>Se não entende Docker, eu fiz videos ensinando:</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>Somando Mise + Docker Compose + LazyVim, é tudo que se precisa pra ser um desenvolvedor web produtivo, além de usar os recursos da sua máquina de maneira eficiente. Todo desenvolvedor PRECISA aprender este combo de opções. Omarchy já trás tudo pré-instalado, por isso tem sido minha recomendação pra todo iniciante.</p>
<p>Não precisa se preocupar: Arch Linux (com archinstall), é tão fácil de instalar quanto um Ubuntu ou Linux Mint e o Hyprland do Omarchy é muito mais bonito e fluido que um MacOS. O melhor dos mundos. Aprenda hoje!</p>
]]></content:encoded><category>omarchy</category><category>ferramentas-de-desenvolvimento</category><category>containers</category></item><item><title>Como Contribuir no Blog do AkitaOnRails usando Docker</title><link>https://akitaonrails.github.io/2025/08/29/como-contribuir-no-blog-do-akitaonrails-usando-docker/</link><guid isPermaLink="true">https://akitaonrails.github.io/2025/08/29/como-contribuir-no-blog-do-akitaonrails-usando-docker/</guid><pubDate>Fri, 29 Aug 2025 22:37:16 -0300</pubDate><description>&lt;p&gt;Contribuir em projetos open source pode ser um saco quando você precisa configurar um ambiente de desenvolvimento complexo. No blog do AkitaOnRails, isso significava instalar Hugo Extended, Go, Ruby e suas dependências. Mas agora temos uma solução muito mais simples: &lt;strong&gt;Docker&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;O Problema&lt;span class="hx:absolute hx:-mt-20" id="o-problema"&gt;&lt;/span&gt;
&lt;a href="#o-problema" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Antes, pra contribuir no blog você precisava:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Instalar Hugo Extended&lt;/li&gt;
&lt;li&gt;Instalar Go&lt;/li&gt;
&lt;li&gt;Instalar Ruby&lt;/li&gt;
&lt;li&gt;Configurar todas as dependências&lt;/li&gt;
&lt;li&gt;Gerar o índice de posts manualmente&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Isso criava barreiras pra novos contribuidores e podia causar conflitos com outras instalações no sistema. Um inferno.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Contribuir em projetos open source pode ser um saco quando você precisa configurar um ambiente de desenvolvimento complexo. No blog do AkitaOnRails, isso significava instalar Hugo Extended, Go, Ruby e suas dependências. Mas agora temos uma solução muito mais simples: <strong>Docker</strong>.</p>
<h2>O Problema<span class="hx:absolute hx:-mt-20" id="o-problema"></span>
    <a href="#o-problema" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Antes, pra contribuir no blog você precisava:</p>
<ol>
<li>Instalar Hugo Extended</li>
<li>Instalar Go</li>
<li>Instalar Ruby</li>
<li>Configurar todas as dependências</li>
<li>Gerar o índice de posts manualmente</li>
</ol>
<p>Isso criava barreiras pra novos contribuidores e podia causar conflitos com outras instalações no sistema. Um inferno.</p>
<h2>A Solução: Docker<span class="hx:absolute hx:-mt-20" id="a-solução-docker"></span>
    <a href="#a-solu%c3%a7%c3%a3o-docker" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Agora você só precisa do Docker e Docker Compose. Tudo o resto roda em containers isolados.</p>
<h3>Setup Rápido<span class="hx:absolute hx:-mt-20" id="setup-rápido"></span>
    <a href="#setup-r%c3%a1pido" 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 o repositório</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"># Inicie o ambiente</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"># Acesse o 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>Pronto! O blog está rodando e você pode começar a contribuir.</p>
<h2>Comandos Úteis<span class="hx:absolute hx:-mt-20" id="comandos-úteis"></span>
    <a href="#comandos-%c3%bateis" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O projeto inclui scripts que facilitam o desenvolvimento:</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"># Ver todos os comandos disponíveis</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"># Criar um novo 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"># Ver logs do servidor</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"># Gerar índice de posts</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"># Parar o ambiente</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>Criando um Post<span class="hx:absolute hx:-mt-20" id="criando-um-post"></span>
    <a href="#criando-um-post" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Criar um novo post é super simples:</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>O script automaticamente:</p>
<ul>
<li>Cria a estrutura de diretórios com a data atual</li>
<li>Gera um arquivo <code>index.md</code> com front matter básico</li>
<li>O servidor Hugo recarrega automaticamente</li>
</ul>
<h2>Estrutura de um Post<span class="hx:absolute hx:-mt-20" id="estrutura-de-um-post"></span>
    <a href="#estrutura-de-um-post" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O arquivo criado terá esta estrutura:</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>Vantagens do Docker<span class="hx:absolute hx:-mt-20" id="vantagens-do-docker"></span>
    <a href="#vantagens-do-docker" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><h3>Para Contribuidores<span class="hx:absolute hx:-mt-20" id="para-contribuidores"></span>
    <a href="#para-contribuidores" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><ul>
<li><strong>Setup rápido</strong>: Apenas Docker necessário</li>
<li><strong>Ambiente isolado</strong>: Não interfere com sistema local</li>
<li><strong>Hot reload</strong>: Mudanças aparecem automaticamente</li>
<li><strong>Reproduzível</strong>: Funciona igual para todos</li>
</ul>
<h3>Para o Projeto<span class="hx:absolute hx:-mt-20" id="para-o-projeto"></span>
    <a href="#para-o-projeto" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><ul>
<li><strong>Menos issues</strong>: Ambiente consistente</li>
<li><strong>Mais contribuidores</strong>: Barreira de entrada menor</li>
<li><strong>Manutenção</strong>: Fácil de atualizar dependências</li>
</ul>
<h2>Como Funciona<span class="hx:absolute hx:-mt-20" id="como-funciona"></span>
    <a href="#como-funciona" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O ambiente Docker inclui:</p>
<ul>
<li><strong>Hugo Extended</strong>: Gerador de sites estáticos</li>
<li><strong>Go</strong>: Para módulos Hugo</li>
<li><strong>Ruby</strong>: Para script de geração de índice</li>
<li><strong>Volumes</strong>: Para hot reload dos arquivos</li>
</ul>
<p>O <code>docker-compose.yml</code> mapeia os diretórios locais para o container, então quando você edita um arquivo, o Hugo detecta a mudança e recarrega automaticamente.</p>
<h2>Desenvolvimento Local (Alternativa)<span class="hx:absolute hx:-mt-20" id="desenvolvimento-local-alternativa"></span>
    <a href="#desenvolvimento-local-alternativa" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Se preferir não usar Docker, ainda é possível instalar tudo localmente:</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"># Gerar índice</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"># Rodar servidor</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>Mas o Docker é muito mais prático!</p>
<h2>Próximos Passos<span class="hx:absolute hx:-mt-20" id="próximos-passos"></span>
    <a href="#pr%c3%b3ximos-passos" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><ol>
<li><strong>Teste o ambiente</strong>: Execute <code>./scripts/dev.sh start</code></li>
<li><strong>Crie um post</strong>: Use <code>./scripts/dev.sh new-post</code></li>
<li><strong>Explore o código</strong>: Veja como personalizar layouts</li>
<li><strong>Contribua</strong>: Faça um Pull Request</li>
</ol>
<hr>
<p><strong>Dica</strong>: O ambiente Docker salva automaticamente suas mudanças e recarrega o servidor. Basta editar os arquivos e ver as mudanças em tempo real! 🚀</p>
]]></content:encoded><category>open-source</category><category>containers</category><category>tutoriais</category></item><item><title>Gerando Imagens com I.A - até estilo Ghibli 😂 - com Docker e CUDA</title><link>https://akitaonrails.github.io/2025/04/20/gerando-imagens-com-i-a-ate-estilo-ghibli-com-docker-e-cuda/</link><guid isPermaLink="true">https://akitaonrails.github.io/2025/04/20/gerando-imagens-com-i-a-ate-estilo-ghibli-com-docker-e-cuda/</guid><pubDate>Sun, 20 Apr 2025 17:30:00 -0300</pubDate><description>&lt;p&gt;Este tema vai ser dividido em dois posts. Neste é só técnico de como fazer rodar, no outro vou explicar o que diabos é um ComfyUI e mais ou menos como usar.&lt;/p&gt;
&lt;p&gt;Mais de ano atrás, quando fiz meus últimos videos sobre I.A. e nos podcasts eu sempre falava que rodava tudo na minha máquina. Mas nunca detahei como. Então hoje consertei isso com vários posts de blog e projetinhos no GitHub com Dockerfiles pra você mesmo rodar na sua própria máquina, sem configurar nada difícil.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Este tema vai ser dividido em dois posts. Neste é só técnico de como fazer rodar, no outro vou explicar o que diabos é um ComfyUI e mais ou menos como usar.</p>
<p>Mais de ano atrás, quando fiz meus últimos videos sobre I.A. e nos podcasts eu sempre falava que rodava tudo na minha máquina. Mas nunca detahei como. Então hoje consertei isso com vários posts de blog e projetinhos no GitHub com Dockerfiles pra você mesmo rodar na sua própria máquina, sem configurar nada difícil.</p>
<p>Finalmente fiz um <a href="https://github.com/akitaonrails/ComfyUI-Docker-CUDA-preloaded"target="_blank" rel="noopener">Dockerfile</a> pra ComfyUI, que é a melhor interface gráfica pra edição de workflows de geração de imagens. Se você já trabalhou com Davinci Resolve Fusion ou Blender Geometry Nodes da vida, workflows são basicamente configurações de nós pra processar suas imagens, áudio, video e ir passando por processamentos não só de I.A.. É como programar visualmente. Por isso é uma ferramenta muito mais complicada do que o FramePack que mostrei no post anterior. Não é só clicar.</p>
<p>Recentemente todo mundo ficou histérico com as atualizações do ChatGPT e como ele <em>&ldquo;OH MY GOD&rdquo;</em> consegue gerar imagens em estilo de anime da Ghibli e ficaram spammando o mundo com memes idiotas em estilo anime. Histeria online é um saco mesmo. Mas como eu falei, por mais impressionante que seja, é isso: um gerador de memes. Não dá pra usar profissionalmente.</p>
<p>Cansei de tentar subir fotos minhas ou até fotos genéricas, de personagens fictícios, alguns até desenhos meus, e vira e mexe, aleatoriamente, o ChatGPT se recusa a gerar a imagem. Alegando privacidade, segurança, copyright e tudo mais que ele conseguir inventar de desculpa. Não dá pra confiar nisso pra trabalhar. Quem é profissional deveria ter controle completo sobre o processo e é isso que vou ensinar hoje.</p>
<p>Pra começar, clone meu <a href="https://github.com/akitaonrails/ComfyUI-Docker-CUDA-preloaded"target="_blank" rel="noopener">repositório no GitHub</a> e siga as instruções do README ou faça isto:</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>Sendo mais complexo, precisei colocar um Docker Compose desta vez. Então pra subir, depois do build finalizar, só fazer isso:</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>Agora vai demorar, porque por padrão ele vai baixar quase <strong>500GB</strong> de modelos pré-treinados pra conseguir fazer quase tudo de imagens. Sim, pra brincar com I.A. localmente precisa ter infra. Ou se não, alugar máquina parruda com muito armazenamento numa RunPod da vida e rodar remoto. Tem como baixar menos modelos, vou explicar a seguir.</p>
<p>No Docker Compose. Recomendo evitar a opção &ldquo;-d&rdquo; de subir como daemon, porque é bom reservar um terminal pra ficar vendo os outputs (você pode dar <code>docker compose logs --follow</code>, se quiser também, mas é mais fácil direto).</p>
<p>O ComfyUI é um programa mais complexo que a média, muito mais que um FramePack da vida. Ele tem suporte a instalar &ldquo;extensions&rdquo; (plugins, addons, chame como quiser). E na verdade ele sozinho não faz muita coisa, precisa instalar várias extensions pra começar a ficar realmente útil. Vira e mexe alguma dessas extensions não trata erros direito e estoura no console, algo como isso:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/n7w5oxun51elrsv2fra46f684xre" alt="Console Error"  loading="lazy" /></p>
<p>É útil sempre ficar de olho no console pra descobrir se ele tentou carregar um arquivo que não existe. Daí dá pra pegar o nome do arquivo e buscar no Google, baixar e colocar no lugar certo. Ou se é uma extension muito bugada que é melhor tirar do diretório &ldquo;custom_nodes&rdquo;, e assim por diante. Sem ver o console, não aparece na interface web e você vai ficar a ver navios.</p>
<p>Muito dos modelos e extensions eu me baseei na configuraçãol <strong>Ultimate ComfyUI versão 3</strong> recomendada pelo <strong>Aitrepreneur</strong>, de novo. Assista <a href="https://youtu.be/q5kpr84uyzc"target="_blank" rel="noopener">este video dele</a> pra ter todos os detalhes e tudo que é possível fazer. Ele tem os scripts pra Linux, Windows, e os workflow na versão paga do Patreon dele e, claro, não vou compartilhar os workflows porque não são meus. Em teoria, minha config deve ser capaz de fazer o que ele ensina.</p>
<p>O workflow V3 do Aitrepreneur é muito ph0da. Mas eu não posso compartilhar porque é material fechado no Patreon dele, mas eis uma foto de tela do meu ComfyUI com tudo dele carregado:</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>Mas a config em si, eu refiz inteira pra funcionar neste Docker. Dá pra carregar qualquer outro workflow aberto que achar num Reddit da vida.</p>
<p>O que eu fiz foi o seguinte:</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>No projeto tem que ter um diretório &ldquo;models&rdquo;, se não tiver crie, porque o docker compose vai mapear pra dentro do container. Quando iniciar, ele vai rodar o arquivo <a href="https://github.com/akitaonrails/ComfyUI-Docker-CUDA-preloaded/blob/master/init_models.sh"target="_blank" rel="noopener">init_models.sh</a>. Esse script vai carregar um arquivo de configuração que você pode editar depois: <a href="https://github.com/akitaonrails/ComfyUI-Docker-CUDA-preloaded/blob/master/models.conf"target="_blank" rel="noopener">models.conf</a>. Ele é enorme e um pedaço dele é assim:</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>Cada seção mapeia pra um sub-diretório, então &ldquo;[SAMS]&rdquo; vai mapear pra &ldquo;models/sams&rdquo; e baixar os binários nos lugares certos. Tem alguns especiais nas seções &ldquo;[GIT_REPOS]&rdquo;, para os que podem ser clonados via git ou &ldquo;[CUSTOM]&rdquo; pra arquivos que precisa baixar depois dentro desses projetos clonados, por exemplo. Não sei ainda se dá pra simplificar, mas isso eu peguei na config do Aitrepreneur.</p>
<p>O ideal é sempre manter essa lista organizada com modelos que acharem depois. Se quiserem contribuir também seria bacana. O motivo é que fica tudo espalhado em tudo que é site por aí, você baixa manualmente, daí move pro diretório dentro de &ldquo;models&rdquo;. Amanhã, se reinstalar sua máquina, e pra lembrar todos os sites? São dezenas de modelos disponíveis.</p>
<p>No outro post vou tentar resumir o que diabos são &ldquo;checkpoints&rdquo;, &ldquo;diffusion models&rdquo;, &ldquo;loras&rdquo;, &ldquo;controlnet&rdquo; e tudo mais. Mas por enquanto, a extensão dos arquivos não diz muita coisa. Dizer &ldquo;.pt&rdquo; por exemplo só quer dizer que é um arquivo que o PyTorch consegue abrir, mas não diz se é um clip ou um text encoder, por exemplo. Extensão &ldquo;.safetensors&rdquo; mesma coisa, pode ser qualquer coisa, e o ComfyUI precisa que cada arquivo esteja no sub-diretório correto pra conseguir usar depois, não pode sair movendo arquivo aleatoriamente.</p>
<p>O script &ldquo;init_models.sh&rdquo; roda toda vez que o Docker é reiniciado e é inteligente pra só baixar URLs que já não estão baixadas no diretório &ldquo;models&rdquo;. Por isso sempre roda quando o container sobe, no entrypoint. Essa é uma das vantagens da minha config: ela é tem manutenção automática.</p>
<h3>Extensions e Dependências<span class="hx:absolute hx:-mt-20" id="extensions-e-dependências"></span>
    <a href="#extensions-e-depend%c3%aancias" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Eu fiz um segundo script chamado <a href="https://github.com/akitaonrails/ComfyUI-Docker-CUDA-preloaded/blob/master/init_extensions.sh"target="_blank" rel="noopener">init_extensions.sh</a>, que também roda no entrypoint do container e você não deve mexer, mas ele carrega um outro arquivo de configuração chamado <a href="https://github.com/akitaonrails/ComfyUI-Docker-CUDA-preloaded/blob/master/extensions.conf"target="_blank" rel="noopener">extensions.conf</a> e, esse sim, você pode mexer e adicionar novas extensions depois. Um pedaço dele é assim:</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>Aqui eu declaro todas as extensions que adicionam novas funcionalidades em cima do ComfyUI, em particular novos <strong>NODES</strong>, por isso ficam no diretório &ldquo;custom_nodes&rdquo;, que é um volume externo ao Docker e por isso é persistente, ou seja, toda nova extension sobrevive restarts do container.</p>
<p>Mas tem ainda um porém GRANDE: <strong>Dependências de Python</strong>. Um pesadelo.</p>
<p>A build da Imagem Docker faz um monte de <code>pip install</code> e como estou usando Virtual ENV, vai tudo instalado em <code>/venv</code> dentro do container, como deveria.</p>
<p>Porém, as extensions que são instaladas fora da build - toda vez que o container reinicia - costumam ser código Python e também tem mais dependências. Extensions são projetos no GitHub e podem estar mudando neste exato momento. Se mudar e fizer um <code>git pull</code> pra atualizar, precisa rodar o <code>pip install -r requirements.txt</code>. Mas se tiver dezenas de extensions, rodar <code>pip install</code>, de cada um, em cada restart do Docker é uma demora grande pra não fazer quase nada. Como eu disse, pesadelo.</p>
<p>Minha solução - pra me gabar um pouco - conserta boa parte dessa manutenção. Quando o script <code>init_extensions.sh</code> executa, se uma extension da lista já estiver instalada, ele vai pular. Daí vai gravar no diretório <code>./custom_nodes/.last_commit/*.commit</code> o último commit de cada extension. Quando o Docker reiniciar, ele vai dar um <code>git fetch</code> e checar se o hash do último commit mudou.</p>
<p>Só se mudou, ele vai rodar <code>pip install</code> pra essa extension que ganhou atualização e pular todas as outras que não mudaram desde o último restart, economizando MUITO tempo e evitando que você precisa manualmente atualizar as coisas. Como estou usando VENV, vai ficar tudo persistido no volume &ldquo;/venv&rdquo;. Sim, é um VOLUME, porque ela muda depois do build a cada atualização de extension.</p>
<p>Dessa forma, tudo que tem na imagem vai ser copiada nesse volume, e tudo que as extensions instalarem por fora, também vai pra dentro desse volume. Toda vez que reiniciar, volta tudo como estava antes. Foi a solução que encontrei e até agora parece funcionar direitinho.</p>
<p>Mas é só questão de tempo pra alguma extension mal feita instalar dependências que quebrem outras dependências, ou um caso real que eu passei: tinha uma extension que crasheia sem mensagem de erro se estiver usando Python 3.11, o Zoe Depth Map da extension <code>comfyui_controlnet_aux</code>, um dos pacotes que parece que são populares.</p>
<p>Eu não tinha me dado conta que comecei usando uma image base de Docker de ubuntu22 em vez de ubuntu24, eu estava com medo de usar algo novo demais e quebrar, mas acabei usando velha demais e quebrou do mesmo jeito. Gerenciar versionamento é um saco. Enfim, mudei o Dockerfile pra ubuntu24.</p>
<p>Mas no volume externo do VENV agora sobrou um monte de dependências que se limitavam ao Python 3.11. Não tem problema, basta apagar o 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 # garantir que está parado
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>E pronto, o VENV vai reiniciar vazio, preencher com as dependências corretas de Python 3.12 da imagem e ao iniciar o container meu script <code>init_extensions.sh</code> não vai achar o &ldquo;.last_commits&rdquo; e vai forçar um <code>pip install -r requirements.txt</code> de todas as extensions, agora com Python 3.12. Super limpo e confiável.</p>
<p>Se mesmo assim tiver erro, aí pode ser bug numa extension em especial, só vá até o diretório dele em &ldquo;./custom_nodes&rdquo; e apague manualmente antes de reiniciar o container.</p>
<p>Acho que isso é o menos complicado que dá pra fazer nesse caso. Esse docker não foi feito pra rodar sozinho num servidor, é pra rodar na sua própria máquina local, onde você está monitorando o output do terminal mesmo.</p>
<p>Ah sim, se sair versão nova do ComfyUI aí tem que re-buildar a imagem e apagar o volume de VENV. A imagem é estática, não esqueçam. Dentro o Dockerfile faz um novo git clone e pega o commit mais novo. Daí vai instalar pacotes python mais novos, então precisa zerar o volume da VENV e fazer as extensions re-instalarem suas dependências depois também. É um enorme saco, mas é assim mesmo que funciona.</p>
<h3>Conclusão<span class="hx:absolute hx:-mt-20" id="conclusão"></span>
    <a href="#conclus%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Feito tudo isso, com um simples <code>docker compose up</code> consigo ter um ambiente de ComfyUI estável e isolado do resto do meu sistema, sem sujar nada com milhares de dependências duvidosas. Quando terminar só fazer <code>docker compose down</code> ou &ldquo;ctrl-c&rdquo; no console dele e pronto, meu sistema continua limpo.</p>
<p>Além disso, diretórios como &ldquo;models&rdquo; e seus 500GB de modelos ficam separados, dá pra fazer backup pra outro lugar se precisar. E se quiser subir em outra máquina, basta levar somente esses diretórios e subir Docker Compose lá e tem que subir igualzinho, zero-setup. Esse foi meu objetivo ao Dockerzar ComfyUI, FramePack, Video2K, ter tudo portátil, isolado e limpo pra subir em qualquer máquina depois sem precisar ficar rodando scripts duvidosos de instalação direto na minha máquina.</p>
<p><a href="https://x.com/AkitaOnRails/status/1914030815962411367"target="_blank" rel="noopener">No meu X</a> eu já mostrei meu ComfyUI funcionando. Carreguei um workflow qualquer que achei na Web e olha só:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/jhx6v5va7qac4vocp17ku5o6ha51" alt="ComfyUI"  loading="lazy" /></p>
<p>Nesse workflow eu subo uma imagem de referência e posso escrever um prompt de como quero transformar ela:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20xq72nojtvb887mebeksr6q32cw" alt="Prompt"  loading="lazy" /></p>
<p>Com ComfyUI eu tenho controle sobre todas as etapas do processo, por exemplo, esse workflow consegue separar coisas como Mapa de Profundidade e Contorno de Bordas da imagem original, pra usar de contexto antes de fazer a imagem final, garantindo um resultado muito mais preciso:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/4r1en3a557znzmy0zojk8ei9tflu" alt="Depth Map"  loading="lazy" /></p>
<p>E eis um resultado que já consegui usando o modelo &ldquo;waiNSFIllustrious&rdquo; que esse workflow sugere, que é um modelo treinado pra gerar imagens estilo anime (tem dezenas, pra vários estilos diferentes).</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/m7amy8gmp5y4snztatjjlwlr0wye" alt="anime"  loading="lazy" /></p>
<p>Mudando o modelo pra <code>Mistoon</code>, baseado em Stable Diffusion (SDXL), eis outro resultado, com o mesmo prompt, no mesmo workflow:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/lkj86wdlvh5fy8h78a8ml5io8t6g" alt="anime 2"  loading="lazy" /></p>
<p>E posso ficar tunando dezenas de parâmetros dentro desse workflow, trocar vaes, text encodes, controlnets e muito mais até conseguir exatamente o resultado que eu quiser. Esse é o poder do ComfyUI e seus Custom Nodes e Workflows. É o que vou tentar explicar o básico no próximo post.</p>
]]></content:encoded><category>midia-generativa</category><category>containers</category><category>tutoriais</category></item><item><title>Gerando Videos de até 2 min a partir de uma Foto com I.A.</title><link>https://akitaonrails.github.io/2025/04/19/gerando-videos-de-ate-2-min-a-partir-de-uma-foto-com-i-a/</link><guid isPermaLink="true">https://akitaonrails.github.io/2025/04/19/gerando-videos-de-ate-2-min-a-partir-de-uma-foto-com-i-a/</guid><pubDate>Sat, 19 Apr 2025 20:00:00 -0300</pubDate><description>&lt;p&gt;Fácil o melhor canal de tutoriais de ferramentas open source pra I.A. é o &lt;a href="https://www.youtube.com/watch?v=GywyMij88rY"target="_blank" rel="noopener"&gt;Aitrepreneur&lt;/a&gt;. E nesse link ele apresenta uma nova ferramenta chamada &lt;a href="https://github.com/lllyasviel/FramePack"target="_blank" rel="noopener"&gt;FramePack&lt;/a&gt;, que usa o famoso modelo &lt;a href="https://github.com/Tencent/HunyuanVideo"target="_blank" rel="noopener"&gt;HunyuanVideo&lt;/a&gt; da Tencent pra pegar uma única imagem, uma foto, e conseguir gerar videos de excelente qualidade de até 2 minutos de duração. É realmente impressionante! Assistam o video, mas eis aqui embaixo um video gerado de 10 segundos de uma foto que eu tirei de uma das action figures da minha coleção.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Fácil o melhor canal de tutoriais de ferramentas open source pra I.A. é o <a href="https://www.youtube.com/watch?v=GywyMij88rY"target="_blank" rel="noopener">Aitrepreneur</a>. E nesse link ele apresenta uma nova ferramenta chamada <a href="https://github.com/lllyasviel/FramePack"target="_blank" rel="noopener">FramePack</a>, que usa o famoso modelo <a href="https://github.com/Tencent/HunyuanVideo"target="_blank" rel="noopener">HunyuanVideo</a> da Tencent pra pegar uma única imagem, uma foto, e conseguir gerar videos de excelente qualidade de até 2 minutos de duração. É realmente impressionante! Assistam o video, mas eis aqui embaixo um video gerado de 10 segundos de uma foto que eu tirei de uma das action figures da minha coleção.</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>Já que estava no embalo de empacotar esse tipo de ferramenta em Docker, resolvi fazer mais um projetinho com um Dockerfile pra rodar no meu Manjaro Linux com RTX 4090. O bom do Aitrepreneur é que ele tem um Patreon que você pode assinar pra contribuir (é barato) e ele sempre sobe scripts pra rodar no <a href="https://www.runpod.io/"target="_blank" rel="noopener">RunPod</a> caso você não tenha uma máquina parruda ou GPU, ou script .BAT pra rodar no seu Windows.</p>
<p>Meu projetinho está <a href="https://github.com/akitaonrails/FramePack-Docker-CUDA"target="_blank" rel="noopener">neste GitHub</a> e pra usar é muito simples:</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>Isso vai construir a imagem de Docker e rodar. E na primeira vez dá pra ver que ele vai baixar uma TONELADA de models, o HunyuanVideo, Flux e mais. Se prepara pra mais de 30GB. Mas é só na primeira vez porque eu mapeio o diretório de download pra fora, então se reiniciar o container ele já vai ter na próxima vez.</p>
<p>Quando terminar, é só acessar <strong>http://localhost:7860</strong> e vai ver esta interface super simples:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/x5nwxu25vlb9ox5kda5xiz6b4fwf" alt="FramePack Web UI"  loading="lazy" /></p>
<p>Os controles são muito simples:</p>
<ul>
<li>Desabilite a opção &ldquo;Use TeaCache&rdquo; que deixa mais rápido mas aí você arrisca a ter aqueles defeitos de I.A. como mão com mais dedos e coisas assim.</li>
<li>Total do Video pode ser até 2 minutos, mas só faça isso se realmente tiver paciência porque demora BASTANTE! Acho melhor começar mesmo testando com videos curtos de 5 a 10 segundos primeiro.</li>
<li>Se sua GPU tiver pouca VRAM vai ser obrigado a mexer em &ldquo;GPU Inference Preserved Memory (GB) (larger means slower)&rdquo;. No meu caso que tenho 24GB de VRAM deixei como está, mas olha só rodando como ele consome:</li>
</ul>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/5purckhhytuqd7bwp2wjqhyxq66p" alt="btop"  loading="lazy" /></p>
<p>Sim! 100% da GPU e quase 100% da VRAM, puxando mais de 350W da parede! (foda-se meio ambiente! kkkkk) A CPU até fica de boa, porque esse processo é feito pra realmente MASSACRAR a GPU. Se não tiver GPU boa, alugue uma máquina na RunPod como falei antes.</p>
<p>O video de 10 segundos no começo do post é uma foto da uma das action figures que eu mais gosto na minha coleção, o Link de Breath of the Wild. E isso é uma coisa que me deixou empolgado. Esta é a foto original que serviu de base pro FramePack fazer o video:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/g3dm9u1ij8r6gl0dk90qool0htyw" alt="Link Foto"  loading="lazy" /></p>
<p>Fazer video de fotos de pessoas é sem graça e tedioso, mas agora que estou aprendendo a modelar 3D pra imprimir na minha Bambulab, imagina depois ainda conseguir fazer ele ficar animado?</p>
<p>Pra animar no Blender e renderizar, também é bastante processamento e demora. Mas com isso eu consigo rapidamente prototipar uma animação e ver como fica, pra ver se vale a pena fazer uma versão melhor no Blender depois. Abre muitas possibildades de experimentação!</p>
<p>Por mais impressionante que isso pareça, ele não faz <strong>QUALQUER COISA</strong>, tive várias experimentações que deram errado e a movimentação que ele faz é bem limitada, afinal ele só tem uma foto pra usar de referência e não tem dá pra fazer movimentos bruscos demais, tem limites. Mesmo assim é bem divertido de brincar. O fato dele conseguir identificar mais que seres humanos é um grande &ldquo;plus&rdquo; na minha opinião, porque é onde eu me interessei mais.</p>
<p><strong>AH SIM!</strong> Lembram o <a href="https://www.akitaonrails.com/2025/04/19/aumentando-resolucao-de-anime-velho-pra-4k-com-i-a"target="_blank" rel="noopener">outro blog post</a> que publiquei hoje sobre fazer <strong>UPSCALE PRA 4K</strong>?? Dá pra usar aqui também, pegue um video que o FramePack gerou e você gostou muito e faça upscale pra 4K pra ter mais definição ainda! Aí fica em qualidade pra até usar em edição de videos.</p>
<p>Videos de 2 minutos deve levar horas pra fazer. 30 segundos levou quase 1 hora. Melhor coisa é ficar em videos de 5 segundos pra brincar e experimentar, só isso já leva aí quase meia hora. Aqui vão mais alguns exemplos tirados de fotos de itens da minha coleção (aqueles que vocês viam no fundo dos meus videos). Divirtam-se!</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>Este é um Ultraseven de mais de 30cm de altura que eu encontrei e trouxe direto de um sebo de Tóquio ano passado, um dos que eu mais gosto especialmente porque assisti muito no começo dos anos 80 quando eu era criança.</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>Falando em nostalgia, esse é outro item da hora da minha coleção, um Jiraya super detalhado feito pela Iron Studios. E animado até que ficou bem da hora. O que acharam?</p>
<p>Mas agora o meu <strong>FAVORITO</strong>, quem me acompanha no <a href="https://www.instagram.com/p/DIAe3pNOFgffFZ5J32fWlc-8Kmy3Lhap5Vm58U0/?img_index=1"target="_blank" rel="noopener">Instagram</a> viu que faz algumas semanas que venho me dedicando a melhorar minhas técnicas de 3D e também de desenho e eu fiz um conceito de Mandaloriam + 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>Depois vejam os outros desenhos no meu Instagram, mas enfim, eu precisava saber se o FramePack era capaz de animar também desenhos e SIM, ELE CONSEGUE!! OLHA QUE PH0DA!!! Esse me deixou impressionado, dá uma nova dimensão nas minhas coisas!</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>Sim, tudo isso é bem pesado, mas a vantagem é que eu posso experimentar INFINITAMENTE, porque tudo roda local. Num produto comercial de alguém no &ldquo;cloud&rdquo;, eu precisaria pagar alguma assinatura e ele ia me limitar, seria impossível ficar experimentando quando eu quisesse. Agora eu posso fazer tudo que eu quiser, quando eu quiser, como eu quiser, sem que ninguém nem nada possa encher meu s@co.</p>
<p>Aliás, se puderem assistam esse clip do &ldquo;Boba Dredd&rdquo; em tela cheia num monitor grande. Notaram que tá bem nítido? É porque eu passei no Video2K e fiz upscale pra 4x a resolução que o FramePack me deu. Juntando essas ferramentas dá pra fazer coisas BEM interessantes, depois me falem se conseguiram usar e o que fizeram de legal!</p>
]]></content:encoded><category>midia-generativa</category><category>containers</category></item><item><title>Aumentando Resolução de Anime velho pra 4K com I.A.</title><link>https://akitaonrails.github.io/2025/04/19/aumentando-resolucao-de-anime-velho-pra-4k-com-i-a/</link><guid isPermaLink="true">https://akitaonrails.github.io/2025/04/19/aumentando-resolucao-de-anime-velho-pra-4k-com-i-a/</guid><pubDate>Sat, 19 Apr 2025 15:30:00 -0300</pubDate><description>&lt;p&gt;Mais de um ano atrás eu estava brincando com ErsGAN, redes adversariais generativas pra tarefa de fazer &amp;ldquo;upscaling&amp;rdquo; (aumentar resolução) de arquivos de animes velhos que eu tenho.&lt;/p&gt;
&lt;p&gt;Pra quem coleciona, o problema é que muito anime dos anos 90 pra trás nunca saíram e nem nunca vão sair em Blu-Ray (1080p) nem UHD (4K). Só os mais famosos recebem tratamento de &amp;ldquo;remaster&amp;rdquo; (pegar as fitas master originais e recapturar em mais resolução). Então muito anime velho está preso na era de DVD (480p) ou VHS (480i).&lt;/p&gt;</description><content:encoded><![CDATA[<p>Mais de um ano atrás eu estava brincando com ErsGAN, redes adversariais generativas pra tarefa de fazer &ldquo;upscaling&rdquo; (aumentar resolução) de arquivos de animes velhos que eu tenho.</p>
<p>Pra quem coleciona, o problema é que muito anime dos anos 90 pra trás nunca saíram e nem nunca vão sair em Blu-Ray (1080p) nem UHD (4K). Só os mais famosos recebem tratamento de &ldquo;remaster&rdquo; (pegar as fitas master originais e recapturar em mais resolução). Então muito anime velho está preso na era de DVD (480p) ou VHS (480i).</p>
<p>Nem todo anime velho dá pra aumentar resolução, pode ficar &ldquo;lavado&rdquo; demais. Quem baixa torrent já deve ter visto upscalings mal feitos. Mas eu queria poder testar isso eu mesmo, na minha máquina local, com meus arquivos velhos. Eis um pequeno clip de exemplo:</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>É um pedaço do 1o episódio do lendário Rurouni Kenshin de 1996. Sim, hoje temos um remake, que estão refazendo do zero. Mas pra quem tem nostalgia pelo original, não tem substituição. A qualidade de DVD não está horrenda, dá pra assistir de boa ainda hoje assim mesmo. Mas fico curioso pra ver como fica em 4K.</p>
<p>Não fiz nenhuma pesquisa longa, mas um ano atrás eu tinha esbarrado no <a href="https://github.com/xinntao/Real-ESRGAN"target="_blank" rel="noopener"><strong>Real ERSGAN</strong></a>. Esse projeto está descontinuado faz uns 4 anos, então não sei o que é o mais moderno hoje. Mas os modelos pré-treinados deles ainda são úteis.</p>
<p>Ainda tem projetos que usam esse modelo, o que eu conheço é o <a href="https://github.com/k4yt3x/video2x"target="_blank" rel="noopener"><strong>Video2K</strong></a> Em Windows parece que tem até uma interface gráfica bonitinha, mas a vantagem pra Linux é que ele roda em Docker. Um exemplo seria assim:</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>Se tiver com sua NVIDIA configurada com o Container Toolkit, Cuda e tudo mais (pergunte ao ChatGPT), o Docker consegue mapear o dispositivo direto pra dentro do container. Isso facilita muito não deixar meu Linux sujo cheio de dependências e configuraçõe soltas por aí. Não faço mais nada desse tipo fora de containers.</p>
<p>Mas eu esbarrei com um problema. Se você estiver usando GPU AMD, só assim já deve funcionar, porque essa imagem Docker na verdade usa Vulkan e não CUDA. No meu caso eu tenho uma GPU integrada AMD primária e uma NVIDIA RTX 4090 secundária. Eu tentei passar direto:</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>Mas não vai de jeito nenhum. Se eu só disser &ldquo;&ndash;gpus all&rdquo; ele vai pegar a iGPU por padrão. Fazer o upscaling de um video acaba consumindo 100% da minha CPU, todas as 32 threads, mas minha GPU NVIDIA fica parada olhando sem fazer nada. Se eu tentar passar &ldquo;/dev/dri/card0&rdquo; ou &ldquo;/dev/nvidia0&rdquo;, ele dá erro e não consegue começar.</p>
<p>Então a partir daqui a solução é só pra quem tem NVIDIA:</p>
<h3>Dockerfile CUDA<span class="hx:absolute hx:-mt-20" id="dockerfile-cuda"></span>
    <a href="#dockerfile-cuda" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Desta vez resolvi não ser preguiçoso e organizar tudo no projetinho no Github. Clone direto de lá <a href="https://github.com/akitaonrails/Batch-Anime-Upscaler-Video2K-Docker-CUDA"target="_blank" rel="noopener">Batch Anime Upscaler Video2K Docker CUDA</a> Só fazer assim:</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 # coloque seus videos velhos aqui
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>Se nada der errado, isso vai construir uma imagem de Docker com tudo que você precisa, daí é colocar seus videos no subdiretório que falei, ou outro que quiser e configurar no comando pra rodar:</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>E é só isso. Demora bastante. Com meu Ryzen 7950X3D mais RTX 4090, ele processa a uma taxa de uns 13 frames por segundo. Então menos da metade da velocidade de playback. Se um video tiver 20 min, vai levar mais de 40 minutos pra processar cada um. É bom reservar um bom tempo, e por isso também é bom só pra videos que você realmente queira assistir.</p>
<p>O script original do Video2K suporta vários parâmetros, que eu deixei expostos como variáveis de ambiente no 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>Daí dá pra mudar mexendo assim:</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>Como podem ver, por padrão usamos o modelo pré-treinado &ldquo;realesr-animevideov3&rdquo; que é menos detalhado que o modelo de 6 bilhões de parâmetro mas é bem mais estável entre frames, que é mais importante pra um video. A maioria dos outros modelos que ele suporta, como &ldquo;RealESRGAN_x4plus&rdquo; ou &ldquo;ESRGAN_x4&rdquo; servem pra fotos e imagens mas não pra video. Video exige coerência temporal, que é bem difícil.</p>
<p>No máximo vale testar o modelo &ldquo;realesr-general-x4v3&rdquo; que dizem ser um pouco mais leve, caso sua máquina esteja capengando muito pra rodar o padrão. Mas aí vai ter que modificar o Dockerfile pra fazer download desse modelo e re-buildar:</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>Eu não testei todos, mas pra quem quiser, acho que esses são os links. Mude no Dockerfile, faça o build, mude a variável MODEL e teste nos seus videos.</p>
<p>O que vale mexer são coisas como DENOISE, que está pra &ldquo;1.0&rdquo; que é máximo, mas isso pode deixar a imagem &ldquo;esticada&rdquo; demais. Vale testar valores menores, como &ldquo;0.5&rdquo;. Varia de caso a caso, só testando.</p>
<p>Outro fator é a escala, SCALE, que o default é &ldquo;4x&rdquo;, que transforma 1080p em 4K ou 720p em 1440p. Mas às vezes não precisa subir tanto, e nem adianta porque a imagem original já tem muito pouco detalhe mesmo. Talvez só 2x seja suficiente. O script suporta 2x, 3x, 4x mas tem teto na resoluçãod e 4K, então não adianta tentar transformar video que já é 4K em 8K por exemplo.</p>
<p>Minha RTX 4090 e meu Ryzen 9 são bem potentes. Processando só um video eu não vejo ele suando nem um pouco:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2dz1abq9b1wteho3r1onowmij14v" alt="btop"  loading="lazy" /></p>
<p>Claramente daria pra rodar mais videos em paralelo. Tem que tomar cuidado quando é desbalanceado. Se a CPU subir pra perto de 100% pra separar os frames, não adianta estar sobrando na GPU. Os dois tendo sobra, dá pra fazer o script subir mais de 1 processo na GPU e mandar frames em paralelo com a opção &ldquo;-e NUM_PROC=2&rdquo;. Olha como agora ele já usa mais da GPU e a CPU ainda tá sobrando. Talvez 3 seja o máximo, mas tem que testar, não é constante o tempo todo.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/qur7lw4ox5s3nymh025u6q446831" alt="btop x2"  loading="lazy" /></p>
<p>Isso pode diminuir o tempo total dramaticamente. Você pode escolher entre aumentar os processo na GPU pelo script ou ter dois diretórios separados de video e subir dois containers paralelos desse programa pra rodar ao mesmo tempo, o que funcionar melhor pra você.</p>
<h2>Resultado Final<span class="hx:absolute hx:-mt-20" id="resultado-final"></span>
    <a href="#resultado-final" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Vamos ver como ficou aquele primeiro clip com upscale 4x e denoise 1.0:</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>Dá uma diferença ENORME, mas se ficou &ldquo;BOM&rdquo; vai ser BEM subjetivo. Eu particularmente não achava o original ruim de assistir. Talvez se mexer no DENOISE pra um pouco menos ou SCALE pra 2x em vez de 4x já fique bom, não sei.</p>
<p>Pra comparar, eis outro clip na versão original antiga:</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>E de novo, como fica depois do upscale 4x:</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>Pelo menos está funcionando como deveria, a partir daqui é fazer algum tuning. Se alguém tiver mais dicas ou conhecer modelos ou projetos mais novos de upscaling open source, mande nos comentários abaixo!</p>
<p>Antes que alguém pergunte, porque não usar DLSS 3 da NVIDIA, que eles usam pra aumentar resolução de games? Resposta: porque eles não expõe essa funcionalidade como uma API que dá pra acessar, é só pros drivers proprietários deles em profile de jogo. Eles ligam e tunam caso a caso. Não tem API geral na lib CUDA.</p>
<h2>Update pós-publicação<span class="hx:absolute hx:-mt-20" id="update-pós-publicação"></span>
    <a href="#update-p%c3%b3s-publica%c3%a7%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Resolvi testar o modelo RealERSGAN_x4plus_anime_6B, que é bem mais pesado que o realesr-animevideov3 que subi no repositório.</p>
<p>Mude o Dockerfile e adicione isto depois do wget do outro modelo:</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>Esse modelo não tem como rodar em paralelo nem na minha RTX 4090, que fica constantemente em 100%. Se antes ia a uns 13 frames por segundo, com esse vai só a 3 frames por segundo. É 4x mais pesado e somente pra GPUs realmente parrudas. Se não tiver algo assim, fique na &ldquo;realesr-animevideov3&rdquo; mesmo ou no mais leve ainda (menos qualidade) &ldquo;realesr-general-x4v3&rdquo;. Esses modelos dependem muito da potência da GPU e principalmente, quantidade de VRAM.</p>
<p>Pra rodar adicione &ldquo;-e MODEL=RealESRGAN_x4plus_anime_6B&rdquo; no comando &ldquo;docker run&rdquo;.</p>
<p>Veja como ficou o mesmo clip de antes usando este modelo:</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>Pra ter gasto 4x mais recursos da minha máquina pra processar, o resultado final eu não achei que ficou tão diferente, e em algumas partes eu acho que ficou mais defeitos até. Então é isso, o resultado vai variar, tem que testar. Depende muito da qualidade do anime original, mas não é porque existe um modelo maior e teoricamente &ldquo;melhor&rdquo; que todo resultado vai ficar melhor.</p>
]]></content:encoded><category>midia-generativa</category><category>performance</category><category>containers</category></item><item><title>Colorindo Imagens Preto e Branco com I.A.</title><link>https://akitaonrails.github.io/2025/04/19/colorindo-imagens-preto-e-branco-com-i-a/</link><guid isPermaLink="true">https://akitaonrails.github.io/2025/04/19/colorindo-imagens-preto-e-branco-com-i-a/</guid><pubDate>Sat, 19 Apr 2025 01:40:00 -0300</pubDate><description>&lt;p&gt;Minha namorada me deu um desafio hoje: ela tinha fotos preto e branco antigas e queria saber se eu conseguia colorizar elas. Se sair procurando na Web esbarra em alguns sites pra isso, como esse: &lt;a href="https://palette.fm/"target="_blank" rel="noopener"&gt;Palette&lt;/a&gt;. Mas é pago, não é barato e acho que ele não consegue uma coisa que ela queria: usar uma outra imagem colorida como referência pra tirar as cores em vez de tentar colorizar por chute do modelo.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Minha namorada me deu um desafio hoje: ela tinha fotos preto e branco antigas e queria saber se eu conseguia colorizar elas. Se sair procurando na Web esbarra em alguns sites pra isso, como esse: <a href="https://palette.fm/"target="_blank" rel="noopener">Palette</a>. Mas é pago, não é barato e acho que ele não consegue uma coisa que ela queria: usar uma outra imagem colorida como referência pra tirar as cores em vez de tentar colorizar por chute do modelo.</p>
<p>Saí fuçando GitHub e tem uma página <a href="https://github.com/MarkMoHR/Awesome-Image-Colorization"target="_blank" rel="noopener">Awesome Image Colorization</a> com vários papers de pesquisa. Muito útil pra quem for pesquisador mas totalmente inútil pra mim que não vou fazer um do zero kkkk. Tinha links pra alguns projetos como ChromaGAN, mas que está descontinuado faz uns 5 anos. E vários outros projetos que eu vi foram descontinuados lá atrás mesmo, não sei porque tem tão pouco open source ainda atualizado.</p>
<p>Mas felizmente esbarrei em um promissor: <a href="https://github.com/piddnad/DDColor"target="_blank" rel="noopener">DDColor</a>.</p>
<p>Bora clonar o repositório. E pra continuar, prefiro fazer tudo dentro de Docker. Ficar baixando dependência de python sempre zoa meu sistema e deixa um monte de lixo pra trás, então melhor coisa é isolar tudo. Outra coisa, precisa baixar os binários dos modelos pré-treinados. Só pra isso vou usar um pouco de Python por causa da lib &ldquo;modelscope&rdquo; que faz isso:</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"># criar um novo venv dentro do projeto DDColor</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>Boa prática de Python acho que é fazer tudo dentro de uma VENV né? Enfim, isso deve baixar o binário do modelo, que pesa quase 900MB. Não é grande.</p>
<p>Agora precisamos de um 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>Preguiça de abrir um pull request pra isso, se alguém quiser, mande lá.</p>
<p>Agora precisamos colocar uma imagem preto e branco em algum lugar:</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>Como as fotos da minha namorada são familiares e particulares, obviamente não vou compartilhar, então peguei uma aleatória qualquer no Google Images:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/qgt3lynkp1f5uzkbysyvio2kmq04" alt="Foto B&W"  loading="lazy" /></p>
<p>Agora colocamos em &ldquo;./input&rdquo; e rodamos o Docker com este comando:</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>Preste atenção no mapeamento de diretórios. E note a opção &ldquo;&ndash;gpus&rdquo; que só funciona no meu caso porque eu tenho uma RTX 4090 instalada aqui. Não tenho a mínima idéia se é a mesma coisa com AMD, mas como a imagem é baseada em cnDNN/CUDA, acho que não. Pergunte ao ChatGPT.</p>
<p>Se estiver tudo certo e rodar com sucesso, vai levar só 1 segundo e ter este output no 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>E eis o resultado que aparece no diretório `./results&quot;:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/o2r085pzzpsavbyr39btg1vq0hwp" alt="Foto re-colorizada"  loading="lazy" /></p>
<p>Sim, eu fiquei impressionado. Mesmo a imagem sendo da Web, com marca d&rsquo;agua e tudo, ele fez tudo direitinho e funcionou!!</p>
<p>Único problema é que ainda falta o problema de conseguir usar uma imagem de referência pra tentar pegar as cores. Esse é um problema mais difícil e eu perguntei ao ChatGPT que me respondeu isto:</p>
<p><strong>Sem suporte nativo para imagem de referência:</strong></p>
<p>O modelo DDColor, tanto na versão oficial hospedada no ModelScope quanto no repositório GitHub <code>piddnad/DDColor</code>, foi projetado apenas para colorização <strong>automática de imagens em preto e branco</strong>, sem aceitar uma imagem de referência como entrada. A própria descrição oficial deixa claro que o modelo recebe <strong>uma única imagem P&amp;B</strong> e retorna uma imagem colorida.</p>
<p>O método <code>forward</code> do modelo espera <strong>apenas um tensor</strong>, referente à imagem P&amp;B convertida em RGB artificial (na verdade, a luminância expandida para 3 canais). Ou seja, <strong>não há lugar no modelo para passar uma segunda imagem como referência de cores</strong>.</p>
<p>Isso também fica evidente no <code>predict.py</code> original da biblioteca Cog, que aceita apenas o parâmetro <code>image</code> como entrada — sem suporte a <code>reference</code>, <code>ref_image</code> ou algo do tipo.</p>
<h3>Como resolver o problema: duas abordagens possíveis<span class="hx:absolute hx:-mt-20" id="como-resolver-o-problema-duas-abordagens-possíveis"></span>
    <a href="#como-resolver-o-problema-duas-abordagens-poss%c3%adveis" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Como o DDColor não suporta nativamente uma imagem de referência para guiar as cores, temos duas alternativas:</p>
<h3>🔁 1. Usar um modelo especializado em colorização com referência<span class="hx:absolute hx:-mt-20" id="-1-usar-um-modelo-especializado-em-colorização-com-referência"></span>
    <a href="#-1-usar-um-modelo-especializado-em-coloriza%c3%a7%c3%a3o-com-refer%c3%aancia" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Existem modelos desenvolvidos especificamente para isso, como:</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>Esses modelos aceitam <strong>duas imagens</strong>:</p>
<ul>
<li>
<p>A imagem em P&amp;B (entrada principal)</p>
</li>
<li>
<p>Uma imagem colorida como <strong>referência de cores</strong></p>
</li>
</ul>
<p>Esses modelos mapeiam similaridades semânticas (ex: “pele aqui deve ter a mesma cor de pele dali”) e fazem uma transferência <strong>baseada no conteúdo da imagem</strong>, obtendo resultados muito mais detalhados e específicos. No entanto, isso exigiria:</p>
<ul>
<li>
<p>Baixar pesos diferentes</p>
</li>
<li>
<p>Alterar completamente a arquitetura usada</p>
</li>
<li>
<p>Adaptar o pipeline e a inferência</p>
</li>
</ul>
<h3>🎯 2. Usar transferência global de cor após a saída do DDColor (nossa abordagem atual)<span class="hx:absolute hx:-mt-20" id="-2-usar-transferência-global-de-cor-após-a-saída-do-ddcolor-nossa-abordagem-atual"></span>
    <a href="#-2-usar-transfer%c3%aancia-global-de-cor-ap%c3%b3s-a-sa%c3%adda-do-ddcolor-nossa-abordagem-atual" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>A solução prática e leve que usamos foi:<br>
➡️ Rodar o DDColor normalmente para gerar a imagem colorida automaticamente<br>
➡️ Depois, ajustar <strong>globalmente</strong> as cores do resultado para ficarem mais parecidas com as da imagem de referência.</p>
<p>Esse ajuste é feito com o algoritmo de <strong>Reinhard et al. (2001)</strong>:</p>
<ol>
<li>
<p>Convertemos tanto a imagem de saída quanto a referência para o espaço de cor Lab.</p>
</li>
<li>
<p>Calculamos a <strong>média e o desvio padrão</strong> dos canais <code>a</code> e <code>b</code> (os que representam cor).</p>
</li>
<li>
<p>Ajustamos o resultado para ter <strong>a mesma distribuição estatística</strong> de cor que a imagem de referência.</p>
</li>
<li>
<p>Isso tende a deixar a imagem com o mesmo “clima” ou tom geral (mais quente, mais frio, mais vibrante).</p>
</li>
</ol>
<p>Esse método é rápido, leve, e funciona <strong>sem alterar o modelo DDColor</strong>.</p>
<h2>Continuando<span class="hx:absolute hx:-mt-20" id="continuando"></span>
    <a href="#continuando" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Eu usei a modificação que ele sugeriu ao arquivo predict.py:</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>Com essa modificação posso rodar com esse novo parâmetro, note a opção &ldquo;&ndash;ref&rdquo; que tem agora e coloque sua imagem no lugar certo:</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>Passei outra foto colorida de referência mas não dferença no resultado. Novamente, perguntei ao ChatGPT:</p>
<h3>Por que você não percebeu diferença no resultado?<span class="hx:absolute hx:-mt-20" id="por-que-você-não-percebeu-diferença-no-resultado"></span>
    <a href="#por-que-voc%c3%aa-n%c3%a3o-percebeu-diferen%c3%a7a-no-resultado" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>A <strong>transferência global de cor</strong> só altera a “paleta geral” — e não as cores de cada objeto.</p>
<p>Se a imagem de referência tem tons suaves ou parecidos com os que o DDColor já aplicaria automaticamente, o efeito final <strong>vai parecer idêntico</strong>.</p>
<p>Mas se você usar como referência uma imagem bem saturada (por exemplo, um céu alaranjado, ou um ambiente azul escuro), você verá a saída do DDColor ser <strong>puxada visualmente</strong> para aquele estilo.</p>
<h2>Conclusão<span class="hx:absolute hx:-mt-20" id="conclusão"></span>
    <a href="#conclus%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Segui a sugestão e baixei outra imagem nada a ver, colorida, super saturada com cores vibrantes pra ver se fazia diferença. Esta é a imagem de referência que passei pro Docker:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/ryptql9u8mk04o3r7ah01fk1qz5q" alt="Imagem de Referência"  loading="lazy" /></p>
<p>E eis o novo resultado aplicando ela à imagem anterior em pós-processamento (esse passo não é mais I.A. é pós-processamento de imagem mesmo).</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/cxcwwnj2ncszvmv4whj4qesafgnm" alt="Foto pós-processada"  loading="lazy" /></p>
<p>Comparem com a outra foto re-colorizada, vou colocar aqui embaixo de novo pra dar pra comparar:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/o2r085pzzpsavbyr39btg1vq0hwp" alt="Repetindo a primeira re-colorização"  loading="lazy" /></p>
<p>Entenderam? Agora precisa ficar testando com referências diferentes pra ver a influência no resultado final, mas de fato, ele consegue aplicar o algoritmo de Reinhard e fazer uma transferência global de cor. Como o nome diz, é &ldquo;global&rdquo;, não &ldquo;por objeto&rdquo;, então é difícil conseguir controlar só partes da imagem e sim a imagem toda. Mas em teoria eu acho que você conseguiria fazer uma colorização manual no Photoshop com cores saturadas, perto de onde quer influenciar, pra tentar ajustar, mas eu não tentei isso ainda, depois se alguém fizer, mande nos comentários.</p>
<p>Mas é isso. Agora pelo menos eu consigo me divertir pegando fotos antigas de família e re-colorizar.</p>
]]></content:encoded><category>midia-generativa</category><category>containers</category><category>tutoriais</category></item><item><title>Meu "Netflix Pessoal" com Docker Compose</title><link>https://akitaonrails.github.io/2024/04/03/meu-netflix-pessoal-com-docker-compose/</link><guid isPermaLink="true">https://akitaonrails.github.io/2024/04/03/meu-netflix-pessoal-com-docker-compose/</guid><pubDate>Wed, 03 Apr 2024 13:30:00 -0300</pubDate><description>&lt;p&gt;Quem acompanhava meu canal no &lt;a href="https://www.youtube.com/channel/UCib793mnUOhWymCh2VJKplQ"target="_blank" rel="noopener"&gt;YouTube&lt;/a&gt; ou meu &lt;a href="https://www.instagram.com/akitaonrails/"target="_blank" rel="noopener"&gt;Instagram&lt;/a&gt; já acompanhou a saga com meu &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; (meu servidor pessoal), meu &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; com quase 80 TB de espaço.&lt;/p&gt;
&lt;p&gt;Todos os videos do meu canal, incluindo os arquivos originais, estão lá. Só isso dá terabytes. Todo minha biblioteca do Steam está lá, são uns 4 terabytes. Todos os meus jogos antigos, retro-games, também, incluindo ISOs de Xbox 360 e PS3. São mais 4 terabytes. Toda minha coleção de discos Ultra HD (BluRay 4K) &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;eu ripei&lt;/a&gt; (fiz backup), são mais alguns terabytes. Neste instante já estou usando mais de 50 terabytes.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Quem acompanhava meu canal no <a href="https://www.youtube.com/channel/UCib793mnUOhWymCh2VJKplQ"target="_blank" rel="noopener">YouTube</a> ou meu <a href="https://www.instagram.com/akitaonrails/"target="_blank" rel="noopener">Instagram</a> já acompanhou a saga com meu <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> (meu servidor pessoal), meu <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> com quase 80 TB de espaço.</p>
<p>Todos os videos do meu canal, incluindo os arquivos originais, estão lá. Só isso dá terabytes. Todo minha biblioteca do Steam está lá, são uns 4 terabytes. Todos os meus jogos antigos, retro-games, também, incluindo ISOs de Xbox 360 e PS3. São mais 4 terabytes. Toda minha coleção de discos Ultra HD (BluRay 4K) <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">eu ripei</a> (fiz backup), são mais alguns terabytes. Neste instante já estou usando mais de 50 terabytes.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/29mxcuiblyunqnnbtc10sdnqjl3f" alt="Meu Plex com UHDs"  loading="lazy" /></p>
<p>Antes que venham dar palpite, sim, isso não é pra qualquer um. Estamos falando de 8 HDs de 10.9 TB, mais upgrade de 2 HDs de 20 TB, mais outros 2 HDs de 20 TB de backup pra caso um deles dê pane, mais 2 NVMEs de 1 TB cada só pra ser cache, mais placa de rede de 10 Gbps. Além disso eu mantenho várias cópias online de parte desse conteúdo em Google Drive ou Dropbox, e um backup inteiro na Amazon Glacier, pra rara ocasião se minha casa pegar fogo, por exemplo. Estou preparado pra qualquer catástrofe, sempre.</p>
<p>A Synology é uma das melhores marcas de NAS caseiros. Sim, dá pra fazer bem mais sofisticado também, basta montar um servidor de verdade, com CPUs como AMD EPYC ThreadRipper, usando sistemas como <a href="https://www.truenas.com/"target="_blank" rel="noopener">TrueNAS</a>. Mas aí é demais pra mim, já não me sinto confortável dando manutenção. Como eu disse, os limites cada um tem que saber os seus.</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;Por que não deixar tudo online??&rdquo;</em> Pra maioria das pessoas é o caminho mais fácil mesmo. Como eu disse, meu caso é particular. Você NÃO QUER ter mais de 50 TERAbytes online. Minha rede cabeada local é de 10 Gbps, minha internet de fibra é míseros 0.5 Gbps (500 Mbps). Trafegar video de alta qualidae pela internet é horrivelmente lento. Eu quero tudo em tempo real, e pra isso só sendo rede cabeada local.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/hejrhguxmbprma7c02hsp4mpr2rq" alt="Synology Dashboard"  loading="lazy" /></p>
<p>Isso dito, eu montei um segundo mini-computador, usando um Intel NUC, conectado na mesma rede cabeada com o NAS. É Core i7, 32GB de RAM, ethernet 2.5Gbps, com o único intuito de ser um servidor Ubuntu vazio (poderia ser qualquer Linux, só escolhi o mais fácil mesmo) que serve pra subir containers Docker.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/byblhfmjthild3vjq4kyif974vt2" alt="Intel Nuc Core i7"  loading="lazy" /></p>
<p>Mesmo esse PC-zinho é overkill, muito mais do que é necessário. Muitos vão comentar que um Raspberry-Pi 5 seria suficiente, e não estão errados. A melhor solução é a que você tem orçamento pra comprar e se sente confortável em dar manutenção. Não tente copiar exatamente o setup de ninguém, estude o que melhor encaixa pra você. E não percam tempo dando pitaco no setup dos outros, façam o seu.</p>
<p>De qualquer forma, resolvi compartilhar com vocês todos os arquivos de docker-compose que estou usando pra subir os containers docker nesse servidor. Eis o <a href="https://github.com/akitaonrails/plex_home_server_docker"target="_blank" rel="noopener">repositório no GitHub</a>.</p>
<p><em>AVISO:</em> não esqueçam que precisa editar os diretórios nos scripts. Ele está configurado pra acessar o mount point do meu NAS em rede, que é <code>/mnt/terachad</code>. Arrume pra apontar pros lugares certos na sua máquina.</p>
<p><em>AVISO 2:</em> existem muitos detalhes que não estou cobrindo neste artigo. Este <a href="https://www.reddit.com/r/pirataria/comments/18ch7bt/guia_do_streaming_dom%C3%A9stico_automatizado_sonarr/"target="_blank" rel="noopener">fórum do Reddit</a> tem mais detalhes e mais discussões. Dependendo se estiver com dúvidas ou problemas específicos, talvez esteja respondido lá.</p>
<p>Pra você não se perder no meio de tantos serviços, aqui está o desenho completo do que a gente vai montar, do pedido até assistir na TV, incluindo as camadas de proteção que impedem malware e download errado de entrar na sua biblioteca:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260905220142_home-server-arquitetura-pt.png" alt="Arquitetura completa do home server pra Plex, do pedido até a reprodução"  loading="lazy" /></p>
<p>A ideia é essa: você pede num lugar (Seerr), o sistema encontra (Prowlarr e os indexers), baixa com proteção (qBittorrent e SABnzbd, mais as camadas de segurança), organiza no NAS (com legendas do Bazarr) e você assiste em qualquer tela (Plex e Navidrome). Deixa eu começar explicando um a um dos principais.</p>
<h2>Portainer e Utilitários<span class="hx:absolute hx:-mt-20" id="portainer-e-utilitários"></span>
    <a href="#portainer-e-utilit%c3%a1rios" 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>Este é o trecho (veja completo no <a href="https://github.com/akitaonrails/plex_home_server_docker/blob/master/kaizoku-docker-compose.yml"target="_blank" rel="noopener">repositório</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> é um gerenciador visual dos recursos gerenciados pelo serviço de Docker na sua máquina. Objetivo é mais pra eu visualmente conseguir &ldquo;bater o olho&rdquo; e ver se tem algum container com problema ou pra testar algum ajustezinho rápido em alguma configuração antes de editar no arquivo de docker compose.</p>
<p>No arquivo <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> tem outros serviços como Organizr que eu instalei mas ainda não parei pra configurar nem ver se realmente quero manter. Tem o Librespeed, que funciona como aqueles testes de velocidade de internet, mas pra rede local. Tem o <a href="https://github.com/containrrr/watchtower"target="_blank" rel="noopener">Watchtower</a>, que monitora atualização das imagens de todos os containers Docker, faz o download e automaticamente manda reiniciar com versões novas, pra manter todos os containers sempre atualizados.</p>
<p>E pra acessar os serviços locais fora de casa, tem opções fáceis de VPN como o <a href="https://tailscale.com/"target="_blank" rel="noopener">Tailscale</a> ou o <a href="https://www.zerotier.com/"target="_blank" rel="noopener">ZeroTier</a>, com pouca configuração. Sim, sim, antes que alguém vá pros comentários, dá pra montar OpenVPN do zero, mas vai por mim, essas opções são infinitamente mais &ldquo;plug and play&rdquo; e possivelmente mais seguras, porque todo mundo instala, mas poucos têm a disciplina de dar manutenção e atualizar o que precisa o tempo todo. Hoje em dia eu acabei migrando pra um túnel da Cloudflare pra expor meus serviços, mas pra começar, um Tailscale da vida resolve super bem.</p>
<h2>MakeMKV e HandBrake<span class="hx:absolute hx:-mt-20" id="makemkv-e-handbrake"></span>
    <a href="#makemkv-e-handbrake" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Pra montar sua biblioteca pessoal de filmes, de forma legítima, o correto é você ser proprietário de discos de BluRay ou UHD e &ldquo;ripar&rdquo; eles. Se for esse o caso, recomendo comprar modelos de leitor de discos como o <a href="https://www.amazon.com/Pioneer-BDR-XS07S-Silver-Revision-Blu-ray/dp/B081R74KVW"target="_blank" rel="noopener">Pioneer BDR-XS07S</a> Tem que ficar MUITO esperto porque não é qualquer leitor que funciona.</p>
<p>Veja no <a href="https://forum.makemkv.com/forum/viewforum.php?f=16"target="_blank" rel="noopener">fórum do MakeMKV</a> quais os modelos mais recomendados. Tem modelos que tiveram atualização de firmware que impedem deles serem usados pra ripar BluRay. Precisa achar modelos que ou não foram atualizados, ou que não é difícil fazer downgrade de firmware. O ideal é já comprar modelos que todo mundo sabe que funciona, e pra isso tem que fuçar fóruns como esse.</p>
<p><a href="https://www.makemkv.com/"target="_blank" rel="noopener">MakeMKV</a>, como o próprio nome diz, &ldquo;Faz arquivos de video MKV&rdquo;, que é o bom e velho formato Matroska de envelope de video, um dos mais versáteis (porque suporta múltiplos streams não só de áudio, mas de legenda também).</p>
<p><em>Crash course:</em> uma coisa é o codec que faz o encodings dos streams de video e streams de audio. Por exemplo, H.265 ou AV1, são codecs de video. MP3 ou AAC são codecs de áudio. Agora precisamos empacotar esses dois streams (ou mais, caso tenha múltiplas dublagens, por exemplo), num &ldquo;envelope&rdquo;, um &ldquo;container&rdquo;, um &ldquo;formato de arquivo&rdquo;, como arquivos &ldquo;.mp4&rdquo; ou &ldquo;.mov&rdquo;. Matroska é um formato de envelope de arquivo, que é bem flexível, permite múltiplos streams de tudo, incluindo de legenda, por isso é muito usado pra compartilhar em torrent.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/0swbtawzxepqgd4r88cfnbg97ne9" alt="MakeMKV"  loading="lazy" /></p>
<p>Com esse container, você consegue acessar via navegador a interface gráfica do aplicativo (é como se fosse um cliente remoto de Windows, como VNC). Basta ter o leitor de Blu-Ray conectado via USB no seu computador e deve achar. Ele vai usar a biblioteca <a href="https://forum.makemkv.com/forum/viewtopic.php?t=18856"target="_blank" rel="noopener">libredrive</a> pra descriptografar os streams de dados do Blu-Ray, e por isso a importância das versões de firmware serem compatíveis (já terem sido quebrados).</p>
<p>Muitos não sabem que a gente chama tudo de &ldquo;Blu-Ray&rdquo; mas na realidade Blu-Ray é pra filmes encodados em versão Full HD, que é 1080p. As versões em 4K (2160p) vem em UHD, que é <strong>Ultra HD</strong>. A caixa de Blu-Ray costuma ser azul, as de UHD são pretas, você quer as UHD sempre que possível, claro. E eu tenho uma coleção enorme:</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>A vantagem de um app como MakeMKV é conseguir descriptografar o stream de video e copiar bit a bit exatamente como está no UHD, a melhor qualidade possível. É o que se chama de REMUX (remuxing), que é extrair o stream do container e colocar em outro container sem re-encodar. Já deve ter visto em alguns arquivos de Torrent, é a melhor qualidade. Porém significa que vai gastar o mesmo espaço que no disco, uma faixa de 50 a 80 GIGAbytes, por filme. Cada uma dúzia de filmes vai consumir quase 1 terabyte. Dependendo de quantos filmes você quer ter, seu espaço livre vai acabar muito rápido.</p>
<p>Pra minimizar isso, podemos recodificar ou &ldquo;re-encodar&rdquo;. É como quando pegamos uma foto em RAW e re-encodamos pra um formato de menor qualidade, como PNG ou mesmo JPEG. Se souber onde procurar, vai achar os artefatos de compressão, mas no geral, compensa. Por exemplo, no meu caso, encodar pra H.265 10-bit, QP 15 Constant Quality.</p>
<p>A melhor opção é ter uma placa de video NVIDIA, com suporte a encoding de video via hardware NVENC e usar o melhor software pra encoding de video: <a href="https://handbrake.fr/"target="_blank" rel="noopener">Handbrake</a>. No meu caso, eu escolho o perfil pré-configurado de <strong>Matroska H.265 MKV 4K60fps</strong> e mudo pra usar o encoder NVENC da minha placa RTX 4090, que isso acelera o encoding em múltiplas vezes.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/vcjscocbej7d64mo6gnvu97rcoiz" alt="HandBrake"  loading="lazy" /></p>
<p>No docker compose, assim como MakeMKV, posso subir esse app pra ter acesso via navegador web, assim:</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>Seu PC tem no mínimo uma placa de video Intel com suporte a encoding via hardware QSV ou melhor, da NVIDIA, com suporte a NVENC. Em ambos os casos, fazemos o container ter acesso a isso mapeando o dispositivo <code>/dev/dri</code>. Sem isso, o encoding vai ser via CPU, e vai demorar ordens de grandeza mais.</p>
<p>Lembrando que encoding via software, é mais lento mas vai ter qualidade superior a Intel QSV que, apesar de rápido, sempre me dá qualidade de video inferior. NVENC é o melhor dos dois mundos: excelente qualidade e muito rápido.</p>
<p><strong>AVISO:</strong> no meu caso, quero a melhor qualidade possível, por isso faço backup de UHD, por isso mesmo torrent eu baixo BR-DISC que é backup de UHD, e isso é absurdamente pesado como expliquei. Daí eu manualmente refaço o encoding pro perfil Matroska H.265 como também expliquei.</p>
<p>Muitos não querem ter esse trabalho, pra isso não deixe de ler o <a href="https://www.reddit.com/r/pirataria/comments/18ch7bt/guia_do_streaming_dom%C3%A9stico_automatizado_sonarr/"target="_blank" rel="noopener">fórum do Reddit</a> que explica como configurar os próximos serviços pra não baixar arquivos grandes assim, eu não vou mostrar aqui, porque não é meu caso.</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>Por mais Blu-Ray e UHDs que você tenha ou consiga comprar, muita coisa nem existe em disco físico. Nesse caso, sua única opção é BitTorrent. E sim, é <strong>ilegal piratear</strong>, eu <strong>não estou incentivando pirataria</strong> bla bla bla &hellip;</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/ar374pd43442x45030482kqhzq17" alt="QBitTorrent"  loading="lazy" /></p>
<p>Cada país tem uma lei, vá pesquisar. Vou assumir que todo mundo sabe com o que está fazendo e não é problema meu. Não adianta comentar sobre isso, não vou alongar isso. A Web já está abarrotada de material a respeito, vá no Google e procure você mesmo.</p>
<p>Primeira coisa que queremos é instalar <a href="https://www.qbittorrent.org/"target="_blank" rel="noopener">QBitTorrent</a>, provavelmente o melhor e mais completo aplicativo pra baixar torrent, e também tem interface web e funciona 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>É importante lembrar de gerar uma porta aleatória nas configurações do QBitTorrent, como mostrado na imagem abaixo. Qualquer porta entre 1025 e 65535 deve funcionar, mas é possível que sua rede bloqueie alguma sequência de portas nesse intervalo, então tem que testar. Pesquise fóruns na web, cuidado que tem muita dica online que já está desatualizado.</p>
<p>Precisa entender o mínimo de redes, entender se está atrás de um NAT, como mapear portas externas, se tem firewall ativo bloqueando portas, etc.</p>
<p>Se o torrent não estiver baixando, &ldquo;pode ser&rdquo; isso. Coloco entre aspas porque é só uma possibilidade. Então clique em &ldquo;random&rdquo; pra escolher outra porta, edite a configuração de docker compose pra mapear a porta nova e reinicie o container.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/vb3jnft1drvqsjjreo2bycp1y0e5" alt="QBitTorrent Port Listen"  loading="lazy" /></p>
<p>Por alguma razão o gerenciamento de salvamento vem tudo como Manual, é importante mudar pra automático:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/jqk5vdpxthahdwi2q5y1edgyxxa6" alt="QBitTorrent Saving Management"  loading="lazy" /></p>
<p>Outro detalhe é pedir pro QBitTorrent re-anunciar os downloads para os trackers quando a porta mudar. Pra isso habilite essa opção &ldquo;Reannounce to all trackers when IP or port changed&rdquo;, senão os downloads podem não reiniciar.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/yoo525n36cc08u6mijoxzwpgg79e" alt="QBitTorrent Reannounce"  loading="lazy" /></p>
<p>Agora o ajuste mais importante desta seção inteira, e que eu aprendi na marra: <strong>bloquear executáveis no momento do download</strong>. Um belo dia achei nove torrents &ldquo;completos&rdquo; na biblioteca cujo único conteúdo era um executável de Windows de quase 1 GB, sem video nenhum. E o pior, o nome do release era limpinho, coisa tipo <code>Reacher S04E07 1080p WEB H264-CAKES</code> ou <code>Rick and Morty S09E10 ... EDITH.scr</code>. Você olha o nome e jura que é o episódio. O <code>.exe</code> ou <code>.scr</code> estava escondido no arquivo lá dentro.</p>
<p>Filtro por nome não pega isso, porque o nome está limpo. A defesa que realmente funciona é bloquear a extensão na hora de gravar. No QBitTorrent, vá em Options, Downloads, e habilite &ldquo;Excluded file names&rdquo; com esta lista:</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>Com isso, o arquivo malicioso simplesmente nunca é escrito no disco. O torrent &ldquo;completa&rdquo; sem nada de útil, o Sonarr ou Radarr percebem que a importação falhou, jogam aquele release na blocklist e saem procurando outro sozinhos. O malware nem toca no disco, e você nem repassa a porcaria seedeando pra frente. Essa é a primeira camada de defesa, e a mais eficaz.</p>
<p>Finalmente, como pesquiso coisas pra baixar? Posso manualmente ir no Google ou DuckDuckGo ou direto em sites como os do PirateBay da vida e buscar arquivos &ldquo;.torrent&rdquo; ou links magnéticos. Pra saber o que é isso, assista meu vídeo sobre <a href="https://www.youtube.com/watch?v=iAA8NrfQtHo"target="_blank" rel="noopener">Criptografia na Prática</a> onde explico tudo sobre Torrent também.</p>
<p>O QBitTorrent tem suporte a plugins de pesquisa. Ele já vem com alguns habilitados. No canto inferior direito, tem o botão de &ldquo;Search plugins&rdquo;. Queremos adicionar o plugin que engloba tudo: o serviço <a href="https://github.com/qbittorrent/search-plugins/wiki/How-to-configure-Jackett-plugin"target="_blank" rel="noopener">Jackett</a>.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/dvndok2h1lgavk0igv24dazi2pwp" alt="Jackett plugin"  loading="lazy" /></p>
<p>Veja que ele quer um host com nome &ldquo;jackett&rdquo; habilitado na porta 9117, pra isso temos este trecho no 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>Ele vai subir um serviço onde podemos acessar a interface web assim:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/p2f8xj5cemh9g48f8sayg52os2ts" alt="Jackett Web UI"  loading="lazy" /></p>
<p>É um serviço que consolida dezenas de sites de pesquisa de torrents num lugar só, consegue testar se estão online e, mais importante, fornece uma chave de API no canto superior direito, que devemos copiar e editar na <a href="https://github.com/qbittorrent/search-plugins/wiki/How-to-configure-Jackett-plugin"target="_blank" rel="noopener">configuração do plugin</a> do QBitTorrent. Com isso é possível fazer pesquisar diretamente, recebendo respostas assim:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/q8tbrlvwuoj2ilehs05kbfj40cs4" alt="QBitTorrent Search Engine"  loading="lazy" /></p>
<p>Veja na coluna da direita que lista de onde puxou esses resultados: está vindo do serviço Jackett.</p>
<p>Um aviso importante, porque isso mudou com o tempo: o Jackett aqui serve pra ISSO, a busca manual que você mesmo faz no QBitTorrent. Não caia na tentação de plugar o Jackett direto no Sonarr e no Radarr. A parte automática vai ser gerenciada pelo Prowlarr, que eu explico mais pra frente, e é ele que alimenta os indexers dos serviços. Eu mantenho o Jackett rodando só pra busca manual e, no máximo, como ponte pra algum tracker que o Prowlarr não suporta nativamente, tipo o RuTracker, que o Prowlarr consegue consumir através do Jackett.</p>
<h2>Radarr (Filmes)<span class="hx:absolute hx:-mt-20" id="radarr-filmes"></span>
    <a href="#radarr-filmes" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Procurar manualmente funciona, mas existem formas melhores: deixar um serviço fazer a procura e gerenciar o download pra você, e pra baixar filmes, podemos usar o serviço <a href="https://radarr.video/"target="_blank" rel="noopener">Radarr</a>:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/d9fz9nc4dc8mgcaowpo5xul6w71e" alt="Radarr Web UI"  loading="lazy" /></p>
<p>Pra isso, temos este trecho no 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>Veja que mapeei diretórios diferentes pra filmes ocidentais e filmes anime, é uma preferência minha, você pode organizar como quiser. Como avisei antes, não copie e cole igual estou mostrando aqui: ajuste os diretórios pra sua máquina.</p>
<p>O principal é que uma vez iniciado, podemos navegar pela interface web, pra opção &ldquo;Settings&rdquo;. Ali vai ter &ldquo;Download Clients&quot;e podemos registrar o serviço web de QBitTorrent:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/o3bkso3dmxppqk7qkbxc5miwt3ul" alt="Radarr QBitTorrent"  loading="lazy" /></p>
<p>Eu não mostrei, mas nas configurações do QBitTorrent vai ter um lugar pra cadastrar essa senha de admin pra acessar a API dele via web. Não esqueça de cadastrar lá e usar a mesma senha aqui. Assim o Radarr pode controlar o download de filmes diretamente com o QBitTorrent, sem você precisar se meter.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/hx4xnogjlt7i4hjkpkrniqgboaui" alt="QBitTorrent Web UI"  loading="lazy" /></p>
<p>Tem um ajuste no Radarr que eu recomendo fazer logo de cara, porque ele vem com um buraco bobo: por padrão, TODA definição de qualidade vem com tamanho mínimo zero. Isso deixa passar arquivo falso minúsculo se passando por 4K. Eu já fui vítima: um tal <code>Superman (2025) WEBDL-2160p.iso</code> de 598 MB entrou como se fosse o filme e ficou meses aparecendo no Plex. Por dentro da ISO tinha um <code>.exe</code> de 597 MB disfarçado.</p>
<p>A correção é ir em Settings, Quality, e botar um piso de tamanho em cada definição de qualidade. Uso mais ou menos assim, em MB por minuto de 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>É baixo o suficiente pra aceitar um bom encode x265 enxuto, e alto o suficiente pra recusar um &ldquo;4K&rdquo; de menos de 1 GB, que só pode ser cilada. Um detalhe chato: o Radarr tem um bug em que salvar tudo de uma vez não persiste o tamanho mínimo, então tem que salvar qualidade por qualidade, uma de cada vez.</p>
<h2>Sonarr (Séries de TV)<span class="hx:absolute hx:-mt-20" id="sonarr-séries-de-tv"></span>
    <a href="#sonarr-s%c3%a9ries-de-tv" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Mesma coisa que o Radarr, mas pra séries de TV, temos o serviço <a href="https://sonarr.tv/"target="_blank" rel="noopener">Sonarr</a>:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/ky9rxjko4m5mfordc84fn6so7xlx" alt="Sonarr Web UI"  loading="lazy" /></p>
<p>Esse serviço é sensacional. Ele acha suas séries, cruzando com bancos de dados online com detalhes como temporadas, nomes de cada episódio e vários outros metadados. Daí ele não só consegue baixar episódios antigos, como mantém uma agenda pra ir baixando séries que ainda estão passando. Toda semana aparece episódio novo sozinho:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/xf3cj62rrzl30q34qnf1f629fmm3" alt="Sonarr Série"  loading="lazy" /></p>
<p>E mesma coisa do Radarr: em Settings e Download Clients, posso registrar meu QBitTorrent, daí o Sonarr se vira pra ir baixando os episódios:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/qxxuy0wa90se9x8alm9pi7ycmzt0" alt="Sonarr QBitTorrent"  loading="lazy" /></p>
<p>E pra subir o serviço, no docker compose temos este trecho:</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>Mesma coisa que o Radarr, eu mapeei pastas diferentes pra anime e não-anime, que é questão de preferência da sua organização.</p>
<p>Tanto no Sonarr quanto no Radarr, tem uma configuração que virou obrigatória pra mim depois que comecei a levar cilada: um Release Profile que bloqueia nomes de release conhecidos por serem armadilha. Em Settings, Profiles, eu deixo uma lista de &ldquo;Must Not Contain&rdquo; com termos assim:</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>Cada um desses tem história. <code>BROADCAST</code>, <code>SODAPOP</code> e <code>REACHERSON</code> são grupos que empacotam RAR dividido com uma pasta <code>Sample/</code> que trava a importação (peguei quase 60 GB de lixo de um <code>Welcome to Derry</code> falso de uma vez). <code>FULL HD</code> é a assinatura de nome dos que carregam <code>.exe</code>. E <code>.scr</code>/<code>.exe</code> no nome é literal mesmo. Um aviso pra quem for copiar: o Radarr, por baixo, usa o mesmo campo <code>ignored</code> que o Sonarr, mesmo a interface chamando de &ldquo;Must Not Contain&rdquo;, então configure a mesma lista nos dois.</p>
<p>Ainda sobra uma classe que nenhum filtro pega sozinho: os fakes de pré-estreia. Aparece um <code>Reacher S04E07</code> com zero seeds pra um episódio que só vai ao ar semana que vem. O Sonarr não recusa release com data anterior à estreia, então esses eu limpo na mão mesmo. A boa notícia é que, com o bloqueio de executável do QBitTorrent lá de cima, mesmo quando um desses baixa, ele vem vazio e é descartado automaticamente.</p>
<h2>Downloads que travam<span class="hx:absolute hx:-mt-20" id="downloads-que-travam"></span>
    <a href="#downloads-que-travam" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Um problema clássico do dia a dia: o torrent fica eternamente preso em &ldquo;downloading metadata&rdquo; ou &ldquo;stalled&rdquo;, com zero peers, e o Sonarr nunca desiste dele sozinho. Já tive episódio parado cinco dias atrás de um magnet morto enquanto o release bom passava batido, esperando a vez que nunca chegava.</p>
<p>A solução que montei é um script simples rodando uma vez por dia (um cron ou um systemd timer, tanto faz). Ele consulta a fila do Sonarr, do Radarr e do Lidarr pela API, acha os itens presos em &ldquo;downloading metadata&rdquo; ou &ldquo;stalled with no connections&rdquo; há mais de 72 horas, e manda remover com um comando só:</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>Isso apaga o download morto, joga o release na blocklist pra não pegar de novo, e a aplicação sai atrás de outra fonte automaticamente. O pulo do gato é o cuidado pra ele NÃO mexer em download que só está lento (menos de 72h) nem em item esperando importação manual, senão você apaga coisa boa por engano. Na primeira vez que rodei, ele limpou dezenove downloads zumbis de uma vez só.</p>
<h2>Prowlarr (indexador)<span class="hx:absolute hx:-mt-20" id="prowlarr-indexador"></span>
    <a href="#prowlarr-indexador" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Pra tanto o Radarr e Sonarr saberem de onde puxar os torrents de séries e filmes, eles precisam pesquisar em indexadores ou trackers públicos (ou privados, se você tiver bons contatos - eu não tenho, nem pergunte). Pra isso temos <a href="https://prowlarr.com/"target="_blank" rel="noopener">Prowlarr</a>. Esse é o serviço mais importante de todos, porque se esse não funcionar, nada vai baixar direito.</p>
<p>Levei um tempo pra chegar numa configuração boa, mas hoje tenho uma regra simples que resolve: o Prowlarr é o único lugar que gerencia indexer. Sonarr, Radarr e Lidarr NÃO devem ter indexer cadastrado direto, só os que o Prowlarr sincroniza. Todo indexer que eu tinha cadastrado na mão dentro do Radarr era fonte de dor de cabeça, inclusive um &ldquo;Lime Torrents&rdquo; que foi exatamente quem me serviu aquele malware da ISO do Superman. Apaguei todos e deixei só o Prowlarr alimentando os três.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/a1xmx4qhm0i554d0vwsij051ivnz" alt="Prowlarr"  loading="lazy" /></p>
<p>Veja que lista dezenas de indexadores, ou trackers. É parecido com o Jackett. A diferença é que o Jackett é pra pesquisas manuais que você mesmo vai fazer, direto no QBitTorrent. Já os indexadores do Prowlarr vão ser configurados direto no Sonarr e Radarr. Aqui começa a confusão, vamos entender:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/5fu7qbgxgr4qf5kzzjyma1zevr9u" alt="Sonarr Indexes"  loading="lazy" /></p>
<p>Tanto no Sonarr quanto no Radarr, além de Download Clients, onde podemos configurar o QBitTorrent, também vai ter essa opção de &ldquo;Indexers&rdquo;, onde podemos configurar em quais trackers procurar pela mídia.</p>
<p>Pense num tracker como um mini-Google público, é onde as pessoas registram seus torrents quando querem compartilhar. Torrents não são mágicos e nem aparecem do nada. Alguém tem que criar um registro em algum lugar. Mas não existe só &ldquo;um&rdquo; lugar: é uma rede distribuída e qualquer participante pode subir um tracker se quiser. É como se você pudesse subir seu próprio Google miniatura, só pra registrar arquivos de torrent.</p>
<p>É um saco ficar gerenciando isso. Alguns trackers pequenos resolvem sair do ar, novos aparecem, configurações são modificadas, e precisamos ficar gerenciando manualmente isso, tanto no Sonarr quanto no Radarr. Pra isso serve o Prowlarr: pra gerenciar os trackers pra você. Vamos entender como.</p>
<p>Nas configurações do Prowlarr, começamos configurando onde encontrar nossos serviços de Sonarr e Radarr:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/t5opcjgeutzt3l4omy6wkk4gbw98" alt="Prowlarr Apps"  loading="lazy" /></p>
<p>Por exemplo, esta é a configuração do meu Sonarr. Notem que o hostname é &ldquo;sonarr&rdquo; e isso funciona porque no meu docker compose eu subo todos os serviços na mesma VLAN, e por padrão o docker registra como hostname o nome do serviço configurado no arquivo YAML. Se não entende Docker, não deixe de assistir meus videos sobre <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>Também precisamos configurar acesso ao QBitTorrent:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/179tso31cnwvyxan9ltvgibeu8sm" alt="Prowlarr QBitTorrent"  loading="lazy" /></p>
<p>Esse outro <strong>SABnzbd</strong> usa outro protocolo de downloads mais antigo, baseado em newsreader de USENET. Sim, aquela Usenet mesmo. Quem sabe, sabe. Eu deixo o torrent como principal, mas mantenho o SABnzbd configurado como segunda fonte, porque muito conteúdo só existe nesses grupos mais antigos. Se for usar, aplique a mesma proteção de extensão do QBitTorrent: no arquivo <code>sabnzbd.ini</code>, na seção <code>[misc]</code>, coloque <code>unwanted_extensions = exe, scr, bat, cmd, msi, lnk, com, vbs, pif</code> e <code>action_on_unwanted_extensions = 2</code>, que manda abortar o download inteiro se aparecer um desses. Assim o Sonarr e o Radarr veem a falha e procuram outra fonte.</p>
<p>Finalmente, muitos trackers hoje implementam algum tipo de proteção contra bots usando captcha da Cloudflare, sabe aquele troço que fica &ldquo;você é um humano?&rdquo;. Pra passar por isso, precisamos do serviço Flaresolvrr, que podemos subir no docker compose assim:</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>No Prowlarr ele entra como um &ldquo;indexer proxy&rdquo;. Você cadastra o FlareSolverr apontando pra <code>http://flaresolverr:8191/</code>, cria uma tag pra ele, e marca com essa tag só os indexers que ficam atrás do Cloudflare. Aí esses passam a resolver o captcha por baixo dos panos, e os outros nem precisam passar por ele:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/7949bp73gbb49994tynwuil9249h" alt="Prowlarr FlareSolvrr"  loading="lazy" /></p>
<p>Ainda não acabamos. Na tela principal, é sair habilitando os indexers que fazem sentido pra você. Não habilite tudo: fica pesado e, pior, alguns indexers servem mais cilada do que conteúdo bom. Veja as tags de cada um. Tem indexer que é só de pornô, tem indexer só de conteúdo russo ou chinês, esses eu pulo.</p>
<p>Os que me servem bem hoje são o <strong>EZTV</strong> e o <strong>1337x</strong> (os dois atrás do Cloudflare, então dependem do FlareSolverr), o <strong>YTS</strong> pra filme, e o <strong>AnimeTosho</strong> mais o <strong>Nyaa</strong> pra anime. No total sobra algo entre 18 e 21 indexers ativos por aplicação. E, mais importante do que habilitar os bons, é desabilitar os ruins: eu desliguei o <strong>LimeTorrents</strong> e o <strong>TorrentDownload</strong>, porque, somados, geraram mais de duzentos downloads e quase nenhuma importação aproveitável, além de serem fonte recorrente de malware e de fake. Menos indexer bom vale mais que um monte de indexer ruim.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/cnl4yefcsvas84m6eslb3in7jmq9" alt="Add Indexer"  loading="lazy" /></p>
<p>No final, é bom clicar esse &ldquo;Test All Indexers&rdquo; pra checar quais ainda estão ativos ou não. E por fim, podemos clicar em &ldquo;Sync App Indexers&rdquo;, que vai cadastrar todos os indexers que sabemos que estão funcionando, nos registros do Sonarr e Radarr. Entenderam? O Prowlarr vai configurar os dois com os indexers mais atualizados, e remover os que não estão mais ativos. Não precisamos configurar cada um deles manualmente.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/if86fzdhieaqt5fxpbozfrh0x13k" alt="Sync App"  loading="lazy" /></p>
<p>É importante entender isso, porque senão você vai ver o Sonarr e o Radarr não conseguindo baixar nada e não vai saber porque: mas é porque os indexers estão errados ou obsoletos ou insuficientes. Gaste bastante tempo lendo a documentação do <a href="https://prowlarr.com/"target="_blank" rel="noopener">Prowlarr</a>.</p>
<h2>Plex (Netflix Particular)<span class="hx:absolute hx:-mt-20" id="plex-netflix-particular"></span>
    <a href="#plex-netflix-particular" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Pra assistir tudo que baixar, tem várias opções. Se é só pra você, basta ir baixando tudo num HD externo ou algo assim, e assistir diretamente do seu PC ou notebook. Dependendo da Smart TV ou console de videogame, também dá pra conectar seu HD ou pendrive via USB e assistir direto.</p>
<p>Se for um pouquinho mais sofisticado, e tiver um PC ou Raspberry Pi sobrando, dá pra instalar Sonarr, Radarr, e compartilhar o diretório de downloads pela rede, usando protocolos como CIFS, que é pra pasta compartilhada de Windows. Novamente, muitas Smart TVs e consoles suportam montar pastas compartilhadas pela rede.</p>
<p>Mas se quiser algo mais amigável e sofisticado, dá pra ter literalmente seu &ldquo;Netflix Particular&rdquo;, com uma interface gráfica parecida com qualquer plataforma de streaming, onde mostra thumbnails, descrições, detalhes do filme ou série, tocador de video e tudo mais e até sugestões do que assistir, sugestões de filme baseado no que está assistindo, informações de atores e tudo mais.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/uvfh2i3ppr5mgrt1xl1r6kjgtvdb" alt="Plex"  loading="lazy" /></p>
<p>Existem vários projetos que fazem isso. Um deles, que a comunidade curte mais por ser 100% aberto, é o <a href="https://jellyfin.org/"target="_blank" rel="noopener">Jellyfin</a> (derivado do antigo Emby). Recomendo que testem, tem tudo que a maioria das pessoas precisa. Basta mapear as pastas de downloas do Sonarr, Radarr e ele vai indexar tudo e organizar pra você. Daí, de qualquer PC, é só acessar a interface web e tudo só funciona.</p>
<p>Mas eu pessoalmente prefiro o <a href="https://www.plex.tv/pt-br/"target="_blank" rel="noopener">Plex</a>, que é mais antigo, é mais fechado (por isso alguns não gostam), mas por oferecer <a href="https://support.plex.tv/articles/202526943-plex-free-vs-paid/"target="_blank" rel="noopener">planos pagos</a> com funcionalidades exclusivas, o nível de polimento é nitidamente melhor. A interface é bem mais bonita, e parece mais &ldquo;profissional&rdquo;. Jellyfin, por melhor que seja, ainda tem cara que foi um programador backend que fez, com muita má vontade em UX e design.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/gh6kum745hoedx34km4rggpfubjd" alt="Plex details"  loading="lazy" /></p>
<p>Além disso, pra assistir numa Smart TV ou Android TV da vida, precisa ter app no Google Play Store. Jellyfin não tem app pra tudo, algumas TV, não lembro se LG ou Samsung, nem tem o app disponível. Então precisa fazer side-load. Não é amigável. Plex, por outro lado, está disponível em bem mais lugares, então costuma ser mais &ldquo;plug and play&rdquo;, parecido com baixar app de Netflix ou Amazon Prime.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/zvipl6wce0rruosctykjnou6ayu0" alt="Plex Skip Intro"  loading="lazy" /></p>
<p>Plex também tem funcionalidades que &ldquo;just works&rdquo;, coisas triviais que se espera num tocador, como botão de &ldquo;pular abertura&rdquo; ou &ldquo;resumir a partir de onde parou&rdquo; ou &ldquo;pular pro próximo episódio&rdquo; ou &ldquo;pesquisar e trocar legenda&rdquo;. É isso que interfaces como essas vão te oferecer, daí a usabilidade é realmente muito parecida com assistir Netflix. Não tem isso se usar um tocador genérico de PC como um VLC da vida.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/mev5extpv8lgv7k1lhsq0bcm8i46" alt="Plex Subtitles"  loading="lazy" /></p>
<p>Pra subir é fácil, eis o trecho do meu 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">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>Existe um pequeno detalhe que muitos não entendem: Plex faz <em>transcoding</em> dos videos. Ou seja, ele vai re-encodar o video antes de mandar pro app ou navegador que vai tocar, isso é pesado, é conversão de stream de video. Se seu servidor não for forte o suficiente, seu video vai ficar engasgando pra tocar.</p>
<p>Na realidade, é mais complicado do que isso. Existe um protocolo chamado &ldquo;Direct Play&rdquo; ou &ldquo;Direct Stream&rdquo;. Tem um <a href="https://support.plex.tv/articles/200250387-streaming-media-direct-play-and-direct-stream/"target="_blank" rel="noopener">artigo que explica isso</a> no site do Plex. Se o PC, SmartTV, console, etc, tiverem suporte ao codec do video em questão, teoricamente o Plex só vai passar os bits direto, sem tentar converter nem nada. Entenda: nem todo dispositivo tem capacidade pra tocar qualquer codec de video.</p>
<p>A maioria do conteúdo hoje em dia é encodado em H.264 ou H.265 ou até AV1, que é mais novo e mais eficiente. Problema, digamos que você queira tocar num PC bem antigo ou num PS4 da vida. Eles vão ter suporte a H.264, talvez H.265, se tiver sorte, mas certamente não a AV1. Daí seria super lento tentar tocar usando só a CPU. O certo é ter instruções de hardware pra tocar AV1, especialmente num CPU antigo.</p>
<p>Então o Plex pergunta pro dispositivo: <em>&ldquo;você suporta AV1?&rdquo;</em> O tocador vai dizer <em>&ldquo;nope, sou fraco.&rdquo;</em> Então o Plex vai fazer o transcoding de AV1 pra H.264, por exemplo. Tudo isso é transparente pra você.</p>
<p>Mas aí você vai notar que na hora de tocar o video fica engasgando, pausa toda hora. Pode ser um problema de banda da sua rede ou, pode ser problema do seu servidor de Plex ser muito fraco ou, de não estar configurado pra transcoding via hardware, e estar indo na força bruta usando só a CPU.</p>
<p>Assista <a href="https://www.youtube.com/watch?v=j4a1SgUWh1c"target="_blank" rel="noopener">meu video sobre compressão</a>, onde explico sobre codecs, mas em resumo, o correto é que seja lá qual PCzinho estiver usando pro seu Plex, que ele tenha o mínimo de capacidade de encoding de video em hardware. A maioria costuma ter pelo menos capacidade de H.264, como as instruções <strong>Intel QSV</strong>, veja se sua CPU tem esse suporte. Só não vai ter se for velho demais, tipo coisa de 10 anos pra trás. Se for um servidor forte, com GPU NVidia, vai ter coisas como <strong>NVENC</strong>, que dá suporte a H.265. Se for CPU Intel moderna, vai ter suporte a <strong>AV1</strong>.</p>
<p>Em todo caso, em Linux, essas instruções devem ficar expostas no dispositivo <code>/dev/dri</code>, e por isso precisamos mapear no container docker como fiz acima. Cheque se existe esse dispositivo.</p>
<p>Se estiver tudo certo, no Plex vai ter essa opção de configurar &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>Queremos nos certificar que a qualidade está em &ldquo;Make my CPU hurt&rdquo; e que a opção de &ldquo;Use hardware acceleration when available&rdquo; está hablitado também, daí ele vai usar o <code>/dev/dri</code> pra acelerar o transcoding. E isso faz uma diferença BRUTAL na velocidade. Se estiver com playback sendo engasgado, muito provavelmente é o transcoding.</p>
<p>Quando o dispositivo, seu smartphone ou sua TV, tem suporte ao exato codec usado no arquivo de video que queremos assistir, teoricamente, o Plex consegue passar o stream direto, sem transcodar, que é o mundo ideal, porque aí não processamento nenhum. Isso é Direct Play. Mas pra isso os arquivos de video precisam estar num determinado codec, numa determinada configuração. Mas mesmo num codec suportado, se sua TV for fraca e não suportar 4K, e pedir pro Plex em Full HD (1080p), daí o Plex vai transcodar de H.264 4K pra H.264 1080p, por exemplo. Por isso falo que é mais complicado do que parece.</p>
<p><strong>DETALHE IMPORTANTE:</strong> Recentemente fiquei confuso ao tocar alguns dos meus videos. Eu tenho o melhor hardware, tanto pro servidor de Plex, com aceleração via hardware, quanto Android TV pra tocar que é meu NVIDIA Shield Pro, quando rede cabeada, etc. Mas alguns videos ficavam engasgando. E no final o culpado não era hardware, nem configuração, nem nada disso, era a maldita legenda:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/z8wlwg5nze02ubi9m8dbkxlqqnin" alt="Plex problema de Legenda PGS"  loading="lazy" /></p>
<p>Quem já precisou baixar legendas, já deve ter visto o formato mais comum, que é o <strong>.SRT</strong> (SubRip). Alguns que se aventuraram mais, já devem ter visto outros formatos, como SSA (SubStation Alpha), TTML (Timed Text Markup Language), VTT (Web Video Text Track), VOBSUB, etc.</p>
<p>Sim, existe MUITO mais que SRT. Em particular, se você fizer como eu e ripar UHDs e extrair as trilhas de legenda do próprio disco, aplicativos como o MakeMKV podem baixar em formato VOBSUB (que é ok) ou o maldito <strong>PGS</strong> (Presentation Graphic Stream Subtitle Format).</p>
<p>A maioria das trilhas de legenda foram feitas pra serem enviadas ao tocador em paralelo, e separado, da trilha de video. Assim como a trilha de áudio vai separado. O tocador recebe trilha de video, trilha de áudio, e toca os dois sincronizados. Mesma coisa com trilha de legenda, no caso de arquivos SRT ou SSA ou TTML da vida.</p>
<p>Mas PGS é diferente: ele tem capacidades gráficas! Estou chutando, mas se já viu legenda coloridas, animadas, que se mexem ou se posicionam em locais absolutos na tela (como pra ficar exatamente em cima de outdoor no video, por exemplo), daí precisa haver um passo antes de rasterizar e renderizar essa legenda gráfica <strong>EM CIMA</strong> do stream de video. Isso existe um passo mais pesado de encoding, ou seja, vai ser <strong>PESADO</strong> de tocar em tempo real.</p>
<p>A solução: use o Plex pra pesquisar uma nova legenda, em formato SRT, e ignore a legenda PGS. Só de fazer isso, parou de engasgar na hora de tocar.</p>
<h2>Seerr (o antigo Overseerr)<span class="hx:absolute hx:-mt-20" id="seerr-o-antigo-overseerr"></span>
    <a href="#seerr-o-antigo-overseerr" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Só com o Sonarr e Radarr, mais os indexers do Prowlarr, já é suficiente pra procurar e baixar tudo. Mas tem um outro app que facilita muito a parte de descobrir coisa nova pra baixar. Ele se chama <a href="https://seerr.dev/"target="_blank" rel="noopener">Seerr</a>, e é a evolução do que antes era o Overseerr. O projeto do Overseerr se juntou com o Jellyseerr, o fork que atendia Jellyfin, e os dois viraram um só: o Seerr, que agora serve Plex, Jellyfin e Emby no mesmo lugar. Se você ainda ouve falar em &ldquo;Overseerr&rdquo; por aí, é isso aqui.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/scl2gpzjir3g0hga7p1ip50cs54w" alt="Seerr"  loading="lazy" /></p>
<p>Ele vai em bancos de dados online gratuitos (tipo os IMDB) da vida e fica de olho em tudo que sai de novo, tudo que está em &ldquo;trending&rdquo;. Se eu quero achar alguma coisa nova, é uma boa. Mesmo coisas que já sei o que quero, basta pesquisar no Seerr e clicar em &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>Eu configuro a integração com o Sonarr e o Radarr nas configurações, assim ele sabe pra quem mandar a requisição. Funciona tudo integrado. Só preciso usar a interface do Seerr pra pedir as coisas e posso esquecer do Sonarr e do Radarr rodando em background.</p>
<h2>Bazarr (legendas)<span class="hx:absolute hx:-mt-20" id="bazarr-legendas"></span>
    <a href="#bazarr-legendas" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Eu não preciso, mas minha namorada às vezes prefere assistir com legendas em português. E é um saco isso porque nem sempre tem, ou o que tem é de baixa qualidade. Aliás, deixa eu já avisar que tanto legenda oficial quanto não-oficial, eu tô cansado de ver tradução errada. Minha diversão é ficar achando os erros na legenda. Se você depende de legenda, saiba que muitos significados importantes estão errados mesmo. Sempre prefira original.</p>
<p>Antigamente tinha que ir manualmente em sites como <a href="https://www.opensubtitles.org/en/search"target="_blank" rel="noopener">OpenSubtitles.org</a> da vida, baixar o arquivo &ldquo;.SRT&rdquo;, renomear igual o nome do arquivo de video e ficar testando manualmente pra ver se tá sincronizado ou não.</p>
<p>Existem dois jeitos de resolver isso. Um é pelo próprio Plex, que tem a opção de pesquisar legendas em diversas línguas e ficar testando direto da interface do tocador. Facilita bastante porque pula o passo de ter que ficar no PC, manualmente baixando arquivos. Como expliquei na seção anterior, no meu caso, que a legenda dos backups de UHD foram extraídos no formato PGS, eu preciso buscar no formato SRT pra não engasgar na hora de tocar.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/ok05g5tdrevvaywqfqmjwy0we3d6" alt="Plex Legendas"  loading="lazy" /></p>
<p>Outra forma é tentar baixar as legendas automaticamente antes. Isso não é perfeito porque existem diversas versões de legendas, algumas foram feitas pra DVD, outras pra BluRay, alguns até pra versões mais populares de &ldquo;CAM&rdquo; que são torrents de videos gravados direto no cinema com um smartphone, que é a versão mais porcaria de todas.</p>
<p>É muito importante renomear os arquivos de video no formato certo, com os metadados bem organizados, como explicado no <a href="https://www.reddit.com/r/pirataria/comments/18ch7bt/guia_do_streaming_dom%C3%A9stico_automatizado_sonarr/"target="_blank" rel="noopener">fórum de Reddit</a> que mencionei no começo do artigo. Isso ajuda o Bazarr a casar a legenda certa com o arquivo certo. E já avisando: Português do Brasil é uma das línguas que menos tem legenda. É mais fácil achar em Francês, Italiano ou Russo do que em Português.</p>
<p>Pra facilitar isso temos o serviço <a href="https://www.bazarr.media/"target="_blank" rel="noopener">Bazarr</a>:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/mjziydblv9rh4yowhuodgxycjzf6" alt="Bazarr Web UI"  loading="lazy" /></p>
<p>Na configuração do Bazarr, vamos apontar pros nossos serviços locais de Sonarr e Radarr, assim ele fica sabendo toda vez que aparece episódio novo de série ou algum filme novo, e já agenda pra tentar procurar a legenda:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/rhiefnojwsullwi1m9swhxds6ocj" alt="Bazarr integração"  loading="lazy" /></p>
<p>O principal é configurar os Providers, que são os sites de onde vêm as legendas. É aqui que mora a diferença entre &ldquo;não acha nada&rdquo; e &ldquo;acha quase tudo&rdquo;. Os cinco que me dão o melhor resultado são: <strong>opensubtitlescom</strong>, <strong>tvsubtitles</strong>, <strong>yifysubtitles</strong> (só filme), <strong>animetosho</strong> (pra anime, nem precisa de conta) e <strong>gestdown</strong> (um proxy do Addic7ed pra inglês, também sem conta). Uma pegadinha que me custou horas: o provider OpenSubtitles.com pede o seu <strong>nome de usuário, não o email</strong>. Se você botar o email, ele dá erro de &ldquo;você não pode consumir este serviço&rdquo;, e ainda te deixa de castigo por 12 horas mesmo depois de corrigir.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/8lzq8umwyjgg40abb1tlvtvt8nxu" alt="Bazarr Providers"  loading="lazy" /></p>
<p>Pra subir o serviço, no docker compose, temos este trecho:</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>Aqui precisamos mapear as pastas do radarr e sonarr pra ele saber onde gravar as legendas. Um detalhe que trava muita gente: mapeie exatamente as mesmas subpastas que o Radarr e o Sonarr usam (no meu caso, <code>/movies</code> e <code>/anime</code>), senão o Bazarr fica reclamando que &ldquo;não acha o arquivo de video pra analisar&rdquo; na biblioteca inteira.</p>
<p>O último passo, e o que fez o Bazarr finalmente virar útil pra mim, é o perfil de idioma. Criei um perfil &ldquo;Português do Brasil com inglês de reserva&rdquo;: ele lista <code>pb</code> primeiro e <code>en</code> depois, com o corte (cutoff) no <code>pb</code>. Na prática, o Bazarr procura as duas, baixa a que aparecer primeiro, e considera o item resolvido assim que existe uma legenda em português. Se o pt-BR nunca aparecer, o inglês fica de backup.</p>
<p>E o pulo do gato, que me custou caro: <strong>todo item precisa ter um perfil de idioma atribuído</strong>. O Bazarr ignora, calado, qualquer filme ou série sem perfil. Quando fui conferir, tinha 140 séries e 201 filmes sem perfil nenhum, sendo ignorados em silêncio desde que entraram na biblioteca. Marcar o perfil como padrão só cobre o que você adiciona dali pra frente; pra biblioteca que já existe, tem que atribuir o perfil em massa, de uma vez, pra tudo que já está lá.</p>
<p>Tem ainda um problema irmão desse, só que do lado do áudio. De vez em quando o Sonarr ou o Radarr baixam um release cujo único áudio é uma dublagem estrangeira: um italiano, um francês, um russo. E nenhuma legenda salva áudio errado. Pra isso eu uso um Custom Format nos dois, o &ldquo;foreign-language-only&rdquo;, que reconhece marcadores de dublagem estrangeira (ITALIAN, FRENCH, RUS, GERMAN e por aí vai) e dá uma pontuação de -500, o que faz o release ser recusado já na busca. Um detalhe que aprendi na marra: um release russo veio com o título todo em cirílico, sem nenhum &ldquo;RUS&rdquo; em letra latina, e passou batido. Então o filtro também precisa pegar qualquer caractere cirílico.</p>
<p>Depois que arrumei os providers, passei a biblioteca inteira pra esse perfil e liguei esse filtro de áudio, o Bazarr deixou de ser o serviço mais inútil da minha stack e virou um que resolve a maioria dos casos sozinho. Quando ainda falta, aí sim eu caço na mão pelo Plex, mas hoje é exceção. Só lembrando o que já falei: legenda em português do Brasil é das mais escassas que existem, então mesmo com tudo certo, às vezes não tem mesmo.</p>
<h2>Lidarr (Música)<span class="hx:absolute hx:-mt-20" id="lidarr-música"></span>
    <a href="#lidarr-m%c3%basica" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Eu não sou muito de música. Tenho uns MP3 que baixei décadas atrás e, no dia a dia, toco a playlist de Top 50 do Spotify e tá bom. Mas a mesma lógica do Sonarr e do Radarr vale pra música, então acabei montando também: o serviço se chama <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>Ele cataloga seus artistas, cruza com bancos de dados de discografia e vai atrás dos álbuns que faltam, marcando cada artista como &ldquo;Monitored&rdquo; pra buscar lançamentos novos sozinho, do mesmo jeito que o Sonarr faz com episódio de série. No print acima dá pra ver minha biblioteca com tudo em &ldquo;Lossless&rdquo;, que é o perfil de qualidade que eu configurei.</p>
<p>No docker compose é o mesmo padrão dos outros:</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>A configuração é idêntica em espírito à do Radarr e Sonarr. Em Settings e Download Clients eu registro o mesmo QBitTorrent, mas usando uma categoria própria chamada <code>lidarr</code>, pra ele não misturar os downloads de música com os de filme e série. A pasta raiz da biblioteca aponta pra <code>/music</code>, e o perfil de qualidade dá pra deixar aberto (o &ldquo;Any&rdquo;, que pega o que aparecer) ou travar em &ldquo;Lossless&rdquo; quando você só quer FLAC, que é o meu caso.</p>
<p>E o melhor: os indexers eu não configuro na mão. O Prowlarr, que eu expliquei lá em cima, também cadastra o Lidarr como aplicação e sincroniza sozinho os indexers que têm categoria de música. É o mesmo &ldquo;Sync App Indexers&rdquo; que ele faz pro Sonarr e Radarr, agora pra música também.</p>
<p>Pra ouvir, o próprio Plex já toca música: basta criar uma biblioteca do tipo Music apontando pra essa pasta, e no celular o app Plexamp é ótimo só pra isso. E, do mesmo jeito que tenho o Kavita como &ldquo;Plex pra mangá&rdquo;, pra música ainda dá pra usar o <a href="https://www.navidrome.org/"target="_blank" rel="noopener">Navidrome</a>, um servidor de streaming open source tipo um Spotify particular, que indexa a mesma pasta <code>/music</code> (só leitura) e tem vários apps de celular compatíveis. O Lidarr baixa e organiza, o Navidrome ou o Plex servem.</p>
<h2>Outros Serviços<span class="hx:absolute hx:-mt-20" id="outros-serviços"></span>
    <a href="#outros-servi%c3%a7os" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>No <a href="https://github.com/akitaonrails/plex_home_server_docker/tree/master"target="_blank" rel="noopener">meu repositório</a> tem outros arquivos de Docker Compose pra minha infra local. Não vou explicar em detalhes, mas só a saber:</p>
<p>Pra bloquear banner, ad, rastreador e até site de malware na rede toda, eu uso DNS filtrado. Durante muito tempo rodei um <a href="https://pi-hole.net/"target="_blank" rel="noopener">Pi-Hole</a> local pra isso, que é um servidor de DNS caseiro com bloqueio embutido. Funciona muito bem, mas dá manutenção. Hoje eu troquei por um serviço hospedado, o <a href="https://nextdns.io/"target="_blank" rel="noopener">NextDNS</a>, que faz o mesmo bloqueio de ads, rastreadores e malware, só que sem eu precisar manter servidor nenhum, e já vem com DNS criptografado de fábrica.</p>
<p>E por que DNS criptografado importa? Query de DNS, por padrão, é um protocolo de texto aberto. Seu provedor sabe exatamente tudo que você navega. Eu prefiro não dar esse dado de graça pra eles. Com o NextDNS as consultas saem encriptadas e ainda passam pela lista de bloqueio que eu escolho. Não substitui um anti-virus, mas dificulta bastante que terceiros montem uma impressão digital dos meus hábitos de navegação, e dá bem menos trabalho que o Pi-Hole que eu mantinha antes.</p>
<p>Agora, no arquivo <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> tem outro serviço tipo Sonarr pra TV, Radarr pra filmes, que é o <a href="https://github.com/oae/kaizoku"target="_blank" rel="noopener">Kaizoku</a> pra mangá. Se você lê mangá online, já deve ter visto muitos sites sendo fechados por causa de copyright, direitos autorais e tudo mais. Eventualmente, os mangás online que você leu, vão desaparecer. Com o Kaizoku podemos preservar eles localmente também.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/iucprcsmn9emkzzurs7z2fstcsjm" alt="Kaizoku"  loading="lazy" /></p>
<p>O Kaizoku é só pra baixar. Pra ler, precisamos de um leitor, da mesma forma que temos Plex pra video, temos <a href="https://www.kavitareader.com/"target="_blank" rel="noopener">Kavita Reader</a> pra ler os mangás baixados. Assim como Plex, basta na primeira configuração apontar pra pasta onde o Kaizoku baixa tudo, daí o Kavita vai se virar pra organizar a biblioteca, baixar capas e outros metadados. É literalmente o Plex pra Mangá.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/bxuz95wo3luc308lz1dos9bf2kcr" alt="Kavita"  loading="lazy" /></p>
<p>Ainda não explorei muito esse serviço. Ou ainda tem coisas pra refinar nele, ou eu que não sei configurar direito, mas nem tudo baixa capas direito. Vejam como não está bonito. Se tiverem dicas sobre Kavita e Kaizoku, não deixem de compartilhar abaixo.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/bcrh2ifucs86fupzk8gqa38yx5iq" alt="Kavita Reader"  loading="lazy" /></p>
<p>Pra ler em si, é bem parecido com um Kindle Web ou outros sites online de mangá. Dá pra ler página a página, ou &ldquo;scrollar&rdquo; o capítulo inteiro, ele vai lembrar onde você parou e coisas básicas assim. Pode ser um bom projeto pra contribuir, se tem vontade de treinar com código aberto.</p>
<h2>Conclusão<span class="hx:absolute hx:-mt-20" id="conclusão"></span>
    <a href="#conclus%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Fico muito contente com o estado atual dos serviços de compartilhamento de arquivos. Não percam muito tempo pensando demais. Só instalem e se divirtam. O código de tudo é aberto, se você é programador, vá lá fuçar o código e aprender algo novo, e quem sabe, até contribuir.</p>
<p>Eu não sou adepto de teorias apocalípticas, teorias da conspiração, nem nada assim. Eu só considero que, o que eu não tenho controle físico, eu <strong>não tenho</strong>. Se o que &ldquo;é meu&rdquo; está no controle de um terceiro, então &ldquo;não é meu&rdquo;. Esse é o princípio. Eu quero tudo que é meu sob meu único e exclusivo controle e pronto. Não importa se é menos conveniente, não importa se é mais caro. É uma questão de princípios: meu é meu e ponto final.</p>
<p>Além disso, nas plataformas de streaming, não existe tudo. Muita coisa de décadas passadas simplesmente foram apagadas da memória. Muitos nem lembram mais, especialmente quem não viu na época. Eu tenho nostalgia por programas de TV, filmes, que não existe online. Muitos sequer existem em mídia física. Tem filmes que depois de VHS nem chegaram a sair em DVD. Tem DVD que nunca saiu como Blu-Ray. Tem coisa em disco físico que nunca vai sair em streaming. Tem coisa em streaming que vai ser apagado em breve, e muitos nem vão notar.</p>
<p>O que é meu, é meu. Podem apagar o quanto quiserem. O que é meu, ninguém apaga. Se você tem apreço pelo que é seu, comece a explorar estas opções. Não precisa ser um setup gigante como o meu. Qualquer HD externo sobrando já dá pra começar. Comece pequeno, vá aumentando aos poucos. Eu coleciono essas coisas faz décadas! Não comecei hoje.</p>
]]></content:encoded><category>homelab</category><category>containers</category><category>audio-e-video</category></item><item><title>[Akitando] #149 - Configurando Docker Compose, Postgres, com Testes de Carga - Parte Final da Rinha de Backend</title><link>https://akitaonrails.github.io/2023/12/16/akitando-149-configurando-docker-compose-postgres-com-testes-de-carga-parte-final-da-rinha-de-backend/</link><guid isPermaLink="true">https://akitaonrails.github.io/2023/12/16/akitando-149-configurando-docker-compose-postgres-com-testes-de-carga-parte-final-da-rinha-de-backend/</guid><pubDate>Sat, 16 Dec 2023 09:30:00 -0300</pubDate><description>&lt;div class="embed-container"&gt;
&lt;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&gt;
&lt;/iframe&gt;
&lt;/div&gt;
&lt;p&gt;Não podia terminar o ano com pendências, então eis a parte final da Saga da Rinha de Backend. Neste video vou aproveitar os temas da rinha pra demonstrar em mais detalhes como configurar um Docker Compose de verdade, como funciona testes de carga com Gatling, como usar esses dados pra configurar coisas como o Postgres melhor. Também vamos ver como avaliar exatamente quanto pesa rodar SQL num Postgres, como determinar coisas com tamanho de pool de conexões e muito mais!&lt;/p&gt;</description><content:encoded><![CDATA[

<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>Não podia terminar o ano com pendências, então eis a parte final da Saga da Rinha de Backend. Neste video vou aproveitar os temas da rinha pra demonstrar em mais detalhes como configurar um Docker Compose de verdade, como funciona testes de carga com Gatling, como usar esses dados pra configurar coisas como o Postgres melhor. Também vamos ver como avaliar exatamente quanto pesa rodar SQL num Postgres, como determinar coisas com tamanho de pool de conexões e muito mais!</p>
<p>Hoje é dia de aprender ferramentas que todo programador deveria mesmo saber no dia a dia de projetos de verdade.</p>
<h2>Capítulos<span class="hx:absolute hx:-mt-20" id="capítulos"></span>
    <a href="#cap%c3%adtulos" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><ul>
<li>00:00:00 - Intro</li>
<li>00:01:03 - CAP 01 - Minhas Pendências da Rinha - Como Começar?</li>
<li>00:03:28 - CAP 02 - Entendendo Docker Compose - PHP do Lauro</li>
<li>00:07:38 - CAP 03 - Aprendendo a Lidar com Poucos Recursos - Configurando NGINX</li>
<li>00:15:33 - CAP 04 - Gargalos de Docker - Network Mode: Host</li>
<li>00:21:41 - CAP 05 - Prefira Docker em Linux - Problemas em Mac e Windows?</li>
<li>00:26:51 - CAP 06 - Algumas Dicas de Docker Compose - Node do Lucas Weis</li>
<li>00:30:21 - CAP 07 - Básico de Testes de Carga - Gatling</li>
<li>00:44:26 - CAP 08 - Comparando com Network Mode HOST - Faz diferença??</li>
<li>00:52:53 - CAP 09 - Entendendo PostgreSQL - Banco era Gargalo??</li>
<li>01:01:12 - CAP 10 - Full Text Search no PostgreSQL - Faz diferença??</li>
<li>01:07:35 - CAP 11 - Estratégia de SQL com EXPLAIN ANALYZE - SQL por baixo dos panos</li>
<li>01:16:11 - CAP 12 - Quantas Conexões Configuro no Banco? - Medindo com pgAdmin</li>
<li>01:23:34 - CAP 13 - Discutindo sobre Linguagens - Kotlin e Crystal</li>
<li>01:31:05 - CAP 15 - Como Linguagens afetam Recursos do Banco? - Só a saber, sobre threads vs fibers</li>
<li>01:35:00 - Bloopers</li>
</ul>
<h2>Links<span class="hx:absolute hx:-mt-20" id="links"></span>
    <a href="#links" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><ul>
<li><a href="https://github.com/zanfranceschi/rinha-de-backend-2023-q3"target="_blank" rel="noopener">Repositório Oficial da Rinha</a></li>
<li><a href="https://github.com/lukas8219/rinha-be-2023-q3"target="_blank" rel="noopener">GitHub Versão Node.js do Lucas Poole</a></li>
<li><a href="https://github.com/lauroappelt/rinha-de-backend-2023"target="_blank" rel="noopener">GitHub Versão PHP do Lauro Appelt</a></li>
<li><a href="https://github.com/akitaonrails/rinhabackend-rails-api"target="_blank" rel="noopener">GitHub - akitaonrails/rinhabackend-rails-api</a></li>
<li><a href="https://docs.docker.com/network/drivers/host/"target="_blank" rel="noopener">Docker Network Mode Host</a></li>
<li><a href="https://gatling.io/docs/gatling/tutorials/installation/"target="_blank" rel="noopener">Gatling</a></li>
<li><a href="https://github.com/tsenart/vegeta"target="_blank" rel="noopener">Vegeta</a></li>
<li><a href="https://www.haproxy.org/"target="_blank" rel="noopener">HAProxy - The Reliable, High Perf. TCP/HTTP Load Balancer</a></li>
<li><a href="https://www.envoyproxy.io/"target="_blank" rel="noopener">Envoy proxy - home</a></li>
<li><a href="https://caddyserver.com/docs/quick-starts/reverse-proxy"target="_blank" rel="noopener">Reverse proxy quick-start — Caddy Documentation (caddyserver.com)</a></li>
<li><a href="https://github.com/jesseduffield/lazydocker"target="_blank" rel="noopener">GitHub - jesseduffield/lazydocker: The lazier way to manage everything docker</a></li>
<li><a href="https://www.commandprompt.com/education/how-to-insert-bulk-data-in-postgresql/"target="_blank" rel="noopener">How to Insert Bulk Data in PostgreSQL - CommandPrompt Inc.</a></li>
<li><a href="https://www.pgadmin.org/"target="_blank" rel="noopener">pgAdmin - PostgreSQL Tools</a></li>
<li><a href="https://start.spring.io/"target="_blank" rel="noopener">Spring Initializer</a></li>
<li><a href="https://kotlinlang.org/"target="_blank" rel="noopener">Kotlin Programming Language (kotlinlang.org)</a></li>
<li><a href="https://crystal-lang.org/"target="_blank" rel="noopener">The Crystal Programming Language (crystal-lang.org)</a></li>
<li><a href="https://luckyframework.org/"target="_blank" rel="noopener">Lucky - Web framework for Crystal (luckyframework.org)</a></li>
<li><a href="https://aws.amazon.com/pt/what-is/elk-stack/"target="_blank" rel="noopener">O que é a pilha ELK? | Explicação sobre a pilha Elasticsearch, Logstash, Kibana | AWS (amazon.com)</a></li>
<li><a href="https://aws.amazon.com/pt/what-is/elasticsearch/"target="_blank" rel="noopener">O que é Elasticsearch – Explicação sobre o mecanismo Elasticsearch – AWS (amazon.com)</a></li>
<li><a href="https://aws.amazon.com/elasticache/"target="_blank" rel="noopener">Redis and Memcached-Compatible Cache – Amazon ElastiCache – Amazon Web Services</a></li>
<li><a href="https://aws.amazon.com/pt/msk/"target="_blank" rel="noopener">Apache Kafka totalmente gerenciado | Amazon MSK | Amazon Web Services</a></li>
<li><a href="https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_PostgreSQL.Replication.ReadReplicas.html"target="_blank" rel="noopener">Working with read replicas for Amazon RDS for PostgreSQL - Amazon Relational Database Service</a></li>
<li><a href="https://stackoverflow.blog/2020/10/14/improve-database-performance-with-connection-pooling/"target="_blank" rel="noopener">Improve database performance with connection pooling - Stack Overflow</a></li>
<li><a href="https://brandur.org/postgres-atomicity"target="_blank" rel="noopener">How Postgres Makes Transactions Atomic (brandur.org)</a></li>
</ul>
<h2>SCRIPT<span class="hx:absolute hx:-mt-20" id="script"></span>
    <a href="#script" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Olá pessoal, Fabio Akita</p>
<p>Antes de ir pro assunto de hoje, só confirmando que sim, o último video foi um experimento de fazer um episódio totalmente gerado por IA. Maioria sacou, mas muitos demoraram bastante ou nem se tocaram. Mas não se preocupem, foi só uma vez pra ver como fica. Não é nem um pouco prático fazer videos via ferramentas de IA como o HeyGen. Eu expliquei o processo na descrição do video, que vocês sempre pulam. Então leiam lá.</p>
<p>Assunto de hoje, finalmente, vai ser a prometida Parte 2 da Rinha de Backend. Sim, o video das 16 Linguagens em 16 dias foi lançado em Setembro e eu prometi uma parte 2. Estamos em dezembro, quase virando o ano e só agora estou soltando. Mas é assim que este canal funciona: me deu vontade de falar de outras coisas antes.</p>
<p>Objetivo deste video é complementar algumas dicas e informações que podem auxiliar nos seus estudos. Em particular algumas das técnicas que diferenciam projetinho de tutorial de projeto de verdade. Então, vamos lá &hellip;</p>
<p>(&hellip;)</p>
<p>Um dos motivos de porque demorei tanto pra fazer esse episódio é por causa destes dois itens que eu tinha colocado pra falar neste episódio: Resolvendo a Rinha em 5 minutos e O que é um framework web? Esses dois itens seriam um video inteiro de 2 horas e sinceramente, não estou com paciência pra esse tema porque já tem documentação suficiente online. Abra o site de qualquer framework web, seria o primeiro tutorial de como usa. Eu só estaria repetindo o que já existe online. Em vez disso vou só resumir alguns pontos principais.</p>
<p>Resolvendo a Rinha em 5 minutos. Sim, eu poderia fazer um speed run e editar um video demonstrando passo a passo como fazer uma versão das APIs. Eu acho inútil. Speed run de código só pra mostrar que sei fazer, realmente não me motiva. É óbvio que eu sei fazer. Flex desnecessário. Aliás, o video de 16 linguagens em 16 dias já foi flex o suficiente.</p>
<p>Se você é iniciante e tem dificuldade de resolver esse exercício, obviamente ainda falta estudar mais e treinar mais. O que eu sempre repito? Não existe uma única forma de resolver um problema de código. Não existe um único passo a passo de tutorial ou curso que consiga cobrir todas as permutações. O que vocês tem que fazer? Abrir o GitHub do evento e ir no diretório de participantes. Por isso achei este evento interessante: pela quantidade de versões diferentes que os participantes publicaram pra resolver o mesmo problema.</p>
<p>Vou dar um exemplo. Fiz clone do repositório principal da rinha na minha máquina. Peguem o link na descrição abaixo. Indo no diretório de participantes, temos 94 versões. Eu analisei a maioria delas no outro video, depois assistam lá. Mas digamos que esteja estudando PHP e queira ver como se resolve esse problema em PHP. A partir do diretório de participantes, só abrir um terminal e digitar <code>grep &quot;php&quot; */README.*</code> e olha só, aparecem 4 sub-diretórios, dois do Lauro Appelt e dois de um tal de Predator. No video anterior eu expliquei uma das versões do Lauro, que é feito em Hyperf com Swoole.</p>
<p>Vamos abrir o README de um dos diretórios e olha só, temos o link do repositório com o código em PHP do Lauro. Podemos clonar em qualquer outro lugar e fuçar pra ver como foi que ele implementou a versão dele. A parte mais interessante desse evento, é que um dos requerimentos foi pra todo participante criar Dockerfiles e Docker Compose pra tudo. Então é super fácil de executar, sem precisar adivinhar como configurar o ambiente pra cada projeto.</p>
<p>Hoje em dia eu considero de boa educação, boa etiqueta, que todo projeto, não importa quão pequeno ou quão grande, sempre deve ter um Dockerfile e um docker-compose. O Dockerfile demonstra como fazer pra rodar sua aplicação e o docker compose demonstra como subir as dependências desse projeto, como banco de dados. Vamos olhar o Dockerfile do projeto do Lauro.</p>
<p>Primeiro, ele inicia baixando uma imagem preparada pelo pessoal do framework Hyperf, que já trás tudo que você vai precisar. Em seguida, faz algumas configurações de timezone. Maioria das imagens boota com tempo UTC de Greenwich, timezone zero, como padrão. Pra ter os horários corretos da sua região geográfica, precisa colocar algo como &ldquo;America/Sao_Paulo&rdquo;, sempre. Ninguém vai adivinhar.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>
FROM hyperf/hyperf:8.2-alpine-v3.18-swoole

ARG timezone

ENV TIMEZONE=${timezone:-&#34;America/Sao_Paulo&#34;} \
    APP_ENV=prod \
    SCAN_CACHEABLE=(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>Esse trecho seguinte grandão aqui é opcional e eu não testei o suficiente pra saber se era ou não necessário pra melhorar os tempos do benchmark. Tudo que ele está fazendo é criando um arquivo de variáveis de configuração, pra mexer em configurações como tamanho máximo de upload, que é desnecessário neste projeto, ou limite de memória, que talvez possa ser útil ou não, não medi pra saber.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>
RUN set -ex \

    &amp;&amp; php -v \
    &amp;&amp; php -m \
    &amp;&amp; php --ri swoole \

    &amp;&amp; cd /etc/php* \

    &amp;&amp; { \
        echo &#34;upload_max_filesize=128M&#34;; \
        echo &#34;post_max_size=128M&#34;; \
        echo &#34;memory_limit=1G&#34;; \
        echo &#34;date.timezone=${TIMEZONE}&#34;; \
    } | tee conf.d/99_overrides.ini \

    &amp;&amp; ln -sf /usr/share/zoneinfo/${TIMEZONE} /etc/localtime \
    &amp;&amp; echo &#34;${TIMEZONE}&#34; &gt; /etc/timezone \

    &amp;&amp; rm -rf /var/cache/apk/* /tmp/* /usr/share/man \
    &amp;&amp; echo -e &#34;\033[42;37m Build Completed :).\033[0m\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>Não saia copiando e colando achando que vai automaticamente melhorar alguma coisa. Vá na documentação e entenda item a item daqui. Daí rode uma versão sem isso, e outra versão com isso, rodando o mesmo benchmark, daí compare se faz alguma diferença. O fato de coisas que não importam pra este desafio, como tamanho máximo de upload, me diz que o Lauro copiou o Dockerfile de exemplo no site da imagem desse Hyperf, e colou aqui.</p>
<p>Finalizando, adicionamos drivers de postgres pra PHP, mudamos o diretório de trabalho pra /app, que é pra onde vamos copiar os arquivos do projeto na sequência. O que eu achei estranho é que não tem um último comando de ENTRYPOINT indicando como rodar a aplicação, também senti falta de rodar &ldquo;composer install&rdquo; pra baixar as bibliotecas. Isso é uma peculiaridade desta versão do Lauro, guardem isso que já já explico porque tá faltando.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>RUN set -ex \
    &amp;&amp; apk --no-cache add \
    &amp;&amp; apk add php82-pdo_pgsql

WORKDIR /app

COPY . /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>Agora vamos ver o docker-compose.yaml. Docker compose é a descrição de uma mini-infraestrutura. Do que instalar, configurar e subir Redis, Postgres, Nginx, tudo na mão, podemos automatizar tudo num único comando. É muito mais fácil pra um novo desenvolvedor que entra no seu projeto, aprender como as coisas funcionam, se tiver um docker compose já pronto. Funciona com uma documentação viva da infra do projeto. É de bom tom, todo projeto de verdade, ter docker compose configurado que funciona.</p>
<p>Nunca fez um docker compose na vida? Como faz? Simples, começa pegando qualquer um dos 90 e tantos exemplos que tem neste evento como base. Vamos abrir o do Lauro e ir de baixo pra cima. Olha só, já sabemos que esse projeto depende de Redis estar de pé. Esse nome &ldquo;redis&rdquo; na primeira linha é qualquer nome que quiser. Por convenção colocamos o nome do que vamos rodar, mas poderia ser &ldquo;cache_server&rdquo; ou algo assim.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>    redis:
        container_name: rinha-backend-redis
        image: redis:latest
        command: redis-server --save &#34;&#34; --appendonly no --maxclients 20000
        network_mode: host
        deploy:
            resources:
                limits:
                    cpus: &#39;0.1&#39;
                    memory: &#39;0.2GB&#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>A única linha obrigatória mesmo é o nome da imagem. Docker, por padrão, vai baixar tudo de dockerio.com. Se quiser baixar de outro lugar, tem que colocar a URL inteira, senão só o nome e a versão da imagem é suficiente. Aqui está &ldquo;latest&rdquo;, que é a versão mais recente. Não gosto de usar latest porque se amanhã mudar a versão, e sua app não foi adaptada e teve mudanças que quebram compatibilidade, seus desenvolvedores vão começar a ter problemas. Se forem muito júniors, não vão associar que foi porque mudou a versão do redis e por isso as coisas quebraram do nada. É melhor colocar a versão fixa, como 7.2.3-alpine3.19, que significa Redis versão 7.2.3 rodando em cima da distro Alpine versão 3.19.</p>
<p>As outras opções são específicas da Rinha, em particular os limites de recursos na seção de deploy. Esse foi um dos critérios, que achei inteligente. Tínhamos no máximo 1.5 CPUs e 3 Gigabytes de RAM pra usar. À primeira vista, parece muito pouco. Não é fácil de imaginar como esses recursos encaixam em cada componente. Se você é um iniciante e tem uma máquina fraca, já aprendeu a ter que lidar com isso: como faz pra tirar o máximo proveito do seu Dual Core com 2 gigabytes de RAM? Todo programador que ficou bom, começou tendo que fazer rodar programas pesados em máquinas fracas.</p>
<p>Se você é um iniciante e já começou com oportunidade de usar máquinas novas e parrudas pra trabalhar, qualquer coisa acima de um quad core, acima de 8 gigabytes de RAM, tipo um Macbook da vida, e não teve que lidar com máquinas fracas, cuidado. Você vai pode acabar ficando complacente, preguiçoso. Vai achar que o mundo é igual sua bolha: que tem CPU sobrando, que tem RAM sobrando, e nunca vai entender o valor de otimizar as coisas. Pra que otimizar? Se tá tudo sobrando? Na hora que ficar difícil, você vai ser o tipo que desiste fácil.</p>
<p>Por isso eu repito que na minha teoria de Aprendendo a Aprender, tem que ter a Dor de Aprender. Não importa se você cai, importa como se levanta. Não importa quantas vezes cai, importa que toda vez se levanta mais rápido. Quem nunca sentiu dor, nunca aprendeu nada. E nesta rinha, a versão disso é essa limitação de recursos, senão, claro, o participante com uma máquina como a minha, com 64 giga de RAM, Ryzen 9 de 32 threads, facilmente conseguiria gerar resultados com números muito melhores que todo mundo. Mas com esse limite, o participante com um quad core de 4 giga de RAM poderia ultrapassar minha máquina, se soubesse administrar melhor os recursos limitados.</p>
<p>E isso de limitar recursos no Docker, não foi feito pra concursos. Foi feito pro caso de alguém realmente precisar rodar dois processos numa mesma máquina, e caso um deles seja fominha demais e consuma recursos demais, não deixar o outro processo morrendo de fome. Se o servidor tem 4 giga de RAM, eu subo dois programas, cada um no seu container de docker. Digamos que o primeiro resolva alocar toda a memória disponível, mesmo que não vá usar. Daí quando o segundo container tentar rodar, vai crashear, por falta de recursos.</p>
<p>Existem processos que alocam mais memória do que deveriam? Sim, por exemplo, um Node.js tem recurso de cluster, que eu mencionei no episódio anterior. O que muita gente faz? Vai no Google pesquisar e só copia um trecho parecido com esse de algum stackoverflow da vida. Esse script roda &ldquo;os.cpus.length&rdquo; e vê que sua máquina tem 4 cores, ou seja, 4 cpus, então faz 4 forks da aplicação. Cada fork pode ocupar, digamos, meio gigabyte. Então 4 forks daria 2 gigabytes. Como sua máquina tem 8 gigabytes, você nem se preocupa. Funciona tudo direitinho.</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="kr">const</span> <span class="nx">numForks</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">&#39;os&#39;</span><span class="p">).</span><span class="nx">cpus</span><span class="p">().</span><span class="nx">length</span> <span class="o">||</span> <span class="mi">4</span><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="nx">cluster</span><span class="p">.</span><span class="nx">isPrimary</span> <span class="o">&amp;&amp;</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">CLUSTER</span> <span class="o">===</span> <span class="s1">&#39;true&#39;</span><span class="p">){</span>
</span></span><span class="line"><span class="cl">    <span class="nx">logger</span><span class="p">.</span><span class="nx">info</span><span class="p">(</span><span class="sb">`index.js: Primary </span><span class="si">${</span><span class="nx">process</span><span class="p">.</span><span class="nx">pid</span><span class="si">}</span><span class="sb"> is running`</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="nx">numForks</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nx">cluster</span><span class="p">.</span><span class="nx">fork</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>Aí você aloca um servidor num cloud qualquer, com 8 giga de RAM. Mas resolve pegar um CPU potente, de 16 cores. Seu script de cluster vai ver 16 cpus e subir 16 forks. Cada fork ocupando meio giga de RAM. Ou seja, vai ocupar todos os 8 giga de uma só vez. E você não tem 8 giga sobrando, seu sistema operacional e outros serviços ocupam RAM. Alguns desses forks vão crashear logo no boot.</p>
<p>O certo é consertar esse script e trocar a chamada de os.cpus e usar uma variável de ambiente no lugar. Assim podemos declarar essa variável, como CLUSTER_WORKERS, no yaml do docker compose, e ter controle sobre quantos processos realmente vai subir. Como desenvolvedor você sempre precisa ter controle sobre o que exatamente vai rodar, quantos recursos vai usar. Um bom jeito de se exercitar é acertando script de 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-js" data-lang="js"><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">numForks</span> <span class="o">=</span> <span class="nb">Number</span><span class="p">(</span><span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">CLUSTER_WORKERS</span><span class="p">)</span> <span class="o">||</span> <span class="mi">4</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>Continuando, depois do redis tempos nginx. Esse foi outro pré-requisito nas instruções da rinha: ter um proxy reverso pra servir de balanceador de carga entre, no mínimo,  duas instâncias da sua aplicação. Quando um estudante segue um tutorial de web qualquer, ele só entende que tem um único processo, o servidor da aplicação. Sobe numa porta como 4000 e no navegador coloca localhost:4000 e pronto, acabou.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>    nginx:
        container_name: rinha-backend-nginx
        image: nginx:alpine
        volumes:
            - ./docker/nginx/nginx.conf:/etc/nginx/nginx.conf
        network_mode: host
        depends_on:
            - api-01
            - api-02
        deploy:
            resources:
                limits:
                    cpus: &#39;0.1&#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>Esse é o nível zero no aprendizado. O nível logo acima desse hiper básico, tem que aprender que em projetos de verdade, subimos múltiplos processos em paralelo, seja na mesma máquina, seja em múltiplas máquinas. Se for na mesma máquina, um sobe na porta 3000, outro na porta 3001, outro na 3002. Em qualquer dos casos, sempre colocamos um balanceador de cargas na frente, na forma de um proxy reverso. Explico isso no episódio &ldquo;Tornando sua Web App Mais Rápida&rdquo;.</p>
<p>Alguns mais experientes poderiam criticar a escolha de NGINX, sendo que existem ou programas mais especializados pra isso como HAProxy ou mais modernos como o Envoy ou Caddy. Mas de jeito nenhum é uma escolha ruim, especialmente porque é um dos que tem mais documentação, é simples de usar, e pra esta rinha, está mais que bom. Nesta seção de docker compose temos duas coisas importantes, a primeira é a imagem. No caso estamos usando a versão de Alpine, que é uma das mais leves. Mais importante é o mapeamento de volume.</p>
<p>Precisamos mapear o arquivo de configuração que fica no nosso projeto do lado de fora, para que o container tenha acesso quando bootar. Lembrem-se, um container é só uma forma de mentir pro processo que roda dentro, pra ele achar que está sozinho, numa máquina vazia. Ele não enxerga nenhum arquivo de fora se nós não declararmos pra dentro, da seguinte forma.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>volumes:
	- ./docker/nginx/nginx.conf:/etc/nginx/nginx.conf</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>O que tem nesse arquivo de configuração? Vamos dar uma olhada. Se nunca viu, parece complicado, mas vamos por partes. Esta primeira parte do topo tem a ver com a escalabilidade do serviço. Nginx funciona numa arquitetura de master e workers. Esse &ldquo;worker_processes&rdquo; como &ldquo;auto&rdquo; de automático quer dizer que ele vai avaliar quantas cpus sua máquina tem e subir 1.5 vezes. Então, se tiver 4 cpus, ele vai subir 6 workers.</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-c" data-lang="c"><span class="line"><span class="cl"><span class="n">worker_processes</span> <span class="k">auto</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>Por que não 1 worker por 1 cpu? Primeiro, uma cpu não é exclusiva de 1 processo. Quem controla qual processo vai rodar em qual cpu durante um período x de tempo, é o scheduler da kernel. Se seu processo é leve e precisa de pouco tempo pra fazer o que precisa, não tem porque bloquear uma cpu inteira só pra ele. Dá pra dividir com outros processos. Depois assista meus episódios sobre concorrência e paralelismo pra entender como schedulers funcionam.</p>
<p>Em seguida temos worker_connections como 4096. Isso indica quantas conexões simultâneas cada worker vai tentar aguentar. Não confunda conexões simultâneas com processamento paralelo. Se realmente vier 4096 requisições web, exatamente no mesmo instante, esse parâmetro só significa que o worker vai aceitar as 4096 conexões, mas uma boa parte vai ficar esperando numa fila, até ser atendido. Se demorar mais do que certo tempo, que podemos configurar, ele pode rejeitar e devolver erro de timeout. Como o nome diz, erro de tempo expirado.</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-c" data-lang="c"><span class="line"><span class="cl"><span class="n">events</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">worker_connections</span> <span class="mi">4096</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>No primeiro episódio que fiz da rinha eu expliquei como muitos ficaram tentando mexer nesse número meio que no chute. Só o Leandro, que fez a versão em Ruby, entendeu que o truque não era aumentar esse número e sim diminuir. Eu explico o raciocínio no outro video, então não deixem de assistir depois.</p>
<p>O Nginx é só o tirador de pedidos, lembram da analogia do caixa de restaurante fast food? Ele tira os pedidos, mas quem realmente executa, os cozinheiros, são estes servidores declarados nesta seção upstream. No caso apontando pra localhost, portas 9501 e 9502. Se já mexeu com docker compose antes, isso poderia parecer um pouco estranho.</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-c" data-lang="c"><span class="line"><span class="cl"><span class="n">http</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">access_log</span> <span class="n">off</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="n">error_log</span> <span class="o">/</span><span class="n">dev</span><span class="o">/</span><span class="n">null</span> <span class="n">emerg</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">upstream</span> <span class="n">api</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="n">server</span> <span class="nl">localhost</span><span class="p">:</span><span class="mi">9501</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="n">server</span> <span class="nl">localhost</span><span class="p">:</span><span class="mi">9502</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="n">keepalive</span> <span class="mi">400</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>Normalmente, pra subir um nginx via docker compose, eu teria uma configuração mais ou menos assim: declararia dois serviços web, as instâncias da minha aplicação, como app1 e app2. Ambos carregando na porta 8080. Daí, na configuração do nginx, essa seção upstream estaria apontando pra app1 porta 8080 e app2 também porta 8080.</p>
<p>Mas como que dá pra ter dois serviços subindo na mesma porta? Não tem conflito? E não tem, porque na prática é como se cada container representasse um servidor externo separado. E sua aplicação está carreagando num servidor fora, só que virtual. Daí, cada servidor tem uma porta 8080 livre. Cada instância pode se pendurar na porta 8080 do seu respectivo &ldquo;servidor virtual&rdquo;. Acontece que o nginx e esses servidores virtuais se comunicam na mesma rede, também virtual, daí o nginx consegue fazer o balanceamento entre esses dois serviços.</p>
<p>No episódio anterior eu expliquei a descoberta que alguns de nós fizemos durante nossos experimentos, o Vinicius Ferraz fez a descoberta de que se usarmos o modo de rede de ponte virtual, que é o padrão, isso introduzia um gargalo pesado e os resultados dos benchmarks ficavam bem errados. Mas se colocarmos o modo de rede pra host, ou seja, sem virtualizar a rede, aí os tempos finalmente ficam corretos.</p>
<p>Não sei se notaram, mas na configuração do redis que mostrei antes, ou nesse de nginx, note que ambos já tem a opção network_mode igual a host, indicando que é pra pendurar o container nas portas da rede de verdade do sistema operacional nativo. Nesse modo não podemos pensar que temos &ldquo;servidores virtuais separados&rdquo; mas sim que estamos subindo realmente tudo na mesma máquina, na rede de verdade. Portanto, precisamos nos preocupar com conflitos de portas. Por isso no upstream do nginx, apontando pra dois servidores em localhost, cada um deles precisa estar numa porta diferente, 9501 e 9502.</p>
<p>É muito importante entender o que é modo de rede ponte e a diferença com modo de rede host. Estudem se não sabiam disso. Mas já que falamos do servidor de aplicação. Note que na configuração do nginx, tem esse seção de depends_on. Ele quer dizer: só suba o serviço nginx quando detectar que os serviços api-01 e api-02 estejam de pé. Então vamos pular pro começo do arquivo do docker compose, pra ver como essa aplicação está configurada.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>services:

    #api1
    api-01:
        container_name: rinha-backend-api-01
        build:
            context: .
            dockerfile: Dockerfile
        environment:
            HTTP_PORT: 9501
            APP_NAME: skeleton
            APP_ENV: dev
            DB_DRIVER: pgsql
            DB_HOST: localhost
            DB_PORT: 5432
            DB_DATABASE: rinha-backend-db
            DB_USERNAME: rinha
            DB_PASSWORD: postgres
            REDIS_HOST: localhost
            REDIS_PORT: 6379
        volumes:
            - ./:/app
        depends_on:
            - db
            - redis
        command: bash -c &#34;php bin/hyperf.php start&#34;
        network_mode: host
        deploy:
            resources:
                limits:
                    cpus: &#39;0.4&#39;
                    memory: &#39;0.3GB&#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>Precisamos dar um jeito de configurar a mesma aplicação duas vezes, como api-01 e depois como api-02 e, como estamos usando modo de rede host, precisamos declarar portas direrentes pra cada uma, que normalmente fazemos via variáveis de ambiente, como esse HTTP_PORT logo no começo. O jeito mais fácil é configurando esse bloco de configuração do serviço api-01 e depois fazendo um copy e paste embaixo pra ser o api-02, só mudando o HTTP_PORT pra 9502.</p>
<p>Existem outras formas de fazer isso. Como exemplo, que pegar o docker compose do projeto de Node.js do Lucas Poole, porque ele ainda está usando o modo de rede padrão, via ponte virtual, daí não precisou se preocupar em configurar portas diferentes, só precisou fazer hostnames diferentes. Vamos dar uma olhada.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>services:
  app1: &amp;app
    # image: lukas8219/rinha-be-2023-q3:latest
    build: .
    hostname: app1
    environment:
      DB_URL: &#34;postgres://postgres:12345678@postgres:5432/postgres&#34;
      DB_POOL: 35
      PINO_LOG_LEVEL: &#34;debug&#34;
      REQ_TIMEOUT: &#34;2000&#34;
      NODE_ENV: production
      CLUSTER: true
      CLUSTER_WORKERS: 5
    expose:
      - &#34;8080&#34;
    depends_on:
      - postgres
    ulimits:
      nproc: 1000000
      nofile:
        soft: 1000000
        hard: 1000000
    deploy:
      resources:
        limits:
          cpus: &#39;0.5&#39;
          memory: &#39;0.6GB&#39;
  app2:
    &lt;&lt;: *app
    hostname: app2</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>Ele declarou o serviço de nome &ldquo;app1&rdquo; e em seguida anexou uma etiqueta com esse &amp; comercial, chamando de &ldquo;app&rdquo;. Descendo pro próximo serviço no mesmo arquivo, observem, temos app2. E na linha seguinte é como se fosse um &ldquo;include&rdquo; ou &ldquo;import&rdquo;, dizendo pra copiar tudo que tinha no app1. A única coisa que muda é o nome do hostname. Entenderam? É assim que podemos reusar as mesmas configurações entre diferentes serviços. Isso é uma características de arquivos de configuração em formato YAML, não é coisa do docker compose. Qualquer outro programa que use YAML, dá pra fazer a mesma coisa.</p>
<p>Como falei antes, o projeto do Lucas não está usando o modo de rede host e sim o de ponte, por isso na configuração do app2, ele mantém o mesmo número de porta. Se fosse no modo host, precisaria declarar pelo menos um número de porta diferente, pra não conflitar no localhost. Se nunca viu isso antes, sei que pode ser confuso, mas se lembre dessa diferença entre ponte de rede virtual e rede host nativa.</p>
<p>A parte mais importante na configuração da nossa aplicação de API é este parâmetro &ldquo;build&rdquo;. Aqui está mais completo do que precisa. Bastaria ter escrito o valor &ldquo;.&rdquo;, mas o Lucas preferiu declarar o contexto como &ldquo;ponto&rdquo; e o arquivo como sendo &ldquo;Dockerfile&rdquo; mesmo, que é o padrão.</p>
<p>É aqui que ele vai usar o arquivo de Dockerfile que já vimos antes, pra construir a imagem pra este container. Todos os outros serviços, nós apontamos pra imagens já prontas no serviço do site Docker Hub. Aquelas imagens foram todas feitas com algum Dockerfile também. A diferença é que nossa aplicação não foi publicada no Docker Hub, estamos construindo ela aqui, então apontamos direto pro Dockerfile. No futuro, quando a aplicação estiver terminada, podemos dar upload da última imagem gerada e passar a apontar pra ela via parâmetro image em vez de build. No arquivo do Lucas, tem até comentado essa opção, porque pra participar da rinha, precisava ter publicado a imagem de verdade.</p>
<p>O Dockerfile do Lauro, que mostrei antes, usa de base a imagem do repositório do Hyperf e também incluímos coisas como o PDO de Postgres. Todos esses elementos podem ser configurados via variáveis de ambiente, que é o que estamos declarando na seção &ldquo;environment&rdquo;, como o HTTP_PORT, que indica em qual porta o servidor de Hyperf vai subir, ou DB_HOST ou REDIS_HOST que declara o nome do servidor que vai ter o banco de dados e o cache. Neste caso, em modo de rede host, vai ser tudo localhost.</p>
<p>No projeto do Lauro, quando fizemos as otimizações de performance que expliquei no outro episódio, ele fez alguns ajustes depois e preferiu não deixar tudo 100% automatizado. Lembram que falei que no Dockerfile dele estava faltando o comando ENTRYPOINT, que define que executável iniciar no container? Pra saber o que fazer, tem que ler o README dele. Ao subir o Docker Compose, os containers vão estar de pé, mas internamente quem vai rodar é este comando de &ldquo;sleep infinity&rdquo;, que é um truque só pro container ficar de pé e não morrer.</p>
<p>A razão disso foi porque ele não adicionou os próximos passos no Dockerfile, não sei por que, mas o README pede pra rodar manualmente, via &ldquo;docker exec&rdquo; o comando de &ldquo;composer install&rdquo; pra instalar as dependências. Como ele mapeia o diretório inteiro como &ldquo;/app&rdquo; pra dentro do container, se não me engano, &ldquo;composer install&rdquo; vai baixar e gravar as dependências no sub-diretório &ldquo;/app/vendor&rdquo;. Por isso tanto faz rodar em qualquer um dos dois containers, o outro vai enxergar automaticamente, já que compartilham do mesmo volume no disco.</p>
<p>Só depois de fazer isso que, via docker exec de novo, podemos rodar o hyperf em cada container, e vai subir direitinho. Durante desenvolvimento, dependências como bibliotecas de node, ou neste caso, do composer, não queremos deixar dentro da imagem, porque ocupam muito espaço e mudam o tempo todo. Seria trabalhoso toda vez reconstruir a imagem. O melhor é mapear um volume pra dentro do container e mandar instalar nesse volume. Só quando for gerar a imagem final pra publicar, aí sim, era bom mudar pra gravar as dependências dentro da imagem. Senão quem for baixar depois, vai precisar fazer isso manualmente, o que não é ideal.</p>
<p>Feito tudo isso, do terminal, basta subir tudo com &ldquo;docker compose up&rdquo;. Isso vai baixar as imagens que ainda não tem, construir essa da aplicação e subir tudo certinho, na ordem certa, graças aos depends_on que todos os serviços declaram. Olhem só, da primeira vez demora um pouquinho pra subir. Vou acelerar aqui um pouquinho e &hellip; pronto. Como expliquei antes, só pra esse projeto do Lauro, temos que rodar o composer install manualmente, aceleramos mais um pouco e &hellip; pronto. Finalmente, podemos subir o Hyperf em cada um dos containers.</p>
<p>Pra testar que tudo deu certo, podemos consultar o endpoint da API de contagem de pessoas, que é usado pelo script de benchmark pra mostrar quantas pessoas foram inseridas no teste de carga. Basta chamar do próprio terminal com &ldquo;curl http://localhost:9999/contagem-pessoas&rdquo;. E.. deu pau. O que aconteceu?</p>
<p>Se estiver rodando em alguma distro Linux, ia retornar bonitinho. Mas se estiver rodando em Mac ou Windows, como neste meu caso, via WSL 2, aquela configuração de network mode host não vai funcionar. Por que?</p>
<p>Se não entendem como um container funciona por baixo, não deixe de assistir o episódio sobre containers. Em resumo, Docker precisa da kernel Linux pra funcionar. Docker não é uma máquina virtual, é uma infraestrutura que pede pra kernel mentir pros processos, fazendo eles acreditarem que estão rodando sozinhos na máquina.</p>
<p>No caso de Mac, o problema é que Macs não rodam kernel de Linux. Apesar de rodarem programas que parecem de Linux, na realidade são todos recompilados pra serem compatíveis com a versão Darwin do BSD UNIX. Macs são UNIX e não Linux. Eu já contei essa história no episódio sobre as diferenças de Mac, Windows e Linux. Programas de Linux não rodam nativamente em Macs, todos precisam ser no mínimo recompilados. Mac e Linux são binariamente incompatíveis.</p>
<p>Windows, obviamente, também não é uma distro Linux. A kernel do Windows é fundamentalmente diferente de Linux e Macs. Mas hoje em dia existe o WSL 2, que é basicamente uma máquina virtual Linux super leve, rodando por cima do Windows usando a infraestrutura de hypervisor do Hyper-V. Essa é a única solução pra Macs e Windows: rodar Linux numa máquina virtual e, dentro dela, rodar Docker.</p>
<p>Tanto no caso de Mac, quanto Windows, uma das opções mais populares é baixar o pacote Docker Desktop, que vai instalar a máquina virtual pro Docker e uma interface gráfica, que é opcional, pra facilitar gerenciar os diversos elementos de Docker, como redes virtuais, imagens e containers. E aqui está o problema de porque não funciona localhost:9999 em modo de rede host. Segundo a própria documentação: o driver de rede modo host só funciona em Linux, rodando na VM do Docker Desktop, o container não vai receber nenhum endereço IP e a publicação de portas vai ser ignorada, tornando o container inacessível.</p>
<p>Quer dizer que Docker Desktop de Windows e Mac são uma droga, então? Recapitulando, porque estávamos usando modo de rede host? Porque no cenário de teste de carga, o modo padrão gera gargalo, então mudamos pro modo host. Isso é só pra situação de teste de carga. Pro dia a dia, enquanto você está programando, em situações normais, não vai sentir diferença nenhuma. Nesse caso, não precisa usar esse modo host e deixar tudo no padrão. No modo ponte virtual, Docker de Mac e Windows vai funcionar direitinho.</p>
<p>No caso do WSL 2, você pode instalar o Docker ou Podman dentro do WSL em vez de usar uma VM separada, como o Docker Desktop faz.</p>
<p>Falando de Docker Desktop, uma pequena tangente, se você quiser muito uma interface gráfica pra gerenciar imagens, containers e tudo mais, experimente outra opção: o Portainer, que por si só é uma aplicação que roda em container Docker, então é trivial de instalar, e te dá uma interface web de administração muito mais avançada do que Docker Desktop. Mas também é legal aprender a ficar na linha de comando e experimentar coisas como Lazy Docker. Dá pra fazer tudo só com o comando &ldquo;docker&rdquo;, mas às vezes não lembramos como fazer coisas que não se usa todo dia, tipo, &ldquo;como era mesmo, pra apagar imagens que não estão sendo usadas?&rdquo;, daí tanto Portainer como Lazy Docker podem ajudar. Dêem uma fuçada.</p>
<p>Na verdade, é muito mais fácil usar o NAT de portas, que é a opção padrão. Vamos pegar o arquivo de nginx do projeto do Lucas, que não optou pelo modo de rede host. Olha a declaração de servidores de upstream. Em vez de estar localhost com duas portas diferentes, tempos aqui dois hostnames diferentes usando as mesmas portas. É como se  estivéssemos declarando dois servidores separados, cada um com uma cópia da aplicação na porta 8080.</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-c" data-lang="c"><span class="line"><span class="cl">    <span class="n">upstream</span> <span class="n">api</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="n">server</span> <span class="nl">app1</span><span class="p">:</span><span class="mi">8080</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="n">server</span> <span class="nl">app2</span><span class="p">:</span><span class="mi">8080</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="n">keepalive</span> <span class="mi">500</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>No docker compose, podemos declarar o app1, expor a porta 8080, etiquetar com &amp; comercial &ldquo;app&rdquo; e lá embaixo declarar o app2 usando a mesma configuração, só sobrescrevendo o hostname. Todo o resto fica igual. Não precisamos nos preocupar em conflito de portas, porque é como se fossem servidores em máquinas diferentes. Essa é a vantagem do NAT de portas e por isso ele é o modo padrão. É muito mais intuitivo. Se não ficou claro isso, assista de novo meu episódio sobre containers e a série de redes.</p>
<p>A versão em PHP do Lauro é uma das mais complicadinhas pra subir, por causa daqueles passos extras no README, que ele preferiu não automatizar. Pra prosseguir a explicação, vamos usar o projeto de Node do Lucas daqui pra frente. O dele é mais simples, basta executar &ldquo;docker compose up&rdquo;, esperar tudo baixar e pronto. Mesmo em Docker de Windows ou Mac não deve ter problemas porque a versão dele usa NAT de portas padrão, então tudo deveria funcionar plug and play, que é o ideal de um docker compose.</p>
<p>Antes que eu me esqueça, se não está acostumado a usar docker compose, sempre se lembre que se não iniciar o comando &ldquo;up&rdquo; com a opção &ldquo;-d&rdquo; de daemon, ele vai ficar prendendo seu terminal. Com control-c dá pra derrubar todo mundo. E garanta que os containers desligaram executando &ldquo;docker compose down&rdquo;. Se modificar seu código e subir depois com &ldquo;up&rdquo; mas sentir que não apareceram suas modificações, dá pra forçar o build da imagem da aplicação com &ldquo;docker compose up &ndash;build&rdquo; ou &ldquo;docker compose up &ndash;force-build&rdquo;.</p>
<p>De outro terminal, dá pra ver os containers que estão de pé fazendo &ldquo;docker ps&rdquo;. Olha só como aparece bonitinho, daí dá pra abrir o shell em qualquer um deles usando &ldquo;docker exec -it&rdquo;. Enquanto estiver trabalhando num projeto, pode só ficar derrubando com o comando &ldquo;down&rdquo; e subindo com o comando &ldquo;up&rdquo;. Mas quando terminar o projeto e não for mais mexer nele, vale derrubar uma última vez com &ldquo;docker compose down &ndash;rmi all&rdquo;. Essa opção vai apagar os containers e as imagens. Imagens de docker usam muito espaço em disco e se ficar esquecendo de limpar de projetos que não vai mais usar, vai estar desperdiçando espaço. Depois pesquise sobre como limpar containers e imagens que não está mais usando.</p>
<p>Voltando, chegou a hora de entender um pouco esse negócio de teste de carga. Entenda, foda-se que linguagem você escolheu, que framework usa, as toneladas de javascript que enfiou no front-end. Svelte, Tailwind, nada disso importa. A única coisa que interessa é que entre seu navegador favorito e sua aplicação, trafegam mensagens em formato texto, obedecendo o protocolo HTTP. Mensagens divididas em cabeçalhos e corpo de texto.</p>
<p>O teste que sugeri antes, curl http://localhost:9999/contagem-pessoas é um exemplo disso. O Curl ou Wget são navegadores de linha de comando. Navegadores simples, que não tem front-end e por isso não precisam interpretar nem html, nem css, nem javascript. Eles só listam o que recebem. Fiquei pensando se perdia tempo explicando curl, como mandar e receber mensagens HTTP. Mas isso seria uma tangente desnecessária. Todo desenvolvedor web, tem que saber como enviar e receber mensagens HTTP manualmente, seja via curl na linha de comando, seja via web developer tools em qualquer navegador. Sem saber isso, sinto muito, você nem começou a entender web ainda. Volte vários passos, estude mais, só depois retorne.</p>
<p>Estou sendo bem cuzão quanto a isso porque é sério: HTTP é simples, os cabeçalhos principais dá pra saber de cabeça, os encodings de mensagem, como URL encode, são simples. E se não entender como funciona essa troca de mensagens, tudo daqui pra frente não vai fazer nenhum sentido. Sem saber isso você não entende nem o que é uma API. Sem saber isso não tem nem como começar a falar de problemas de segurança triviais como request forgery, nem otimizações de performance como conditional get requests. 80% da parte mais interessante e mais importante, você nunca vai ter acesso. Então pare aqui e vá estudar antes.</p>
<p>Isso tudo dito, vamos pular direto pro Gatling, que é a ferramenta de stress test, ou teste de carga, que a rinha escolheu. Eles até deram uma pequena patrocinada, acho que com alguns prêmios pros ganhadores. É uma ferramenta que aceita scripts feitos em Scala ou Kotlin. Primeiro de tudo é ter Java instalado na sua máquina. Cuidado, saiu não faz muito tempo o Java 21, e dependendo da sua distro, com um Arch, que instala tudo que tem de novo, vai ter problemas. Escolha manualmente o OpenJDK 17 ou versões mais antigas como versão 11.</p>
<p>Como eu sei que estou com o java correto? Porra, abre o terminal e digita &lsquo;java -version&rsquo;. Se tiver mais de uma jdk instalada, no caso de um Arch, tem o script &lsquo;archlinux-java set java-17-openjdk&rsquo; pra forçar usar a versão 17, por exemplo. Consulte a documentação do pacote de JDK da sua distro. Se for Windows ou Mac, baixe direto o instalador da versão 17 e já era.</p>
<p>Depois disso precisa baixar o Gatling propriamente dito. Só ir no site e baixar o zip. Depois deszipe onde quiser. Eu pessoalmente prefiro deszipar no diretório &ldquo;/opt/gatling&rdquo; mas você pode deszipar na sua home. Enfim, o importante é entrar no repositório da rinha, no diretório chamado &ldquo;stress-test&rdquo;. Lá vai achar o script principal, &ldquo;run-test.sh&rdquo;. Tem versão de Powershell também, mas honestamente, não perdi meu tempo com isso.</p>
<p>Edite esse script e garanta que a variável GATLING_BIN_DIR está apontando pro diretório bin que veio dentro do zip que baixou. Olhe a linha de baixo, é como ele executa o gatling, passando o script da simulação que é esse RinhaBackendSimulation, que já vamos ver. Em seguida declara os diretórios principais de trabalho. Fica tudo dentro de &ldquo;stress-test/user-files&rdquo;. Em resources temos planilhas com dados de teste. Vamos dar uma olhada.</p>
<p>O arquivo &ldquo;pessoas-payloads.tsv&rdquo; tem os dados de pessoas falsas que ele vai enviar pra API de cadastrar pessoas. Como podem ver, cada linha é um JSON, e os dados são super aleatórios mesmo.  Você acha que todo mundo envia dados bonitinhos pra sua aplicação? Pelo contrário, com o tanto de bots que tem por aí, sua aplicação está recebendo dados parecidos com esse o tempo todo. Nunca assuma que sua aplicação vai ser usada só por pessoas inteligentes que sabem ler e escrever direito.</p>
<p>Pelo contrário, assuma que a maior parte do uso vai vir de bots zoados ou pessoas zoadas que male male sabem escrever o próprio nome e vão preencher tudo errado. Sua aplicação precisa estar preparada para os piores inputs, sempre. Esse é um bom exemplo de ataque pra ver se coisas inválidas acabam conseguindo burlar suas validações, ou o mais comum, você esqueceu de implementar algumas validações.</p>
<p>O segundo arquivo é o &ldquo;termos-busca.tsv&rdquo; que vai ser usado na API de procura por termos. É outra lista de bobagens aleatórias. Mesma coisa, é pra estressar suas APIs e ver como se comportam. Precisa ter validações conforme explicado nas instruções da rinha. Pra um projeto de verdade, é bom ter teste de carga com dados completamente aleatórios como esses, mas também é bom ter dados um pouco mais próximos do que pessoas medianas mandariam, pra ficar mais fácil de analisar depois. Criar dados de teste é, em si, uma arte também.</p>
<p>Testes com inputs aleatórios como esse são chamados de Fuzz Test. Fuzz é tipo barulho, bagunça, que é um jeito de dizer aleatório. Se você só fizer testes com dados que parecem de verdade, está perdendo a oportunidade de ver os limites das validações.</p>
<p>Finalmente, temos o script do teste de carga propriamente dito, neste arquivo RinhaBackendSimulation.scala. Mesmo se você nunca escreveu Scala antes, pra fazer scripts simples com esse não é difícil. Qualquer bom programador deve ser capaz de no mínimo ler e entender sem grandes dificuldades. Vamos lá, por partes.</p>
<p>Gatling tem o conceito de Simulation e nossa classe herda dela. Em seguida começamos definindo pra onde mandar nossa carga. No caso a URL base é localhost porta 9999, onde configuramos o NGINX no Docker Compose, lembram? Como nossa aplicação não tem tratamento pra nenhum cabeçalho, só isso já basta. Só de brincadeira, colocaram um user agent fake aqui.</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-scala" data-lang="scala"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">RinhaBackendSimulation</span>
</span></span><span class="line"><span class="cl">  <span class="k">extends</span> <span class="nc">Simulation</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">val</span> <span class="n">httpProtocol</span> <span class="k">=</span> <span class="n">http</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">baseUrl</span><span class="o">(</span><span class="s">&#34;http://localhost:9999&#34;</span><span class="o">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">userAgentHeader</span><span class="o">(</span><span class="s">&#34;Agente do Caos - 2023&#34;</span><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>Seguimos declarando uma variável &ldquo;criacao E consulta pessoas&rdquo; que é um &ldquo;scenario&rdquo;. Imagino que essa função &ldquo;scenario&rdquo; tenha na classe abstrata Simulation que estou estendendo. Ela devolve um objeto onde podemos sair encadeando várias chamadas de configuração. A API de configuração é bem legível. Olha só: no cenário criação e talvez consulta de pessoas, alimente com o arquivo formato tsv &ldquo;pessoas-payloads.tsv&rdquo; e considere  sendo circular, ou seja, se chegarmos no fim do arquivo, recomece da primeira linha de novo. Pra cada linha execute o bloco a seguir.</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-scala" data-lang="scala"><span class="line"><span class="cl">  <span class="k">val</span> <span class="n">criacaoEConsultaPessoas</span> <span class="k">=</span> <span class="n">scenario</span><span class="o">(</span><span class="s">&#34;Criação E Talvez Consulta de Pessoas&#34;</span><span class="o">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">feed</span><span class="o">(</span><span class="n">tsv</span><span class="o">(</span><span class="s">&#34;pessoas-payloads.tsv&#34;</span><span class="o">).</span><span class="n">circular</span><span class="o">())</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">exec</span><span class="o">(</span>
</span></span><span class="line"><span class="cl">		<span class="o">....</span> <span class="c1">// bloco
</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>Nesse bloco começamos chamando outro método da simulação, &ldquo;http&rdquo;, que imagino que controla uma chamada http. Configuramos com uma chamada POST pra endpoint &ldquo;/pessoas&rdquo;, onde o corpo vai ser o payload, que é uma linha do arquivo de carga que estamos lendo. Como único cabeçalho configuramos &ldquo;Content-type&rdquo; pra ser o mime-type &ldquo;application/json&rdquo;. Todo framework que se preza, checa content type.</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-scala" data-lang="scala"><span class="line"><span class="cl"><span class="o">.</span><span class="n">exec</span><span class="o">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">http</span><span class="o">(</span><span class="s">&#34;criação&#34;</span><span class="o">)</span>
</span></span><span class="line"><span class="cl">  <span class="o">.</span><span class="n">post</span><span class="o">(</span><span class="s">&#34;/pessoas&#34;</span><span class="o">).</span><span class="n">body</span><span class="o">(</span><span class="nc">StringBody</span><span class="o">(</span><span class="s">&#34;#{payload}&#34;</span><span class="o">))</span>
</span></span><span class="line"><span class="cl">  <span class="o">.</span><span class="n">header</span><span class="o">(</span><span class="s">&#34;content-type&#34;</span><span class="o">,</span> <span class="s">&#34;application/json&#34;</span><span class="o">)</span>
</span></span><span class="line"><span class="cl">  <span class="c1">// 201 pros casos de sucesso :)
</span></span></span><span class="line"><span class="cl">  <span class="c1">// 422 pra requests inválidos :|
</span></span></span><span class="line"><span class="cl">  <span class="c1">// 400 pra requests bosta tipo data errada, tipos errados, etc. :(
</span></span></span><span class="line"><span class="cl">  <span class="o">.</span><span class="n">check</span><span class="o">(</span><span class="n">status</span><span class="o">.</span><span class="n">in</span><span class="o">(</span><span class="mi">201</span><span class="o">,</span> <span class="mi">422</span><span class="o">,</span> <span class="mi">400</span><span class="o">))</span>
</span></span><span class="line"><span class="cl">  <span class="c1">// Se a criacao foi na api1 e esse location request atingir api2, a api2 tem que encontrar o registro.
</span></span></span><span class="line"><span class="cl">  <span class="c1">// Pode ser que o request atinga a mesma instancia, mas estatisticamente, pelo menos um request vai atingir a outra.
</span></span></span><span class="line"><span class="cl">  <span class="c1">// Isso garante o teste de consistencia de dados
</span></span></span><span class="line"><span class="cl">  <span class="o">.</span><span class="n">check</span><span class="o">(</span><span class="n">status</span><span class="o">.</span><span class="n">saveAs</span><span class="o">(</span><span class="s">&#34;httpStatus&#34;</span><span class="o">))</span>
</span></span><span class="line"><span class="cl">  <span class="o">.</span><span class="n">checkIf</span><span class="o">(</span><span class="n">session</span> <span class="k">=&gt;</span> <span class="n">session</span><span class="o">(</span><span class="s">&#34;httpStatus&#34;</span><span class="o">).</span><span class="n">as</span><span class="o">[</span><span class="kt">String</span><span class="o">]</span> <span class="o">==</span> <span class="s">&#34;201&#34;</span><span class="o">)</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">	<span class="n">header</span><span class="o">(</span><span class="s">&#34;Location&#34;</span><span class="o">).</span><span class="n">saveAs</span><span class="o">(</span><span class="s">&#34;location&#34;</span><span class="o">)</span>
</span></span><span class="line"><span class="cl">  <span class="o">}</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>Essa função &ldquo;check&rdquo; não tenho muita certeza do que faz e não tive paciência pra ir ler na documentação, mas não me parece importante pra agora. Deve ser algo como, &ldquo;considere que está certo se o resultado da requisição voltar com código de status 201, 422 ou 400&rdquo;. Se voltar um crash com erro 500, ou página não encontrada que é 404, ou outra coisa, considere como um erro e reporte em vermelho.</p>
<p>Depois temos um novo check, pedindo pra salvar alguma coisa chamada &ldquo;http status&rdquo;. Podemos ver que vai ser usado na chamada checkIf abaixo. Essa linha parece dizer o seguinte: execute o bloco seguinte se o código http de status que gravamos na linha anterior for código 201, que é o código http correto quando um recurso é criado no servidor. Se for esse o caso, o bloco seguinte deve salvar o valor que voltar no cabeçalho de Location, dentro dessa sessão chamada &ldquo;location&rdquo;. Lembre-se disso em design de APIs: sempre que um recurso é criado no servidor, retornando código 201, também deve retornar um cabeçalho chamado Location com a URL completa para este novo recurso. É isso que estamos guardando.</p>
<p>Saindo desse bloco, o cenário dá uma pausa de 1 a 30 milissegundos. Em seguida, caso uma sessão de location tenha sido gravado, ele pede pra executar uma nova requisição http para esta location. Se for encontrado, o servidor deveria retornar código http 200 e o json do registro. Se for retorno 200 a simulação deve considerar como verde OK. Se voltar qualquer outro código, deve considerar vermelho knock out.</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-c" data-lang="c"><span class="line"><span class="cl">    <span class="p">.</span><span class="nf">pause</span><span class="p">(</span><span class="mf">1.</span><span class="n">milliseconds</span><span class="p">,</span> <span class="mf">30.</span><span class="n">milliseconds</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">.</span><span class="nf">doIf</span><span class="p">(</span><span class="n">session</span> <span class="o">=&gt;</span> <span class="n">session</span><span class="p">.</span><span class="nf">contains</span><span class="p">(</span><span class="s">&#34;location&#34;</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nf">exec</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="nf">http</span><span class="p">(</span><span class="s">&#34;consulta&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="s">&#34;#{location}&#34;</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>Como o comentário explica, o Zanfrancheschi, que desenhou o desafio da rinha, fez a coisa certa. Pro desafio ser minimamente interessante, só criar uma API que grava uma nova pessoa no banco, seria muito simples. Qualquer um que já tenha terminado pelo menos um tutorial de curso deveria conseguir. Mas, subir duas instâncias da mesma aplicação e colocar embaixo de um proxy reverso cria uma situação um pouco mais interessante.</p>
<p>Digamos que você tenha feito otimização prematura. Vamos tentar ser mais espertos. Ao receber o JSON via POST em /pessoas. Guardamos em memória, sem mandar insert pro banco, pra gravar assincronamente mais tarde, e assim podemos responder imediatamente código http 201. Como geramos um UUID aleatório antes de mandar pro banco, dá pra gerar a URL de Location. Seria algo como &ldquo;http://localhost:9999/pessoas/:uuid&rdquo;</p>
<p>O script do Gatling dá uma pausa de 1 a 30 milissegundos e manda puxar essa URL que voltou no cabeçalho de Location. Se antes desses 30 milissegundos, nossa aplicação não conseguir realmente gravar no banco, tem chance da consulta cair na segunda instância. Se o registro não teve tempo de ser gravado, não vai ser encontrado e vamos receber 404 not found. A simulação iria registrar um erro. Então, só segurar o registro em memória pra responder mais rápido iria quebrar essa condição de pesquisa do teste, tem que ser mais esperto que isso.</p>
<p>A expectativa da rinha, com os limites de CPU e RAM impostos, o requerimento de colocar duas ou mais instâncias, e impor uma carga pesada, enganaria programadores mais calejados a caírem no erro de tentar otimização prematura, colocando coisas como caches ou filas, como expliquei no episódio passado. Se fizer do jeito mais ingênuo que é primeiro escrever no banco e depois devolver código 201 com a URL de Location, vai tudo funcionar corretamente. Mas será que aí o tempo de espera de gravar no banco não vai fazer o NGINX dar timeout em muitas requisições?</p>
<p>E não, foi isso que descobrimos na exploração do episódio de 16 linguagens: todos nós subestimamos o banco de dados Postgres. Acontece que pra casos simples como esse, de inserts simples, ele é tão rápido quanto um servidor de cache como Redis. Qualquer otimização extra não faz diferença. A otimização de usar modo de rede host em vez de NAT de portas virtuais fez mais diferença do que tentar otimizar o Postgres com alguma solução de cache. Já vou voltar nisso, vamos só terminar de ler o arquivo de simulação.</p>
<p>O cenário de criação de pessoas, seguido da consulta dessa pessoa recém-criada, é o cenário mais comprido. Nem é difícil, só comprido. Os outros dois cenários são mais curtos. No &ldquo;busca pessoas&rdquo;, o cenário se alimenta do arquivo tsv &ldquo;termos-busca.tsv&rdquo;, novamente em modo circular, e pra cada linha do arquivo, faz uma requisição GET no endpoint &ldquo;/pessoas&rdquo; passando a variável &ldquo;t=&rdquo;, concatenando o parâmetro de busca.</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-c" data-lang="c"><span class="line"><span class="cl">  <span class="n">val</span> <span class="n">buscaPessoas</span> <span class="o">=</span> <span class="nf">scenario</span><span class="p">(</span><span class="s">&#34;Busca Válida de Pessoas&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">.</span><span class="nf">feed</span><span class="p">(</span><span class="nf">tsv</span><span class="p">(</span><span class="s">&#34;termos-busca.tsv&#34;</span><span class="p">).</span><span class="nf">circular</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">    <span class="p">.</span><span class="nf">exec</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">      <span class="nf">http</span><span class="p">(</span><span class="s">&#34;busca válida&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="s">&#34;/pessoas?t=#{t}&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="c1">// qq resposta na faixa 2XX tá safe
</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>Acho que se não configurar explicitamente, o cenário considera que respostas 200 são OK e qualquer coisa diferente é erro. Por fim, temos o cenário de busca inválida de pessoas, que é o mesmo endpoint, só não passando nenhum termo de procura. Segundo a instrução da rinha, isso deveria retornar código de erro 400, que é o que o script vai checar mesmo. Se devolver 400, marca como verde, OK.</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-c" data-lang="c"><span class="line"><span class="cl">  <span class="n">val</span> <span class="n">buscaInvalidaPessoas</span> <span class="o">=</span> <span class="nf">scenario</span><span class="p">(</span><span class="s">&#34;Busca Inválida de Pessoas&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">.</span><span class="nf">exec</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">      <span class="nf">http</span><span class="p">(</span><span class="s">&#34;busca inválida&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="s">&#34;/pessoas&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="c1">// 400 - bad request se não passar &#39;t&#39; como query string
</span></span></span><span class="line"><span class="cl">      <span class="p">.</span><span class="nf">check</span><span class="p">(</span><span class="n">status</span><span class="p">.</span><span class="nf">is</span><span class="p">(</span><span class="mi">400</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>Com os 3 cenários declarados, agora o script define um bloco que chama de &ldquo;setup&rdquo;. Literalmente, configurar como os cenários devem rodar. Em todos os casos, começa dizendo &ldquo;comece com 2 usuários constantes por segundo, durante 10 segundos&rdquo;, na sequência fala &ldquo;prossiga agora com 5 usuários por segundo, durante 15 segundos&rdquo;. É uma carga leve, só de warm up, ou aquecimento mesmo.</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-c" data-lang="c"><span class="line"><span class="cl"><span class="nf">setUp</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">	<span class="n">criacaoEConsultaPessoas</span><span class="p">.</span><span class="nf">inject</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">	  <span class="nf">constantUsersPerSec</span><span class="p">(</span><span class="mi">2</span><span class="p">).</span><span class="nf">during</span><span class="p">(</span><span class="mf">10.</span><span class="n">seconds</span><span class="p">),</span> <span class="c1">// warm up
</span></span></span><span class="line"><span class="cl">	  <span class="nf">constantUsersPerSec</span><span class="p">(</span><span class="mi">5</span><span class="p">).</span><span class="nf">during</span><span class="p">(</span><span class="mf">15.</span><span class="n">seconds</span><span class="p">),</span> <span class="c1">// are you ready?
</span></span></span><span class="line"><span class="cl">	
</span></span><span class="line"><span class="cl">	  <span class="nf">rampUsersPerSec</span><span class="p">(</span><span class="mi">6</span><span class="p">).</span><span class="nf">to</span><span class="p">(</span><span class="mi">600</span><span class="p">).</span><span class="nf">during</span><span class="p">(</span><span class="mf">3.</span><span class="n">minutes</span><span class="p">)</span> <span class="c1">// lezzz go!!!
</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><span class="line"><span class="cl"><span class="p">).</span><span class="nf">protocols</span><span class="p">(</span><span class="n">httpProtocol</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>Em seguida começa a carga de verdade, olha só &ldquo;ramp&rdquo;, que quer dizer ir aumentando, como numa rampa mesmo. Vá aumentando, começando com 6 usuários por segundo, até atingir 600 usuários por segundo, acelerando constantemente durante 3 minutos, mantendo um crescimento linear. Agora sim, isso vai gerar uma carga pesada até o final. Entenderam?</p>
<p>Mas não é só isso, em paralelo vai rodar também os cenários de busca e busca inválida. Ambos fazem um aquecimento pequeno pelos mesmos 25 segundos. Daí já começa, ramp de 6 usuários por segundo, até atingir 100 usuários fazendo buscas válidas por segundo, durante os mesmos 3 minutos. E por fim, ramp de 6 usuários por segundo até 40 fazendo buscas inválidas por segundo, durante 3 minutos.</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-c" data-lang="c"><span class="line"><span class="cl">	<span class="n">buscaPessoas</span><span class="p">.</span><span class="nf">inject</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">	  <span class="nf">constantUsersPerSec</span><span class="p">(</span><span class="mi">2</span><span class="p">).</span><span class="nf">during</span><span class="p">(</span><span class="mf">25.</span><span class="n">seconds</span><span class="p">),</span> <span class="c1">// warm up
</span></span></span><span class="line"><span class="cl">	
</span></span><span class="line"><span class="cl">	  <span class="nf">rampUsersPerSec</span><span class="p">(</span><span class="mi">6</span><span class="p">).</span><span class="nf">to</span><span class="p">(</span><span class="mi">100</span><span class="p">).</span><span class="nf">during</span><span class="p">(</span><span class="mf">3.</span><span class="n">minutes</span><span class="p">)</span> <span class="c1">// lezzz go!!!
</span></span></span><span class="line"><span class="cl">	<span class="p">),</span>
</span></span><span class="line"><span class="cl">	<span class="n">buscaInvalidaPessoas</span><span class="p">.</span><span class="nf">inject</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">	  <span class="nf">constantUsersPerSec</span><span class="p">(</span><span class="mi">2</span><span class="p">).</span><span class="nf">during</span><span class="p">(</span><span class="mf">25.</span><span class="n">seconds</span><span class="p">),</span> <span class="c1">// warm up
</span></span></span><span class="line"><span class="cl">	
</span></span><span class="line"><span class="cl">	  <span class="nf">rampUsersPerSec</span><span class="p">(</span><span class="mi">6</span><span class="p">).</span><span class="nf">to</span><span class="p">(</span><span class="mi">40</span><span class="p">).</span><span class="nf">during</span><span class="p">(</span><span class="mf">3.</span><span class="n">minutes</span><span class="p">)</span> <span class="c1">// lezzz go!!!
</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>Em cada cenário, só configuramos que busca é um get pra &ldquo;/pessoas&rdquo; ou criação é um post pra &ldquo;/pessoas&rdquo;, mas como ele sabe pra onde mandar essas requisições? Pra isso serve aquela configuração que fizemos lá no começo do script, declarando que a URL base é &ldquo;localhost&rdquo; porta 9999 lembram? No final do setup, plugamos essa configuração. E pronto, é isso! Vamos ver rodando.</p>
<p>Pra ficar claro, tenho um terminal dividido em dois, usando split horizontal do TMUX. Na metade de cima, estamos no projeto do Lucas Poole em Node. Podemos chamar &ldquo;docker compose up&rdquo; e esperar os serviços todos subirem. Na metade de baixo, estou no diretório de &ldquo;stress-test&rdquo; do repositório da rinha, onde tem o script &ldquo;run-test.sh&rdquo;, que vamos rodar agora. Pronto, passa aqueles 25 segundos de aquecimento e aí começa a brincadeira. Agora é esperar os 3 minutos dos cenários.</p>
<p>Lembram que o script configura 3 cenários? Estão aqui: Busca Inválida, Busca Válida e Criação. Em cima tem um resumo, embaixo tem um pouco mais detalhado. O principal: OK são requisições que devolveram códigos http válidos de resposta, como 200, 400 ou 422. Tudo que devolveu erro, código 500, timeout, acho que ele considera como KO ou Knock Out. São as requisições perdidas.</p>
<p>Voltando, depois de algum tempo rodando, quando a carga apertar mais pro fim, em vários dos projetos participantes, vamos ver este erro sendo reportado: java.io.IOException: Premature Close. Esse é o erro que o Vinicius Ferraz descobriu como consertar mudando de network_mode bridge pra host. Olha só o que essa mensagem está dizendo: mais de 9 mil requisições perdidos nesse exceção. 9 mil, num total de pouco mais de 100 mil requisições, é um número considerável, quase 10% de erro.</p>
<p>Vamos abrir o gráfico que o Gatling gera. No mundo ideal gostaríamos de ver somente essa barra verde, sem nada vermelho do lado. Mas temos vermelho. Olhando aqui na tabela veja só, de um total de 108 mil requisições, tivemos quase 99 mil que deram OK, mas quase 10 mil em vermelho, que deram knock out. Já volto nessa tabela, vamos olhar mais pra baixo os gráficos.</p>
<p>Lembram como configuramos pra iniciar os três cenários em paralelo, pra ir subindo de 6 usuários pra mais de 600 simultâneos por segundo? Um ramp up? Por isso o formato do gráfico é um triângulo. Começa com 6 usuários por segundo e vai subindo, ramping up, com aceleração constante, aumentando a velocidade linearmente, até mais de 600 usuários. Por mais de 2 minutos, mais de dois terços do tempo, cresce sem problemas. Mas aí olha essa parte final.</p>
<p>O final é cheio de &ldquo;sobes e desces&rdquo;, esse serrilhado. Aqui a aplicação tá claramente engasgando. Muitas requisições em fila não conseguem ser atendidos, ou demoram demais pra responder, ou dão timeout de tanto esperar. Por isso, no último gráfico, começamos a ter várias áreas vermelhas. Lembrando que muitos desses vermelhos são os IOException de Java que mencionei antes.</p>
<p>Mas será que realmente faz tanta diferença assim? Pra demonstrar, podemos modificar o código do Lucas pra usar network mode host. Começamos mudando o &ldquo;index.js&rdquo; que carrega o node.js na porta 8080, hardcoded, e trocar por uma variável de ambiente chamada &ldquo;HTTP_PORT&rdquo;, que podemos configurar no 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-diff" data-lang="diff"><span class="line"><span class="cl"><span class="gu">@@ -62,6 +62,7 @@ app.get(&#39;/contagem-pessoas&#39;, (_, res) =&gt; {
</span></span></span><span class="line"><span class="cl"> app.use(errorHandler);
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> const numForks = Number(process.env.CLUSTER_WORKERS) || 1;
</span></span><span class="line"><span class="cl"><span class="gi">+const httpPort = Number(process.env.HTTP_PORT) || 8081;
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> if(cluster.isPrimary &amp;&amp; process.env.CLUSTER === &#39;true&#39;){
</span></span><span class="line"><span class="cl">     logger.info(`index.js: Primary ${process.pid} is running`);
</span></span><span class="line"><span class="cl"><span class="gu">@@ -74,8 +75,8 @@ if(cluster.isPrimary &amp;&amp; process.env.CLUSTER === &#39;true&#39;){
</span></span></span><span class="line"><span class="cl">         logger.info(`index.js: worker ${worker.process.pid} died: code ${code} signal ${signal}`);
</span></span><span class="line"><span class="cl">     });
</span></span><span class="line"><span class="cl"> } else {
</span></span><span class="line"><span class="cl"><span class="gd">-    const serverApp = app.listen(8080, () =&gt; {
</span></span></span><span class="line"><span class="cl"><span class="gd">-        logger.info(`index.js:${process.pid}:Listening on 8080`);
</span></span></span><span class="line"><span class="cl"><span class="gi">+    const serverApp = app.listen(httpPort, () =&gt; {
</span></span></span><span class="line"><span class="cl"><span class="gi">+        logger.info(`index.js:${process.pid}:Listening on ${httpPort}`);
</span></span></span><span class="line"><span class="cl">     });
</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>Agora adicionamos essa variável no docker compose e colocamos network_mode host em todos os serviços, como neste trecho de código. Veja como também mudamos a URL de conexão com o banco de dados pra ser localhost. Não vai mais ter NAT pra um host virtual chamado &ldquo;postgres&rdquo;. A rede virtual do Docker possui um DNS interno, que traduz nomes como &ldquo;postgres&rdquo; pro endereço virtual, no modo de rede virtual. Em modo host, o container se pendura direto no localhost, por isso temos que nos preocupar com conflito de portas agora.</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-diff" data-lang="diff"><span class="line"><span class="cl"><span class="gu">@@ -1,17 +1,19 @@
</span></span></span><span class="line"><span class="cl"> version: &#39;3.4&#39;
</span></span><span class="line"><span class="cl"> services:
</span></span><span class="line"><span class="cl"><span class="gd">-  app1: &amp;app
</span></span></span><span class="line"><span class="cl"><span class="gi">+  app1:
</span></span></span><span class="line"><span class="cl">     # image: lukas8219/rinha-be-2023-q3:latest
</span></span><span class="line"><span class="cl">     build: .
</span></span><span class="line"><span class="cl">     hostname: app1
</span></span><span class="line"><span class="cl">     environment:
</span></span><span class="line"><span class="cl"><span class="gd">-      DB_URL: &#34;postgres://postgres:12345678@postgres:5432/postgres&#34;
</span></span></span><span class="line"><span class="cl"><span class="gi">+      DB_URL: &#34;postgres://postgres:12345678@localhost:5432/postgres&#34;
</span></span></span><span class="line"><span class="cl">       DB_POOL: 35
</span></span><span class="line"><span class="cl">       PINO_LOG_LEVEL: &#34;debug&#34;
</span></span><span class="line"><span class="cl">       REQ_TIMEOUT: &#34;2000&#34;
</span></span><span class="line"><span class="cl">       NODE_ENV: production
</span></span><span class="line"><span class="cl">       CLUSTER: true
</span></span><span class="line"><span class="cl">       CLUSTER_WORKERS: 5
</span></span><span class="line"><span class="cl"><span class="gi">+      HTTP_PORT: 8080
</span></span></span><span class="line"><span class="cl"><span class="gi">+    network_mode: host
</span></span></span><span class="line"><span class="cl">     expose:
</span></span><span class="line"><span class="cl">       - &#34;8080&#34;
</span></span><span class="line"><span class="cl">     depends_on:
</span></span><span class="line"><span class="cl"><span class="gu">@@ -27,8 +29,32 @@ services:
</span></span></span><span class="line"><span class="cl">           cpus: &#39;0.5&#39;
</span></span><span class="line"><span class="cl">           memory: &#39;0.6GB&#39;
</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 última coisa, depois de ajustar o docker compose, é ajustar a configuração do nginx. Repetindo, não tem o DNS interno da rede virtual, tudo é localhost, então temos que mudar os servidores upstream do proxy reverso pra ser localhost. Subimos uma instância da aplicação na porta 8080 e outra na 8081, pra não conflitar.</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-diff" data-lang="diff"><span class="line"><span class="cl"><span class="gu">@@ -11,8 +11,8 @@ http {
</span></span></span><span class="line"><span class="cl">     error_log /dev/null emerg;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">     upstream api {
</span></span><span class="line"><span class="cl"><span class="gd">-        server app1:8080;
</span></span></span><span class="line"><span class="cl"><span class="gd">-        server app2:8080;
</span></span></span><span class="line"><span class="cl"><span class="gi">+        server localhost:8080;
</span></span></span><span class="line"><span class="cl"><span class="gi">+        server localhost:8081;
</span></span></span><span class="line"><span class="cl">         keepalive 500;
</span></span><span class="line"><span class="cl">     }
</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>Pronto, não mostrei configuração dos demais serviços no docker compose mas já deve ter dado pra entender, né? Fica de exercício repetir essa modificação em um dos projetos da rinha. Eu mesmo contribuí pull requests em vários dos projetos participantes justamente pra adicionar essa pequena otimização. Se eu consegui, vocês também conseguem. Agora podemos subir com &ldquo;docker compose up&rdquo; de novo e, se não tiver nenhuma mensagem de erro, no outro terminal podemos rodar o Gatling de novo. Não vou mostrar rodando tudo de novo, vamos direto pro final.</p>
<p>A última coisa que o script de Gatling faz é chamar o endpoint &ldquo;/contagem-pessoas&rdquo;. Da última vez tinha voltado que conseguiu gravar no banco uns 40 mil registros. Mas agora, olha a diferença, devolveu  quase 47 mil registros. É uma diferença considerável. Se voltarmos nos resultados que estavam aparecendo no terminal, olhem, não tem mais aquela mensagem de Premature Close do IOException de Java. Vamos abrir os gráficos.</p>
<p>Vou colocar o anterior na esquerda e o novo na direita. Mesmo aplicativo, única coisa que mudou na direita é que subimos docker compose com os serviços em modo de rede host. De cara já podemos ver que a barrinha vermelha sumiu. Ótimo sinal. Apareceu um pouco de laranja. O que antes tava dando vermelho, agora demora mais, mas vai até o fim e responde. Isso que é laranja. Olha a tabelinha mais pra baixo, antes conseguíamos só um total de 108 mil requisições, agora conseguimos mais de 115 mil requisições. Mais importante: antes teve um total de quase 10 mil knockouts, mas agora foram só uns 150. Diferença brutal.</p>
<p>Dando um zoom aqui do lado, tem um conceito que acho importante que todos entendam. Praticamente todo iniciante pensa em tempo de resposta médio. Média é esta coluna aqui pro fim, &ldquo;mean&rdquo;. O que é média? É tabular quanto que levou pra responder cada uma dessas 115 mil requisições, somar tudo e dividir pelo total de requisições. Esse tempo deu 879 milissegundos. Quase 1 segundo por requisição. Se tinha alguma dúvida, esse tempo de resposta é absurdamente lento. Qualquer coisa acima de meio segundo é inaceitável em qualquer aplicação. O certo é sempre manter menos que 100 ou 50 milissegundos.</p>
<p>Veja logo do lado, desvio padrão de 1.7 segundos. O desvio padrão é maior que a média. Só um desvio padrão pro lado e já estamos falando de mais de 2 segundos por requisição. É uma ordem de grandeza inteira. O que isso diz sobre sua aplicação? Nada importante. Vamos entender os números das colunas anteriores, que são mais interessantes.</p>
<p>Olha aqui, 99th percentile. Essa coluna diz o tempo que levou pra responder o 1% das requisições mais lentas nesse teste. Isso dá mais ou menos 1.150 requisições. Soma o tempo delas, divide por 1.150 e isso dá quase 8 segundos. E faz sentido, provavelmente são as últimas mil e tantas requisições desse teste de carga, quando nos aproximamos dos 600 usuários por segundo que configuramos no cenário do Gatling.</p>
<p>Descemos pro 95th percentile, os 5% mais lentos. Já cai pra pouco mais de 5 segundos por requisição. Vamos pro 75th percentile, os 15% mais lentos, já cai pra faixa de 1 segundo por requisição. Veja como os tempos caem drasticamente. No 50th percentile, a média da metade mais lenta de requisições, é na faixa de 2 milissegundos. Vamos olhar nos gráficos lá embaixo pra ter uma noção melhor disso.</p>
<p>Olha esse primeiro gráfico, vejam com os tempos de resposta por requisição se mantiveram super baixo, lá na casa dos 2 milissegundos por quase todo o teste de carga. Lembrando que configuramos o Gatling pra ir subindo de 6 usuários por segundo até 600 ao longo de 3 minutos. Por mais de 2 minutos, nosso setup segurou super bem, quase não varia esse tempo, mesmo aumentando quantidade de pessoas. Mas aí começa o desespero. Veja como os tempos de resposta aumentam drasticamente, salta pra faixa de 5 segundos, tenta cair, mas volta a subir até quase 10 segundos.</p>
<p>No gráfico logo abaixo temos o número de requisições por segundo. De novo, como estamos subindo número de usuários linearmente, o número de requisições também sobe linearmente e, se tivesse recursos sobrando, esse gráfico seria perfeitamente linear até o fim. Mas naquele mesmo trecho, onde os tempos de resposta sobem, começa a ficar serrilhado, até dar um crash drástico na quantidade de requisições por segundo que conseguimos responder.</p>
<p>O gráfico em azul é o número de requisições que conseguimos aceitar, já o gráfico abaixo é o número de respostas que nossa aplicação conseguiu devolver. Verde são os OK, vermelho são os knockouts. Graças ao modo de rede host, veja como quase não tem vermelho, mas se olharmos o gráfico anterior, veja como tinha muito mais vermelho nessa faixa de respostas.</p>
<p>Voltando pra tabelinha, entendam: média de tempo de resposta por requisição não diz nada. Média de 879 milissegundos por requisição. Não diz nada. Agora, lendo o gráfico sabemos que a requisição que mais demorou pra ser respondida demorou mais de 9 segundos, que é essa coluna de máximo. Sabemos que apesar de 5% das requisições ter demorado pra caramba, na faixa de 5 segundos, a grande maioria conseguiu ter resposta na faixa de 2 milissegundos, o que é excelente.</p>
<p>O que causou essa lentidão de segundos na faixa do 95th percentile, não é problema de código, mas limitação de recursos da infraestrutura mesmo. Não tem muito mais que se possa fazer em código pra encurtar tempo de forma significativa. Eu sei, porque eu tentei, em 16 linguagens, lembram do outro video? É mais fácil aumentar os recursos, um pouco mais de CPU, um pouco mais de RAM.</p>
<p>Por outro lado, se no 50th percentile os tempos estivessem na faixa de 1 segundo ou mais. Aí sim, o problema seria definitivamente no código, um gargalo que precisaria ser corrigido. Vocês precisam rodar benchmarks assim em diversos tipos de projetos, diversos tipos de endpoints, pra começar a ter noção de que tipos de tempos de resposta mínimo, máximo, 95th percentile e 50th percentile, são considerados bons ou ruins.</p>
<p>Esses números são importantes pra ajudar a decidir se vale a pena perseguir otimização de código ou simplesmente alocar mais hardware. Seu &ldquo;achômetro&rdquo;, sem números, não vale nada. &ldquo;Ah, eu acho que temos que reescrever esse código em outra linguagem&rdquo;. Me mostre os números. Se não é capaz de gerar estes números, eu sei que também é incapaz de reescrever qualquer coisa pra ficar melhor.</p>
<p>Achei bacana esta rinha ter dado ênfase em um desafio de backend baseado em benchmark e teste de carga. Eu acho que todo bom programador sempre tem uma ferramenta de benchmark guardado na manga pra usar justamente em momentos de decisão como o que falei antes: vale a pena parar pra otimizar? Quanto de melhoria estamos esperando ter ao custo de quanto tempo pra arrumar? Só aumentar máquina resolveria? Faça medições pra saber.</p>
<p>Eu não conhecia o Gatling, mas achei uma ferramenta muito boa, especialmente porque gera gráficos fáceis de ler e de mostrar pros outros. Existem outras ferramentas que eu usava mais antes, como o antigo Apache Bench ou o WRK que acho que fala &ldquo;work&rdquo;. Pra mim esses dois eram tão essenciais quanto saber usar wget ou curl. Recentemente mexi um pouco no tal de Vegeta também, que parece ser mais na categoria do Gatling. E tem dezenas de outros, open source e comerciais. Não deixem de fuçar as opções e testar todas que puder.</p>
<p>Espero que tenham entendido até aqui, porque agora acho que vai ficar um pouco mais complicado, no sentido que vou discutir conceitos de mais alto nível. Se começar a achar que complicou, é a mesma coisa de sempre, anotem os termos que não entenderam e pesquisem. Depois voltem e vejam se conseguem acompanhar o raciocínio até o fim. Até agora foi mais um aquecimento.</p>
<p>Eu deixei de lado banco de dados até agora pra poder atacar esse assunto de uma só vez agora. Eu já fiz um video falando mais sobre como bancos de dados funcionam por baixo, então recomendo que assistam lá se não viram ainda. Ter na cabeça essa estrutura vai ser importante pra entender o que vou explicar agora.</p>
<p>Como disse no primeiro video da rinha, o erro que quase todo mundo cometeu neste desafio foi subestimar o Postgres. Se fosse MySQL teria sido a mesma coisa: nós teríamos achado que seria o gargalo. Isso porque na nossa experiência em projetos de verdade, na maior parte do tempo, o gargalo costuma ser o banco de dados. Por que?</p>
<p>Porque em sistemas mais realistas, com centenas de tabelas, milhares de índices, milhares de relacionamentos entre tabelas, qualquer operação de insert ou update costuma mexer não só em uma linha de uma única tabela, mas pode acionar triggers, gatilhos, pra rodar procedures em outras tabelas. Pode acionar gatilho pra atualizar múltiplos índices. Então nenhuma operação é &ldquo;simplesmente&rdquo; só mexer uma linha, normalmente são múltiplas operações acontecendo em múltiplos lugares diferentes. Isso custa tempo, às vezes pode levar dezenas de milissegundos.</p>
<p>Mesma coisa queries, selects. Em sistemas complexos normalmente temos queries com vários inner joins, outer joins, sub-selects, unions. Toda consulta, na realidade, precisa acionar diversos índices, fazer alguns table scans, então pode demorar, talvez dezenas de milissegundos. Pra piorar temos o custo por conexão. No caso de um postgres, cada conexão ao banco pode custar na faixa de 2 a 4 megabytes. 100 conexões poderiam exigir quase meio gigabyte de RAM.</p>
<p>Por isso precisamos usar pools de conexão pra reciclar conexões. Quando uma requisição termina de ser respondida, o framework devolve a conexão livre pro pool. Assim, outra requisição pode reusar a mesma conexão, sem precisar alocar mais 4 megabytes extras toda vez. Quando estamos falando de milhares de requisições por segundo, megabytes se tornam gigabytes muito rápido, se não tomarmos cuidado.</p>
<p>Lembram do nosso gráfico? Eu disse que uma resposta levar 2 milissegundos é muito bom. Mas se levar 10 milissegundos só pra inserir um registro numa tabela, não vai ser ruim? Vai sim. Por isso em projetos de verdade usamos diversas estratégias pra economizar processamento no banco o máximo possível. Usamos pelo menos dois tipos de estratégia: uma pra leitura, onde cacheamos resultados, pra evitar toda hora repetir as mesmas queries pesadas, e estratégia de filas de jobs assíncronos, pra evitar metralhar o banco com operações demais de gravação e modificação.</p>
<p>Lembrando: toda operação de modificação bloqueia um ou mais registros em uma ou mais tabelas no banco. Por cima desses locks temos transações, logs transacionais pra, caso de aconteça alguma pane, a gente não tenha perda de dados, sem deixar nada corrompido. Toda essa proteção custa processamento e custa tempo. Toda pesquisa sendo feita em tabelas que estão constantemente sendo modificadas significa que uma operação de leitura fica concorrendo com uma operação de escrita por esses locks. Ou seja, se não tivermos controle e recebermos um volume grande demais de escritas, ao mesmo tempo várias leituras, tudo vai ficar lento até parar.</p>
<p>Por isso precisamos desafogar o banco de dados. Dá pra criar servidores separados de réplicas somente de leitura. Dessa forma, podemos separar um banco mestre só pra escrita e bancos só de leitura, pra onde podemos mandar as queries. Mas réplicas não saem de graça: quanto mais réplicas colocar, maior o peso pra manter eles sincronizados. Quando falamos em cache estamos falando em serviços em paralelo como Memcache ou Redis. Até na AWS existe o serviço ElastiCache, que é uma interface pra memcache ou redis como serviço.</p>
<p>No caso de filas, alguns usam Nats ou Redis como gerenciador de fila. Funcionam, mas não recursos avançados. Não oferecem muitas das garantias de um sistema de filas maior. Assim como um banco relacional deve oferecer as proteções ACID, uma fila de verdade precisa de garantias, por exemplo, garantindo que um elemento na fila nunca vai ser duplicado. Filas de verdade são coisas como o lendário RabbitMQ ou o mais popular Apache Kafka. AWS também tem um serviço, o SQS, que é o Simple Queue Service.</p>
<p>No caso do cache a ideia é simples: primeiro pergunta pro cache se ele tem a resposta. Se não tiver, aí manda a query pro banco de dados, daí guarda o resultado no cache pra próxima vez. Dessa forma, o banco não precisa ficar sendo redundante e gastar processamento pra responder o que já acabou de responder. O Cache é uma memória temporária pra diminuir a carga de pesquisa.</p>
<p>No caso de filas a ideia é um pouco mais complicada: em vez de metralhar o banco de dados com operações de modificação, jogue todas essas operações numa fila. Configure um número limitado e controlado de trabalhadores, workers, que ficam escutando essa fila, e vão puxando essas operações. O número de workers é o número de conexões que sabemos que o banco aguenta sem começar a ficar muito lento.</p>
<p>Tudo isso eu já expliquei no episódio de &ldquo;Tornando sua App Web mais rápida&rdquo; e também no episódio onde sugiro como o site Ingresso.com poderia melhorar sua arquitetura. E sabendo de tudo isso, claro, como as instruções da rinha pediam pra limitarmos o uso de recursos do Docker pra no máximo 1.5 CPU e 3 giga de RAM, e sabendo que vai rodar um teste de carga com milhares de inserts e pesquisas simultâneas, o primeiro instinto foi pensar: bora já garantir e colocar cache nas pesquisas e filas e jobs pros inserts.</p>
<p>Eu fiz isso na minha primeira versão de Ruby on Rails, que vocês podem ver no meu GitHub, e como vimos no episódio anterior, diversos projetos que eu avaliei fizeram algo similar. Alguns colocaram só cache de redis. Alguns tentaram adicionar algum tipo de fila. Mas o fato é que se pararmos pra pensar friamente no que exatamente é este projeto e qual é a carga, já dá pra saber que qualquer coisa mais sofisticada assim era matar uma mosca com bazuca.</p>
<p>Esses projetinho inteiro tem uma única tabela. Só uma. Não tem relacionamento nenhum, não tem chave estrangeira, não tem joins, nada. Lembram daquela planilha de dados aleatórios que o script de Gatling usa pra fazer os inserts? Quantas linhas tinha? 100 mil? Eu diria, &ldquo;só&rdquo; 100 mil, e durante os 3 minutos de carga, qual é o total de requisições? Umas 100 mil mesmo. Qual é a contagem final de registros inseridos? Faixa de 45 a 50 mil registros. Entenda, isso é absurdamente pouco. Ordem de poucos milhares de registros é quase nada. Qualquer computador furreca dos anos 80 sabe lidar com milhares de registros.</p>
<p>Um banco com uma única tabela de míseras 50 mil linhas, sofrendo não mais que 100 mil tentativas de inserts, é algo miseravelmente pequeno. Bancos de dados de verdade tem centenas de tabelas, a maioria com centenas de milhares de linhas, muitas com milhões, dezenas de milhões de linhas. Isso sim começa a parecer &ldquo;grande&rdquo;. Uma única tabela é um hello world.</p>
<p>Portanto, apesar de ser divertido de implementar, nem cache de leitura, nem fila de jobs de escrita era realmente necessário. Bastava usar o banco normalmente. Um Postgres é mais do que capaz de fazer inserções simples assim de forma extremamente rápida. Na verdade vai ser na mesma ordem de grandeza de tempo do que escrever num banco de cache como Redis, por isso não faz diferença nesse caso. Só faz diferença usar Redis quando a escrita for ordens de grandeza mais rápido porque o Postgres está tendo que lidar com operações muito pesadas em cima de um volume realmente grande de dados.</p>
<p>Entenderam? Muita gente ainda pensa que Redis ou Memcache é automaticamente mais rápido do que qualquer banco de dados. Isso não é verdade. Um banco de dados quase vazio vai ser tão rápido quanto um Redis. Mesma coisa se você exagerar e colocar coisa demais no Redis, ele vai ficar lento também. Nada é automático em programação. Se você encarar dessa forma, tudo que fizer sempre vai sair uma droga. &ldquo;Ah, mas o Redis não devia cuidar de tudo sozinho e ser mais rápido??&rdquo; Lógico que não, idiota.</p>
<p>Mas havia de fato um pequeno fator de preocupação. Na API de pesquisa por termos, a rinha pede pra pesquisar ou no campo de nome ou no campo de apelido ou no campo de tecnologias. Ficaria um select com ILIKE &ldquo;%blabla%&rdquo; OR ILIKE &ldquo;%blabla%&rdquo;, que é simplesmente um dos piores tipos de pesquisa que se pode fazer em SQL.</p>
<p>Por causa desses porcentos no começo e no fim, significa que não temos como usar um índice. Índices funcionam pra pesquisas exatas. Pesquisa parcial, o único jeito é fazer pattern matching em todas as linhas da tabela. Uma vez pra cada campo depois de &ldquo;OR&rdquo;. Como temos três campos pra pesquisar, vão ser três table scans por todas as linhas da tabela até aquele ponto. Isso pode ficar pesado muito rápido sem você perceber. Se num determinado momento a tabela tiver 10 mil linhas, vamos ter que fazer 30 mil operações de comparação. E quanto maior fica a tabela, mais longa fica a pesquisa. E qual a solução?</p>
<p>Recomendo assistirem depois o episódio do código vazado do algoritmo do Twitter, onde eu faço uma tangente pra explicar o que é full-text search, ou pesquisa por embeddings em espaço vetorial. Não vou repetir o que expliquei lá, mas é o que está por baixo de como um Google ou Bing conseguem responder qualquer pesquisa tão rápido e com os resultados mais relevantes logo no topo da primeira página de resposta. Todo banco de dados moderno tem suporte a algo similar, e isso é chamado de &ldquo;full text search&rdquo;.</p>
<p>No caso do Postgres existem duas técnicas mais conhecidas: GIN e GIST. GIN significa Generalized Inverted Index ou Índice Invertido Generalizado e é mais recomendado pra estruturas de dados complexas como arrays, JSONB, hstore. É otimizado pra pesquisas de múltiplas chaves e permite indexação dos valores individuais de um array, chaves de um JSONB e assim por diante. O índice GIN é construído ao extrair chaves dos valores indexados e criando uma lista de postagem, uma lista ordenada de ponteiros para as linhas indexadas.</p>
<p>GIST vem de Generalized Search Tree ou Árvore de Procura Generalizada. GIST é uma técnica de indexação de uso mais geral e requer a implementação de métodos de procura customizadas. Pode ser usado pra indexar praticamente qualquer tipo de dados no Postgres, incluindo dados espaciais, tipos de dados customizados e qualquer tipo de texto. Índices GIST são caracterizados por uma estrutura hierárquica de nós.</p>
<p>Podemos somar GIST com o suporte a uma extensão de trigramas. LEu explico sobre n-grams no video do Twitter. Vamos mostrar na prática o que todo esse mambo jambo significa. Pra isso vou abrir o código que cria a tabela no banco. O Lucas fez bem em adicionar esse código no Node.js. Ele inicia importando a biblioteca &ldquo;pg&rdquo; e cria um Pool de conexões usando a URL de conexão que vem da variável de ambiente DB_URL no 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-js" data-lang="js"><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">pg</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">&#39;pg&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="p">{</span> <span class="nx">logger</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">&#39;./logger&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">URL</span> <span class="o">=</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">DB_URL</span> <span class="o">||</span> <span class="s1">&#39;postgres://postgres:12345678@localhost:5432/postgres&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">pool</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">pg</span><span class="p">.</span><span class="nx">Pool</span><span class="p">({</span> <span class="nx">connectionString</span><span class="o">:</span> <span class="nx">URL</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nx">max</span><span class="o">:</span> <span class="p">(</span><span class="nb">Number</span><span class="p">(</span><span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">DB_POOL</span><span class="p">)</span> <span class="o">||</span> <span class="mi">200</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="nx">idleTimeoutMillis</span><span class="o">:</span> <span class="mi">0</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nx">connectionTimeoutMillis</span><span class="o">:</span> <span class="mi">10000</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>Daí registra um evento no pool, o evento &ldquo;connect&rdquo;, que diz: assim que conectar, execute a função anônima a seguir, que vai enviar uma query pro pool. Essa query são comandos em SQL. Como temos duas instâncias de Node subindo ao mesmo tempo, em ambos vai executar essa função, então precisamos escrever os comandos pra, caso já exista o que queremos criar, só ignora e segue em frente.</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-js" data-lang="js"><span class="line"><span class="cl"><span class="nx">pool</span><span class="p">.</span><span class="nx">once</span><span class="p">(</span><span class="s1">&#39;connect&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">logger</span><span class="p">.</span><span class="nx">info</span><span class="p">(</span><span class="sb">`database.js: Connected  to db </span><span class="si">${</span><span class="nx">URL</span><span class="si">}</span><span class="sb">`</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="nx">logger</span><span class="p">.</span><span class="nx">info</span><span class="p">(</span><span class="sb">`Creating table &#34;pessoas&#34; if not exists`</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nx">pool</span><span class="p">.</span><span class="nx">query</span><span class="p">(</span><span class="sb">`
</span></span></span><span class="line"><span class="cl"><span class="sb">        CREATE EXTENSION IF NOT EXISTS pg_trgm;
</span></span></span><span class="line"><span class="cl"><span class="sb">
</span></span></span><span class="line"><span class="cl"><span class="sb">        CREATE OR REPLACE FUNCTION generate_searchable(_nome VARCHAR, _apelido VARCHAR, _stack JSON)
</span></span></span><span class="line"><span class="cl"><span class="sb">            RETURNS TEXT AS $$
</span></span></span><span class="line"><span class="cl"><span class="sb">            BEGIN
</span></span></span><span class="line"><span class="cl"><span class="sb">            RETURN _nome || _apelido || _stack;
</span></span></span><span class="line"><span class="cl"><span class="sb">            END;
</span></span></span><span class="line"><span class="cl"><span class="sb">        $$ LANGUAGE plpgsql IMMUTABLE;
</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>Por exemplo, o primeiro comando diz: &ldquo;se já não existir, crie a extensão pg_trgm&rdquo;, que é a extensão que dá suporte a tokenizar textos em trigramas. Em seguida, &ldquo;crie ou substitua caso já exista, a função generate_searchable, que recebe os parâmetros nome, apelido e stack, todos de tipo varchar&rdquo;. Lembram? Varchar é variable characters, uma lista de caracteres de tamanho variável, basicamente uma string.</p>
<p>A função retorna um valor de tipo texto, e esse texto vai ser a concatenação dos campos nome, apelido e stack, tudo junto. Mas pra que isso? Paciência que já vai entender. Na sequência, caso não exista, criamos uma tabela com nome &ldquo;pessoas&rdquo;, que é a tabela principal. Primeiro campo se chama &ldquo;id&rdquo; de tipo UUID. Opcionalmente podemos declarar que se não passarmos um valor explicitamente, pra gerar um uuid aleatório usando a função gen_random_uuid(), além disso a constraint é que esse campo deve ser único e não nulo.</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-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="k">IF</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">EXISTS</span><span class="w"> </span><span class="n">pessoas</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 class="n">id</span><span class="w"> </span><span class="n">uuid</span><span class="w"> </span><span class="k">DEFAULT</span><span class="w"> </span><span class="n">gen_random_uuid</span><span class="p">()</span><span class="w"> </span><span class="k">UNIQUE</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">NULL</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="n">apelido</span><span class="w"> </span><span class="nb">TEXT</span><span class="w"> </span><span class="k">UNIQUE</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">NULL</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="n">nome</span><span class="w"> </span><span class="nb">TEXT</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">NULL</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="n">nascimento</span><span class="w"> </span><span class="nb">DATE</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">NULL</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="n">stack</span><span class="w"> </span><span class="n">JSON</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="n">searchable</span><span class="w"> </span><span class="nb">text</span><span class="w"> </span><span class="k">GENERATED</span><span class="w"> </span><span class="n">ALWAYS</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="p">(</span><span class="n">generate_searchable</span><span class="p">(</span><span class="n">nome</span><span class="p">,</span><span class="w"> </span><span class="n">apelido</span><span class="p">,</span><span class="w"> </span><span class="n">stack</span><span class="p">))</span><span class="w"> </span><span class="n">STORED</span><span class="w">
</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>Em seguida, declaramos o campo apelido como sendo tipo texto, também único e não nulo. Campo nome deve ser tipo texto e não nulo. Campo nascimento deve ser tipo Date e não nulo. Campo stack deve ser tipo JSON, que é uma das vantagens de usar Postgres, porque ele permite tipos de dados mais complexos como JSON. Não precisa de MongoDB pra isso. E, finalmente, declaramos uma coluna especial, de nome &ldquo;searchable&rdquo; e tipo text.</p>
<p>Só que esse campo declaramos como um campo &ldquo;generated&rdquo;. É um campo cujo valor vai ser gerado automaticamente em toda operação de insert ou update e vai usar o retorno desta função que declaramos antes, o generate_searchable. Lembram o que ela faz? Devolve os valores de nome, apelido e stack concatenados. É isso que esse campo vai ter. Mas por que?</p>
<p>O próximo comando SQL é, caso não exista, pra criar um novo índice chamado idx_pessoas_searchable, na tabela pessoas, usando GIST. Ao GIST dizemos pra indexar aquela coluna &ldquo;searchable&rdquo; usando o operador de classe &ldquo;gist_trgm_ops&rdquo;, que é especificamente desenhado pra tokenizar esse campo em trigrams, que são conjuntos de 3 caracteres, quebrados, ou tokenizados, de cada palavra da string. É isso que permite pesquisas parciais.</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-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="k">INDEX</span><span class="w"> </span><span class="k">IF</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">EXISTS</span><span class="w"> </span><span class="n">idx_pessoas_searchable</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="k">public</span><span class="p">.</span><span class="n">pessoas</span><span class="w"> </span><span class="k">USING</span><span class="w"> </span><span class="n">gist</span><span class="w"> </span><span class="p">(</span><span class="n">searchable</span><span class="w"> </span><span class="k">public</span><span class="p">.</span><span class="n">gist_trgm_ops</span><span class="w"> </span><span class="p">(</span><span class="n">siglen</span><span class="o">=</span><span class="s1">&#39;64&#39;</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>O objetivo dessa coluna é evitar ter que fazer nome like blabla &ldquo;OR&rdquo; apelido like blabla &ldquo;OR&rdquo; stack like blabla. Não precisamos mais de &ldquo;OR&rdquo;. Basta fazer searchable like blabla e pronto. Se qualquer um desses campos conter o blabla, vai retornar. &ldquo;OR&rdquo; custa caro, sempre que puder evitar, evite, mesmo que signifique desperdiçar espaço. Pode ser um tradeoff que vale a pena. Pesquisa com porcento, wildcard, também custa caro. Agora vamos ver a função no projeto do Lucas pra pesquisa de termos. Fica mais pra baixo, a função &ldquo;findByTerm&rdquo;.</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-js" data-lang="js"><span class="line"><span class="cl"><span class="nx">module</span><span class="p">.</span><span class="nx">exports</span><span class="p">.</span><span class="nx">findByTerm</span> <span class="o">=</span> <span class="kr">async</span> <span class="kd">function</span> <span class="nx">findByTerm</span><span class="p">(</span><span class="nx">term</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kr">const</span> <span class="nx">query</span> <span class="o">=</span> <span class="sb">`
</span></span></span><span class="line"><span class="cl"><span class="sb">    SELECT
</span></span></span><span class="line"><span class="cl"><span class="sb">        id,
</span></span></span><span class="line"><span class="cl"><span class="sb">        apelido,
</span></span></span><span class="line"><span class="cl"><span class="sb">        nome,
</span></span></span><span class="line"><span class="cl"><span class="sb">        to_char(nascimento, &#39;YYYY-MM-DD&#39;) as nascimento,
</span></span></span><span class="line"><span class="cl"><span class="sb">        stack
</span></span></span><span class="line"><span class="cl"><span class="sb">    FROM
</span></span></span><span class="line"><span class="cl"><span class="sb">        pessoas
</span></span></span><span class="line"><span class="cl"><span class="sb">    WHERE
</span></span></span><span class="line"><span class="cl"><span class="sb">        searchable ILIKE $1
</span></span></span><span class="line"><span class="cl"><span class="sb">    LIMIT 50;`</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nx">pool</span><span class="p">.</span><span class="nx">query</span><span class="p">(</span><span class="nx">query</span><span class="p">,</span> <span class="p">[</span><span class="sb">`%</span><span class="si">${</span><span class="nx">term</span><span class="si">}</span><span class="sb">%`</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>Olha só, definimos o template da query com um string. Select os campos id, apelido, etc, from tabela pessoas, where searchable like $1, limitado a 50 linhas de resposta. Esse &ldquo;$1&rdquo; vai ser substituído por esse valor aqui embaixo, que é o termo concatenado com porcentagem no começo e no fim. Ué, mas isso não era lento de pesquisar? Vamos entender.</p>
<p>Todo banco de dados relacional possui um comando pra explicar como que uma query vai ser executada, a estratégia de pesquisa. Pra ver isso, podemos conectar com o cliente de linha de comando do postgres, o psql. Então podemos nos conectar com &ldquo;docker exec -it node-postgres-1 psql -U postgres -d postgres&rdquo; e pronto.</p>
<p>Vale lembrar que estou conectando no postgres logo depois de rodar o teste de carga, portanto a tabela já está com mais de 46 mil linhas inseridas com sucesso. Vamos começar analisando uma query simples. Eu mando &ldquo;explain analyze&rdquo; seguido da query &ldquo;select * from pessoas where apelido = &lsquo;foo&rsquo;&rdquo;. Como esperado, essa coluna apelido já tem um índice, então ele corretamente diz que vai fazer um &ldquo;index scan&rdquo; no índice &ldquo;pessoas_apelido_index&rdquo;. Pesquisa utilizando índice costuma ser muito rápido e podemos ver o Execution Time aqui no final dizendo que levou só 0.037 milissegundos, 37 microssegundos, super rápido.</p>
<p>Esses outros números aqui em cima, por exemplo em cost, ou custo, o primeiro é o custo relativo pra puxar a primeira linha do resultado, esse 0.41 e esse 8.43 é o custo relativo pra puxar o resto das linhas. Mesma coisa esse actual time, é o tempo pra puxar a primeira linha e depois o tempo pra puxar o resto das linhas. Uma das razões pra ele distinguir a primeira linha do resultado pro resto é pra dar noção da latência que sua aplicação vai ter entre mandar fazer a pesquisa e o primeiro resultado que volta no streaming e que já poderia ser mostrado.</p>
<p>Como esta query usa um índice e a quantidade de linhas é absurdamente pequeno, leva 16 microssegundos até puxar a primeira linha e pouco mais de 16 microssegundos pra puxar todo o resto. É muito rápido mesmo. Mas agora vamos ver quão mais lento seria a pesquisa por termos do jeito que a grande maioria dos programadores iniciantes faria. Olha só: <code>explain analyze select * from pessoas where apelido ilike '%foo%' or nome ilike '%foo%' or stack::text ilike '%foo%'</code>.</p>
<p>Olha o tempo que leva pra executar essa query: 42 milissegundos, ou 42 mil microssegundos. Comparado com a query simples anterior estamos falando de um tempo de resposta mais de mil vezes mais lento. Sim, 42 milissegundos ainda é relativamente rápido, mas considerando que eu falei que essa tabela é absurdamente pequena, e queries baseadas em índice conseguem acabar na ordem de microssegundos, 42 milissegundos é um tempo longo.</p>
<p>Olha só a estratégia que ele diz que essa query vai ser obrigada a usar: &ldquo;seq scan&rdquo; que é scan sequencial, literalmente um full table scan. Imagine um loop com for indo da linha 0 até a linha 46 mil e tanto e checando os campos linha a linha. É o pior caso pra uma pesquisa em banco, que é checar tudo, sem poder usar índices. Você quer evitar esse tipo de query como diabo foge da cruz, porque quanto maior ficar sua tabela, mais lento sua pesquisa vai ficar. Com índice não, você pode dobrar o tamanho da tabela e o tempo da query quase não muda.</p>
<p>Será que tudo isso que fizemos de criar o campo searchable concatenando os campos nome, apelido e stack e depois indexando usando Gist com Trigrams faz alguma diferença? Não podemos só chutar e dizer que sim sem testar. Então vamos lá: explain analyze <code>select * from pessoas where searchable ilike '%foo%'</code> e olha só, é mais rápido sim.</p>
<p>Caímos de 42 milissegundos pra só 4 milissegundos. É pelo menos 10 vezes mais rápido. E olha a estratégia de pesquisa. Não tem mais um sequential scan como antes. Em vez disso tempos um Bitmap Heap Scan. Essa estratégia é dividida em duas e começa primeiro com um Bitmap Index Scan, que vai usar o índice que fizemos usando Gist com Trigram. Nessa fase teremos uma certa quantidade de linhas de resultado. Essas linhas vão ser marcadas num mapa de bits, um bitmap, e na segunda fase de pesquisa, ela vai filtrar só as linhas marcadas, usando essa condição de rechecagem que mostra na estratégia.</p>
<p>Ou seja, ainda tem uma fase de scan de linhas, mas não é na tabela inteira, só nas linhas retornadas na primeira etapa de pesquisa pelo índice. Neste caso, o índice sozinho não dá a resposta toda, por isso existe o Bitmap Heap Scan. É mais lento do que um Index Scan, como no primeiro exemplo que testamos, mas é bem mais rápido do que um full sequential scan.</p>
<p>Portanto sim, valeu a pena tudo isso de GIST. E é uma boa dica pra qualquer projeto. Em projetos maiores, onde estaríamos usando coisas como cache em Redis, também poderíamos delegar pesquisa full text pra outro serviço externo. Eu sempre recomendo o bom e velho ElasticSearch. O Elastic é como se fosse um banco de dados NoSQL especializado em full text search. Povo de monitoramento e devops também costuma integrar ElasticSearch com serviços como Kibana, Logstash, pra fazer pesquisa em logs, por exemplo.</p>
<p>Elastic é pra quando seu projeto crescer e recursos como GIST já não estarem segurando a carga de processamento no banco. Nunca faça overengineering nos primeiros dias de um projeto. Só especialize em serviços externos quando tiver carga pra justificar isso. Quanto mais serviços externos, mais difícil vai ser pra dar manutenção em tudo. É um trade-off. Tudo tem seu tempo, e não é no dia 1.</p>
<p>De qualquer forma, vocês começaram a entender? Uma operação simples SQL num Postgres sem tuning nenhum, rodando na minha máquina, é da ordem de microssegundos. Muito menos de 1 milissegundo. Bancos de dados foram feitos pra serem rápidos. Assistam meus episódios sobre Árvores e Banco de Dados. É muito mais do que só apendar uma linha nova no final de um arquivo.</p>
<p>Outra técnica que eu tinha deixado pra comentar neste episódio foi sobre o tal SQL Bulk Insert. Muitos dos projetos ganhadores com do Vinicius em Rust ou do André e Albert em C#, fizeram o truque de acumular os inserts ou numa fila externa ou em memória e um job em background ir pegando grupos de inserts e mandando de uma só vez pro banco. Mas realmente não tem muito o que comentar. O conceito é bem simples.</p>
<p>Em vez de toda requisição ter que conectar no banco, mandar um insert, esperar o resultado e daí responder, podemos fazer ele responder sem saber se vai gravar com sucesso ou não. E ir acumulando. Depois que acumular uma quantidade razoável, sei lá, uns 100 inserts, daí manda tudo de uma vez só, dentro de uma única transaction. Só de não ter que criar uma nova transação pra cada insert individual, fora mandar pela rede tudo de uma vez, economiza bastante tempo. Mas como já repeti mil vezes neste episódio, 100 mil tentativas de inserts simples é pouca carga. Acho que pra quase nenhuma dos projetos participantes, realmente precisava ter usado essa técnica, no máximo teria feito pouca diferença não usar.</p>
<p>Bulk insert e também upserts são técnicas importantes. Povo de data warehouse, business intellingence, data science e tudo mais que precisa limpar dados e carregar volumes grandes de uma só vez, sabe que precisa fazer bulk insert, senão desperdiça tempo. Cada banco de dados tem peculiaridades de sintaxe, por isso é melhor que vocês pesquisem no google &ldquo;postgres bulk insert&rdquo; ou &ldquo;postgres batch insert&rdquo; que os primeiros links explicam direitinho. Não vou ficar repetindo o que já tem fácil em tutoriais simples.</p>
<p>Mais importante é voltarmos pro docker compose e falar da configuração de postgres. Cada participante da rinha fez essa parte de maneiras ligeiramente diferentes, mas no geral todo mundo estava preocupado em mudar este parâmetro: max_connections. A maioria chutava nessa faixa de 450 conexões. Lembram que uma conexão custa uns 2 megabytes? Se realmente usarmos todas as 450 conexões máximas, o container de postgres poderia usar quase 900 megabytes. Isso é coisa demais, considerando que temos só 3 gigabytes pra caber tudo. Por isso não adianta querer subir esse número todo.</p>
<p>Tem algumas formas diferentes de mudar essa configuração. A mais simples é desse jeito mesmo: mudando a linha de comando do postgres e adicionando o parâmetro direto. Outra forma é que nem fizemos com o nginx e criar um arquivo postgres.conf pra mapear como volume pra dentro do container. E outra forma é como criamos a tabela e índices: direto na aplicação, quando ela se conectar pela primeira vez, manda comandos SQL, e um desses comandos seria pra mandar aumentar o máximo de conexões.</p>
<p>Mas a pergunta mais importante é: quantas conexões de fato precisa suportar pro tipo de carga que nosso teste de Gatling vai executar? 450 é mesmo suficiente? Ou menos daria? E se for menos, quanto menos? Se configurar o pool de conexões da aplicação pra deixar já aberto 450 conexões, estaria desperdiçando muita RAM. Se alocar poucas conexões, daí as primeiras requisições vão gastar mais tempo esperando abrir novas conexões. Tem um balanço aqui, e justamente testes de carga servem pra validar se nossas premissas realmente valem na prática.</p>
<p>Pra facilitar nossa vida, já vimos que dá pra conectar na linha de comando do cliente psql pra fazer queries direto no banco. Mas podemos adicionar algumas ferramentas mais visuais pra facilitar tanto sua vida, quanto minha vida aqui de mostrar. A que deixei pra mostrar por último é o pgAdmin, um velho de guerra que usamos faz mais de década, desde antes de Docker ter sido inventado. É muito simples, basta adicionar este serviço no seu docker compose:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>  pgadmin:
    image: dpage/pgadmin4
    environment:
      PGADMIN_DEFAULT_EMAIL: admin@admin.com
      PGADMIN_DEFAULT_PASSWORD: admin
      PGADMIN_LISTEN_PORT: 5050
    network_mode: host
    depends_on:
      - postgres</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>Email e senha tanto faz, é só porque vai subir uma interface web de administração. Importante é declarar essa porta 5050, porque senão ele vai tentar subir na porta 80, e como estamos em modo de rede host, vai tentar subir na porta 80 de verdade do localhost, e não vai ter permissão sem ser root, então vai falhar no boot. Mude pra uma porta acima de 1000 qualquer, como 5050.</p>
<p>Agora podemos subir &ldquo;docker compose up&rdquo; e esperamos alguns segundos. Vamos acelerar e pronto, no navegador do lado podemos carregar localhost 5050 e olha só que bonito. Só colocar o mesmo email e senha que configuramos e estamos dentro. Depois fucem essa interface e leiam a documentação no site deles pra saber o que é o quê. Mas já deixo um aviso importante: apesar de útil, nunca, jamais, deixe um serviço como esse exposto na internet pública.</p>
<p>Seria uma tremenda burrice deixar isso desprotegido porque se alguém conseguir entrar, vai ter acesso ao seu banco de dados de produção. Nada de nenhum banco de dados de produção jamais pode aparecer na internet pública. É pedir pra ser invadido. Suba temporariamente em produção pra alguma tarefa em específico, mas retire do ar o quanto antes. Use em ambiente de staging, em ambiente de desenvolvimento, em rede privada da empresa, dentro de uma DMZ, mas nunca na internet pública. Sem mais, nem menos, vocês foram avisados.</p>
<p>Enfim, eu já tinha cadastrado o postgres que estávamos usando nos testes, mas deixa eu cadastrar de novo pra vocês verem como é. Dêem um nome qualquer, endereço é localhost, usuário é postgres, senha é a mesma que você configurou no docker compose e pronto, conectado. Mas e daí, o que podemos fazer com isso? Vamos no terminal do lado e rodar o teste de carga do Gatling de novo.</p>
<p>Aqui estou acelerando na edição, mas olhem só que bacana, conseguimos ver em tempo real o monitoramento de coisas como transações por segundo. Vou avançar mais pro meio do teste. Note como o total de transações está em azul mas temos, mais ou menos, meio a meio entre transações que deram commit ok e transações que deram rollback por ter falhado. Isso é simples de explicar.</p>
<p>Lembram que o teste de carga faz um total de umas 100 mil requisições, mas só 46 mil são de fato gravadas? Os outros dão erro porque campos como apelido, nome tem constraints pra serem unique, nunca duplicar. Quando tentamos inserir duplicado, rejeita e dá rollback. Por isso, mais ou menos, meio a meio.</p>
<p>Embaixo podemos ver gráficos pra ter noção do volume de dados entrando e saindo. Cada conjunto de campos, a linha em si, é uma tupla. Temos tuplas entrando, que são os inserts, temos tuplas saindo, que são resultados de queries, temos gráfico de I/O que não tenho certeza da unidade de medida, se são operações de leitura e gravação ou quantidade de bytes ou algo assim.</p>
<p>Mais importante é este primeiro gráfico no topo. Acompanhe em azul, é o total de conexões no banco neste momento. Em verde são conexões onde estão acontecendo operações, como nossos inserts ou selects. Em vermelho são conexões abertas sem fazer nada. Veja como é mais ou menos estável a quantidade de conexões, mesmo sabendo que no teste de carga estamos aumentando gradativamente o número de usuários por segundo, subindo até 600 usuários por segundo.</p>
<p>E essa é a importância de um bom pool de conexões. A mesma conexão é reusada, reciclada, entre várias requisições. Por isso não precisa de tantas. Só bem no finalzinho, bem no final mesmo, quando caímos no território do 95th percentile, talvez 75th percentile, aquele 5 a 15% que demora mais de 1 segundo pra responder, aí aumenta drasticamente o número de conexões abertas ao mesmo tempo. Nesse finalzinho dá impressão que a requisição no Node está tão lenta que não dá tempo de devolver a conexão pro pool, daí precisa ficar abrindo mais e mais conexões.</p>
<p>Essas últimas requisições que demoram mais de 5 segundos, são completamente fora da realidade. Se num produto real estivéssemos tendo tempos nessa ordem de grandeza com frequência, ou precisava ver se falta otimizar alguma coisa, ou precisa urgentemente aumentar os recursos da infra. Não devemos configurar o máximo de conexões no banco, nem o tamanho do pool de conexões baseados nesse pico final.</p>
<p>Ao contrário do que muitos poderiam imaginar, durante quase o teste de carga todo, nunca passou de 40 conexões, ativas ao mesmo tempo, era faixa de 20 conexões. Não é 450, nem 300, nem mesmo 100. Só mesmo no finalzinho, mas aí já era repescagem. Portanto, se alguém chutava que máximo de conexões ao banco seria um gargalo, chutou errado também.</p>
<p>E porque são menos de 40 ao mesmo tempo? Se voltarmos ao docker compose, na configuração da aplicação, veja na seção de variáveis de ambiente. Tem esse DB_POOL que passamos pro pool da biblioteca &ldquo;pg&rdquo; usada pelo Node. Neste caso está 35. No caso temos duas instâncias, então seria um máximo de 70. Mesmo assim, durante a maior parte do teste, nenhum dos dois pool nunca é saturado. Só no final. Aí meio que não importa muito se tivéssemos colocar mais ou não. O pico final a infra não aguenta mesmo.</p>
<p>E qual é o melhor número pra colocar no pool de conexões? Cada blog post e stackoverflow que procurar vai vir com alguma fórmula mágica pra tentar chutar. Mas não tem jeito, pra saber de verdade, só fazendo um teste de carga, monitorando via pgAdmin e, via tentativa e erro, rodar o teste de carga pra diferentes valores de tamanho de pool. Esse é o único jeito certo de saber se estamos chutando um número apertado demais ou sobrando demais. Não acredite em nada a não ser testes que você mesmo rodou pra saber.</p>
<p>Isso tudo dito, muitos de vocês assistindo que tem experiência, incluindo vários dos participantes, e eu mesmo, tínhamos certeza que o Postgres seria um problema. Mas agora que medimos e temos dados de verdade, temos certeza que estávamos errados. As técnicas em si funcionam, mas a aplicação e a carga do teste eram muito menores do que imaginávamos. Superestimamos o problema e fizemos overengineering. Típico de programadores fazer overengineering. A lição é simples: faça o código mais simples que resolve o problema dado, faça medições e, só depois disso, decida onde realmente precisa otimizar, e vá medindo pra ver se a otimização realmente melhorou, ou se não piorou.</p>
<p>E isso nos leva aos tópicos finais que faltou discutir da minha lista de pendências. Eu falei que ia comentar sobre Crystal, sobre Kotlin, daí sobre threads e fibers. Mas quero ser breve nesses pontos. Durante o video de 16 linguagens eu quis mostrar que sim, com praticamente todas as linguagens, era possível bater o máximo do teste de carga. Alguns com um pouco mais de trabalho que outros, mas todo mundo era capaz de passar ou chegar muito perto.</p>
<p>Significa que todas as linguagens são igualmente válidas? Ou igualmente performáticas? Claro que não. Vamos usar o bom senso aqui. Obviamente, todas as linguagens compiladas em binário nativo levam vantagem em performance, Rust e Go, principalmente, seguido de C++, Java e seus derivados como Kotlin ou Scala, C#, Crystal, Zig, até Bun. Essa é uma categoria clara de performance nível A.</p>
<p>Linguagens interpretadas, como PHP, Javascript e seus derivados como Typescript, Ruby, Python, todos fazem parte da mesma categoria B de performance. Não é possível atingir o mesmo nível de performance bruta da categoria A. É sim, possível escalar aplicações web pra números parecidos, ao custo de um pouco mais de hardware. Isso porque boa parte de uma aplicação web depende I/O, e I/O vai ser mais lento independente se a aplicação é compilada ou interpretada.</p>
<p>Portanto sim, a infra pra linguagens da categoria B precisa ser um pouco maior do que da categoria A. Mas não é ordens de grandeza maior, esse é o ponto. Do outro lado da equação está a facilidade ou em encontrar bons profissionais ou treinar novos profissionais, e é aí que linguagens da categoria B ultrapassam muito os da categoria A.</p>
<p>Pergunte a qualquer entusiasta de Rust e ele vai dizer que eu estou equivocado, que Rust é muito fácil, que ele aprendeu super fácil. Mas sinto dizer: eu acredito em você, mas seu perfil é diferente da maioria da população. As pessoas não são iguais. Quem acha ficar fluente em Rust algo &ldquo;fácil&rdquo; é uma fração minúscula da população. A grande maioria não tem nem a vontade e nem a capacidade pra ficar fluente em linguagens de baixo nível, que exigem um entendimento muito mais detalhado de como tudo por baixo funciona, a rede, o sistema operacional, o hardware.</p>
<p>E isso não é algo inerente só a Rust. Nos anos 80 e 90 nós sempre tivemos, Assembly, C, C++, Objective-C. Mas quais foram as linguagens que ficaram populares? Nos anos 80 eram coisas como Cobol ou dBase. Nos anos 90 eram coisas como Visual Basic ou Delphi. Delphi até era compilado nativo, mas o que atraiu as pessoas era o ferramental, a IDE, o banco de dados Interbase que vinha no pacote. Ninguém usava Delphi pra programar drivers de sistema operacional ou coisas assim.</p>
<p>Agora é a mesma coisa. Tem uma categoria de pessoas que desde sempre achava fácil programar em C, C++, até Assembly, e agora Rust, Go. Pra realmente tirar proveito desse tipo de linguagem, você precisa ser o tipo de pessoa que gosta de aprender intimamente como funciona o hardware por baixo, o sistema operacional inteiro por baixo. Pra categoria B, uma pessoa que não está nem aí e nem sabe a diferença entre memória RAM e SSD, ainda consegue produzir alguma coisa. A curva de aprendizado é nitidamente menor e por isso tem mais gente nessas linguagens da categoria B.</p>
<p>Duh, antes que apareça comentário idiota: óbvio que tem programadores avançados na categoria B também. Mas alguém medíocre, não sobrevive na categoria A. Portanto, é sim possível fazer uma web startup inteira só com Rust ou Go, mas boa sorte em manter uma equipe de dezenas ou centenas de pessoas que escala junto com o crescimento da empresa. Em breve não vai acompanhar. Por isso toda grande empresa de tecnologia usa um misto de tecnologias. Não existe empresa gigante que consegue usar uma única linguagem pra tudo.</p>
<p>E isso sem contar o ecossistema. Produtividade não vem só da linguagem. Vem das bibliotecas, produtos, serviços, documentação, cursos, livros, eventos e tudo mais que circula ao redor da linguagem. É muito mais difícil comparar uma linguagem e dizer só &ldquo;ah, é compilado&rdquo; ou &ldquo;ah, é interpretado&rdquo;. Essa é a parte que menos importa na comparação.</p>
<p>Tem uma questão cultural também. Deixa eu dar um exemplo bem besta. Todo mundo que é de Javascript está acostumado a ir no terminal e digitar algo como &ldquo;npx create-react-app my-app&rdquo;. Quem é de Ruby on Rails está acostumado a ir no terminal e digitar &ldquo;rails new my-app&rdquo;. Quem é de Python está acostumado a fazer &ldquo;django-admin startproject my-app&rdquo; e assim por diante.</p>
<p>Como que alguém de Java inicia um novo projeto? Vai no site start.spring.io, preenche um fucking formulário, que vai gerar o projeto e te dar um zip pra baixar. Daí você manualmente deszipa e esse é o começo do seu projeto. Quase em 2024, pra mim isso tem cheiro de 2004. Outras linguagens como C# tem opções de linha de comando com dotnetcore, mas como a maioria dessa comunidade prefere realmente fazer? Sim, abrir o Visual Studio Enterprise e clicar em Novo Projeto e ir preenchendo.</p>
<p>Pra gente de Javascript, Ruby, Python, PHP, isso parece super ultrapassado. Mas pra quem é de Java ou C#, parece uma super frescura ter uma linha de comando pra gerar as coisas se o site ou a IDE já fazem tudo. E é aquele caso que depende do ponto de vista. Nenhum dos dois lados nunca vai concordar nisso. Mas é esse tipo de discordância em ponto de vista que vai garantir que nunca tenhamos uma única linguagem e um único ecossistema que vai ser o padrão pra tudo. Eu detesto ter que usar uma IDE. Do que ter que usar IntelliJ ou Visual Studio, eu prefiro mudar pra qualquer linguagem que eu possa ser produtivo só da linha de comando, usando NeoVim. Não é negociável pra mim.</p>
<p>Eu já fiz um video só explicando as diferenças entre linguagens compiladas e interpretadas e explico mais a fundo sobre essas coisas, recomendo que assistam depois. Mas sobre Crystal e Kotlin em particular que eu analisei na rinha. Ambos são linguagens mais novas, com comunidade pequena. No caso de Kotlin, ele já foi criado por uma empresa grande, a JetBrains e depois apoiado pelo próprio Google. E mesmo assim a adoção não é tão grande quanto eu gostaria de ver a esta altura.</p>
<p>De novo, quem está acostumado só com Java não vê grande valor em migrar pra Kotlin. No final o bytecode é compatível, pra que mudar? Pra nós que estamos acostumados com linguagens mais modernas, o Kotlin é uma excelente opção pra quando formos obrigados a fazer alguma coisa na plataforma Java. Vale a pena ficar de olho e ter no seu arsenal. Mas sempre vai haver essa divisão. Eu sinto que Kotlin é mais atraente pra quem não é de Java, do que pra quem já é experiente em Java.</p>
<p>Crystal é muito menor e bem mais de nicho. Mesmo assim eu acho uma linguagem super competente, estável, com um ecossistema mínimo já funcional. Eu acho a sintaxe de Ruby muito superior à de um Go Lang. Me cancelem, mas é verdade. Porém, obviamente Ruby é uma categoria de performance muito inferior a Go. Mas Crystal é realmente a sintaxe de Ruby com performance de Go, mesma ordem de grandeza. Sim, Go ainda é melhor otimizado e por ser uma linguagem mais primitiva, também é mais rápido de compilar.</p>
<p>Mas pra coisas como microsserviços web, eu acho Crystal e frameworks como o Lucky, bem mais prazerosos de usar do que similares em Go ou Rust. Se produtividade for importante e evitar fadiga também, Crystal pode ser uma boa alternativa, especialmente pra equipes pequenas. A curva de aprendizado é menor. Vale ter no arsenal. E eu reforço isso depois de ter experimentado linguagens como Nim, que existe há muito mais anos, mas continua péssimo de usar, super mal documentado, com poucas bibliotecas usáveis e muitos bugs. Parece uma linguagem experimental, que eu não recomendaria pra usar em produção de jeito nenhum.</p>
<p>Dá pra ficar o dia todo só falando das diversas linguagens, e eu já comentei bastante sobre elas no episódio original da rinha, então assistam lá depois. Mas voltando ao assunto de conexões no banco, só queria deixar um comentário sobre a diferença de linguagens compiladas como Rust e interpretadas como Javascript. Ou mais especificamente, linguagens que tem capacidade de escalar com threads nativas e linguagens que não podem por causa de lock global de interpretador, que todas as linguagens da categoria B tem.</p>
<p>Eu explico isso no episódio de linguagens compiladas e interpretadas. Mas na nossa situação de uso com banco de dados costuma significar o seguinte: além de serem mais performáticos pelo fato de serem compilados, linguagens como Rust ou mesmo Java, tem a vantagem de precisar de menos conexões simultâneas. Um pool de conexões de Java é thread-safe, ou seja, pode ser utilizado por múltiplas threads nativas em paralelo.</p>
<p>Numa linguagem como Javascript ou Ruby, ou qualquer interpretada, cada thread nativa precisa ter seu próprio pool de conexões. Em resumo é mais ou menos assim: em Java poderíamos configurar o pool de conexões pra ter um máximo de 100 conexões, e é isso que a aplicação vai ter no total: 100 conexões.</p>
<p>Já num Javascript, depende. Se rodar com um processo só, vai subir só um pool de conexões, se colocar o máximo de 100, é isso que vai ter: até 100 também. O reactor do Node.js é single threaded. Toda chamada assíncrona roda numa mesma thread nativa e vai acessar um único pool de conexões.</p>
<p>No caso de Node.js em modo cluster, ele pode subir múltiplos forks de processos de Node. Cada um com um pool de conexões separado. Então se for subir 5 forks no cluster, devemos configurar o pool pra ter no máximo 20 conexões, se eu quiser manter o mesmo máximo. 5 vezes 20 é o máximo de 100, entenderam? Por isso não dá pra subir forks infinitos no cluster, porque cada fork vai tendo menos e menos conexões que pode usar no pool.</p>
<p>Estou pulando vários detalhes. Em cada linguagem vai ter alguma peculiaridade na implementação de threads, co-rotinas, pools. Você precisa entender como I/O não bloqueante funciona e como sua linguagem favorita lida com isso. Fica de lição de casa, caso ainda não tenham estudado sobre isso. A dica é começar assistindo meu antigo episódio sobre concorrência e paralelismo pra entender essa base.</p>
<p>Mas o ponto foi só pra vocês saberem que existem diferenças grandes em como se configura coisas como pool de conexões em Java e em Node.js, por exemplo. A configuração de coisas como pool de conexões vai ser diferente. A melhor forma de aprender a diferença é monitorando com um pgAdmin da vida e criando cenários de teste de carga com Gatling ou Vegeta da vida, pra ver na prática quais números funcionam e quais não funcionam.</p>
<p>Quando se é júnior, você faz tudo às cegas, no achismo, achando que vai dar tudo certo. Mas à medida que ganha experiência e vai ficando mais sênior, começa a entender que chutômetro é o caminho mais rápido pra software ruim. Sempre parece que o software faz coisas que você não previa. Pra ganhar controle sobre isso, só tem um caminho: aprendendo sobre benchmarks, profiling, carga, monitoramento, medição, estatística e finalmente começar a enxergar e interpretar corretamente o comportamento do software de acordo com diferentes níveis de carga e estresse.</p>
<p>E com isso, acho que consegui cobrir todos os tópicos que tinha deixado pendente. Não queria fechar o ano com pendências. Com isso posso finalmente dar por encerrado esse assunto de rinha. Vamos começar o ano com temas novos. Em Janeiro de 2024 eu vou tirar férias e passar três semanas no Japão, então acho que não vai ter video em Janeiro. Ainda não sei se volto a publicar antes ou depois do Carnaval, então pode ser ser que eu só volte entre Fevereiro e Março. Aproveitem pra maratonar o canal. Devo ficar postando sobre a viagem no Instagram, então me sigam lá. Se ficaram com dúvidas, mandem nos comentários abaixo, se curtiram o video deixem um joinha, assinem o canal, e não deixem de compartilhar o video com seus amigos. A gente se vê em 2024, Feliz Natal e Bom Ano Novo! Até mais!</p>
]]></content:encoded><category>performance</category><category>containers</category><category>bancos-de-dados</category><category>akitando</category></item><item><title>[Akitando #139] - Entendendo Como Containers Funcionam</title><link>https://akitaonrails.github.io/2023/03/02/akitando-139-entendendo-como-containers-funcionam/</link><guid isPermaLink="true">https://akitaonrails.github.io/2023/03/02/akitando-139-entendendo-como-containers-funcionam/</guid><pubDate>Thu, 02 Mar 2023 09:00:00 -0300</pubDate><description>&lt;div class="embed-container"&gt;
&lt;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&gt;
&lt;/iframe&gt;
&lt;/div&gt;
&lt;p&gt;Finalmente vou pagar promessa. Este é o video sobre containers e Docker que venho falando faz vários videos atrás, que ia fazer. Mas eu precisava explicar a instalação, como Linux boota, como funciona virtualização, pra finalmente chegar em containers. Então este episódio vai amarrar tudo que vocês aprenderam nos videos de Linux, de armazenamento, etc e finalmente vão entender containers do jeito certo.&lt;/p&gt;</description><content:encoded><![CDATA[

<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>

<p>Finalmente vou pagar promessa. Este é o video sobre containers e Docker que venho falando faz vários videos atrás, que ia fazer. Mas eu precisava explicar a instalação, como Linux boota, como funciona virtualização, pra finalmente chegar em containers. Então este episódio vai amarrar tudo que vocês aprenderam nos videos de Linux, de armazenamento, etc e finalmente vão entender containers do jeito certo.</p>
<h2>Erratas<span class="hx:absolute hx:-mt-20" id="erratas"></span>
    <a href="#erratas" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>5:12 - tá errado, isso é em outros comandos, como top eu acho, que mostraria 400% se tivesse usando 4 CPU. Do jeito que eu falei ficou parecendo que naquele quadrado vermelho mostraria isso, mas ali é POR cpu/thread, por isso vai só até 100%. Só em linhas de comando que mostra num número só quanto que tá se usando que apareceria, por exemplo, 400%.</p>
<h2>Conteúdo<span class="hx:absolute hx:-mt-20" id="conteúdo"></span>
    <a href="#conte%c3%bado" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><ul>
<li>00:00 - Intro: qual a diferença de VMs e Containers?</li>
<li>01:49 - CAP 1 - Limitando Processos - cgroups</li>
<li>07:43 - CAP 2 - Enganando Processos - namespaces</li>
<li>14:45 - CAP 3 - Recapitulando Containers - LXC, Docker, OCI</li>
<li>17:34 - CAP 4 - A Base de Containers - RunC</li>
<li>22:29 - CAP 5 - Hello World de Docker - O que tem dentro?</li>
<li>33:20 - CAP 6 - Restringindo Processos Ainda Mais - Capabilities</li>
<li>37:18 - CAP 7 - Orquestrando Containers - Docker Compose</li>
<li>41:26 - CAP 8 - Bônus do Episódio &ldquo;Tornando sua App Web mais Rápida&rdquo;  - Load Balancer com Docker</li>
<li>43:25 - CAP 9 - Orquestra de Gente Grande - K8S e Terraform</li>
<li>47:17 - CAP 10 - Outros Usos de Containers - Apps em Containers?</li>
<li>51:28 - Bloopers</li>
</ul>
<h2>Links<span class="hx:absolute hx:-mt-20" id="links"></span>
    <a href="#links" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><ul>
<li><a href="https://github.com/akitaonrails/episode0139"target="_blank" rel="noopener">https://github.com/akitaonrails/episode0139</a></li>
<li><a href="https://www.zsiegel.com/2022/01/17/load-balancing-in-docker-with-nginx"target="_blank" rel="noopener">https://www.zsiegel.com/2022/01/17/load-balancing-in-docker-with-nginx</a></li>
<li><a href="https://www.mo4tech.com/unshare-command-details-and-examples.html"target="_blank" rel="noopener">https://www.mo4tech.com/unshare-command-details-and-examples.html</a></li>
<li><a href="https://ericchiang.github.io/post/containers-from-scratch/"target="_blank" rel="noopener">https://ericchiang.github.io/post/containers-from-scratch/</a></li>
<li><a href="https://wiki.archlinux.org/title/cgroups"target="_blank" rel="noopener">https://wiki.archlinux.org/title/cgroups</a></li>
<li><a href="https://nodejs.org/en/docs/guides/nodejs-docker-webapp/"target="_blank" rel="noopener">https://nodejs.org/en/docs/guides/nodejs-docker-webapp/</a></li>
<li><a href="https://docs.docker.com/compose/gettingstarted/"target="_blank" rel="noopener">https://docs.docker.com/compose/gettingstarted/</a></li>
<li><a href="https://flast101.github.io/docker-privesc/"target="_blank" rel="noopener">https://flast101.github.io/docker-privesc/</a></li>
<li><a href="https://blog.alexellis.io/runc-in-30-seconds/"target="_blank" rel="noopener">https://blog.alexellis.io/runc-in-30-seconds/</a></li>
<li><a href="https://man7.org/linux/man-pages/man7/capabilities.7.html"target="_blank" rel="noopener">https://man7.org/linux/man-pages/man7/capabilities.7.html</a></li>
<li><a href="https://blog.container-solutions.com/linux-capabilities-in-practice"target="_blank" rel="noopener">https://blog.container-solutions.com/linux-capabilities-in-practice</a></li>
<li><a href="https://github.com/containers/bubblewrap"target="_blank" rel="noopener">https://github.com/containers/bubblewrap</a></li>
</ul>
<h2>SCRIPT<span class="hx:absolute hx:-mt-20" id="script"></span>
    <a href="#script" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Olá pessoal, Fabio Akita</p>
<p>O penúltimo video sobre o setup do meu PC foi pesado em configuração mas se conseguiu chegar até o fim, deve ter entendido um pouco mais sobre como virtualização funciona, em particular usando o emulador QEMU acelerado com KVM. Aquilo é virtualização: criar uma máquina virtual que roda outro sistema operacional, outra kernel, por cima.</p>
<p>Hora de pagar a promessa que vim fazendo em diversos videos. Finalmente falar um pouco sobre os tais &ldquo;containers&rdquo; que até hoje muita gente confunde com virtualização. As duas coisas &ldquo;parece&rdquo; que é rodar um programa dentro de uma máquina virtual. Mas containers são completamente diferentes de máquinas virtuais. Vamos lá.</p>
<p>(&hellip;)</p>
<p>Se fosse fazer um TLDR, uma única frase que resume a diferença, é até simples: uma máquina virtual roda um novo kernel, possivelmente numa outra arquitetura, como x86 em cima de ARM, já um container reaproveita o mesmo kernel, necessariamente na mesma arquitetura de hardware. É essa a diferença. Agora vamos ver na prática.</p>
<p>Vou começar falando só de containers de Linux. BSD, Windows, MacOS tem infraestrutura diferente pra containers. Windows tem como rodar container pra rodar apps de Windows. Isso é leve. Mas quando Windows ou MacOS precisam rodar apps de Linux, na prática o que acontece é que eles de fato sobem uma máquina virtual de Linux primeiro, como o WSL que já fiz dois videos a respeito. Portanto envolve sim virtualização. E é dentro dessa máquina que um Docker Desktop cria containers de Linux. Só em Linux nativo que containers são realmente &ldquo;leves&rdquo;.</p>
<p>Em Linux precisamos entender dois componentes fundamentais: cgroups e namespaces. Cgroups já mencionei no episódio anterior quando mostrei como isolar threads da CPU só pra máquina virtual e só pro host. Cgroups ou control groups é um mecanismo da kernel pra agrupar processos e limitar recursos como cpu e memória.</p>
<p>Existem várias formas de listar cgroups. A mais simples acho que é só dar <code>ls /sys/fs/cgroup</code>. Lembra o script chamado &ldquo;suspend&rdquo; que fiz pra limitar AllowedCPUs pro host? Usei o systemd pra limitar esse parâmetro pros cgroups init.scope, system.slice e user.slice. Vejam na listagem que todos são diretórios. Se listar um deles, vem vários arquivos como &ldquo;cpu.weight&rdquo; ou &ldquo;memory.pressure&rdquo;. É como se parametriza os recursos pra esses cgroups.</p>
<p>É tudo organizado como uma árvore, pra variar olha só árvores de novo, onde um cgroup herda configurações de outro cgroup. Honestamente, não sei os detalhes de como é configurado cada um que já vem de cada distro e imagino que cada um configura essa árvore de formas um pouco diferente. Essa listagem é no meu Manjaro. Num Ubuntu é semelhante, tem a maioria dos mesmos slices.</p>
<p>Se listarmos o que tem em system.slice, tem novos subdiretórios, um pra cada daemon que o systemd inicia, e também cgroups pra cada um dos aplicativos instalados via Snap, que vou falar um pouco mais pra frente. Pra enxergamos isso melhor podemos rodar o comando <code>systemctl status</code>. Agora temos a hierarquia toda de cgroups.  Como falei antes, olha aqui o system.slice e os sub-grupos de cada daemon. E mais pra baixo temos o user.slice onde os programas que estamos rodando como navegador, terminal e tudo mais ficam em seus próprios sub-grupos também. Todos os processos rodando estão organizados em cgroups. Você nem sabia disso, mas toda vez que executa um programa, ele vai estar atrelado a pelo menos um cgroup.</p>
<p>Lembra que quando usamos comandos com <code>ps</code> ou programas como <code>htop</code> eles pesquisam os metadados no sistema de arquivos virtual <code>/proc</code>? Por exemplo, vamos pegar o PID do meu navegador Edge. Tem vários processos, todo navegador separa cada aba num processo diferente pra isolar, ter mais segurança, e se uma aba crashear, não derrubar todas as outras junto. Podemos fazer <code>cat /proc/PID/group</code> e aqui vamos em qual sub-grupo de cgroup esse process está. No escopo do edge, dentro de app.slice, dentro de user service, dentro de user.slice.</p>
<p>Existem várias formas de controlar cgroups. Uma delas é usando o sistema de arquivos virtual disponível no diretório <code>/sys/fs/cgroups</code>. Isso é um ponto de montagem. Se fizermos <code>mount | grep cgroup</code> olha como aparece que o tipo do sistema de arquivos é &ldquo;cgroup2&rdquo;. É como se fosse um dispositivo virtual montado. Lembra que já expliquei nos episódios de Linux, como nos de Slackware, Gentoo e no que explico como funciona o boot, que existem diversos pontos de montagem especiais como &ldquo;/proc&rdquo; ou &ldquo;/dev&rdquo;?</p>
<p>Pra mostrar na prática pra que diabos saporra serve, deixa criar um script idiota em Ruby. Podia ser em Python, podia ser em Java, faça no que quiser. É só um loop infinito printando ponto pra sempre. Vamos rodar e em outro terminal abro o &ldquo;htop&rdquo;. Veja aqui no topo que meu script tá usando 100% de 1 CPU. Se eu tivesse feito suportar múltiplas threads, cada 100% seria uma CPU. Um programa que usa 4 threads inteiras ia aparecer aqui como 400%.</p>
<p>No topo do programa lista todas as threads independentes e veja que tem uma de 100% que é meu script. Mas se esperar um pouquinho vai ver que ele muda de lugar. Um programa não fica sempre fixo no mesmo thread. Pra isso serve o scheduler da kernel. Expliquei sobre scheduler no episódio de concorrência e paralelismo e é a razão de porque no episódio anterior tive o trabalho de mostrar como mandei a máquina virtual usar as threads de 16 a 31 e limitei o host a usar só de 8 a 15. Lembra deste script aqui usando systemd e configurando o parâmetro AllowedCPUs em todos os diretórios raíz de slices?</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-sh" data-lang="sh"><span class="line"><span class="cl"><span class="nv">command</span><span class="o">=</span><span class="nv">$2</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="s2">&#34;</span><span class="nv">$command</span><span class="s2">&#34;</span> <span class="o">=</span> <span class="s2">&#34;started&#34;</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">    systemctl set-property --runtime -- system.slice <span class="nv">AllowedCPUs</span><span class="o">=</span>0-15
</span></span><span class="line"><span class="cl">    systemctl set-property --runtime -- user.slice <span class="nv">AllowedCPUs</span><span class="o">=</span>0-15
</span></span><span class="line"><span class="cl">    systemctl set-property --runtime -- init.scope <span class="nv">AllowedCPUs</span><span class="o">=</span>0-15
</span></span><span class="line"><span class="cl"><span class="k">elif</span> <span class="o">[</span> <span class="s2">&#34;</span><span class="nv">$command</span><span class="s2">&#34;</span> <span class="o">=</span> <span class="s2">&#34;release&#34;</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">    systemctl set-property --runtime -- system.slice <span class="nv">AllowedCPUs</span><span class="o">=</span>0-31
</span></span><span class="line"><span class="cl">    systemctl set-property --runtime -- user.slice <span class="nv">AllowedCPUs</span><span class="o">=</span>0-31
</span></span><span class="line"><span class="cl">    systemctl set-property --runtime -- init.scope <span class="nv">AllowedCPUs</span><span class="o">=</span>0-31
</span></span><span class="line"><span class="cl"><span class="k">fi</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>Ctrl-C pra parar o loop infinito e agora podemos criar sub-diretórios e arquivos usando comandos normais como &ldquo;mkdir&rdquo;, &ldquo;echo&rdquo; ou podemos continuar usar o SystemD pra facilitar, que é o que vou fazer agora. Vou criar um slice pra mim chamado &ldquo;akita.slice&rdquo; em &ldquo;/etc/systemd/system/akita.slice&rdquo;. Só abrir com qualquer editor de textos usando &ldquo;sudo&rdquo; e no caso quero limitar o uso de CPU em só 30% pra processos neste cgroup. Pra isso coloco este conteúdo:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>[Slice]
CPUQuota=30%</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>Posso configurar outros limites, como memória, mas pra esse exemplo, só isso já funciona. Agora vou abrir um shell dentro desse cgroup com este comando <code>systemd-run --slice=akita.slice --uid=akitaonrails --shell</code> e nessa linha de comando que abriu, vou repetir o mesmo comando <code>ruby loop.rb</code>.</p>
<p>Olha o htop. Tá vendo aqui no topo da lista? Antes o script consumia 100% de uma thread, mas como agora reiniciei a partir do meu novo cgroup,que só permiti usar 30% da CPU, a kernel obriga a usar no máximo 30% da CPU. E com isso conseguimos controlar granuladamente o que cada processo no sistema pode ou não consumir.</p>
<p>E por que eu iria querer limitar um programa forçado assim? No Wiki do site do ArchLinux tem um exemplo no fim. Ele menciona o Matlab, que é um programa de cálculo numérico e, dependendo da complexidade do cálculo, pode consumir toda a RAM e toda CPU do sistema. Se um programa usar todos os recursos, vai faltar CPU e RAM até pra kernel e o sistema inteiro vai ficar lento ou crashear. É um tipo de ataque de DoS. DoS ou Denial of Service é um programa saturar todos os recursos da máquina até tudo parar de funcionar. Mas se colocar o Matlab num cgroup onde limito CPU e RAM pra, digamos, deixar sobrar uns 10%, a kernel vai ter a possibilidade de tentar se corrigir.</p>
<p>Depois leia mais sobre cgroups no Wiki do Arch. Mas o importante foi pra explicar que existe esse recurso em todo Linux moderno. Agora vou pular pro segundo grande recurso pra limitar o que processos podem fazer, que se chama &ldquo;Namespaces&rdquo;. Quando listo o que tem em &ldquo;/proc&rdquo; consigo enxergar todos os PIDs de todos os processos rodando no sistema. Se rodar o comando &ldquo;mount&rdquo; consigo ver todos os pontos de montagem ativos no sistema. E existem comandos ou locais onde posso listar a stack de rede, IPC que é comunicação inter-processos, IDs de usuários e grupos.</p>
<p>Mas é possível esconder todos esses detalhes de um processo. Pra isso existe o comando <code>unshare</code>, literalmente &ldquo;descompartilhar&rdquo;. Num terminal normal, se executarmos o comando <code>ps aux</code> isso vai nos devolver uma lista com todos os processos ativos neste momento, e costuma ser uma lista grande num desktop normal. Vamos abrir um novo shell, mas fazendo <code>sudo unshare --fork --pid --mount-proc /bin/bash</code>. Parece nada demais né? Mas se executarmos <code>ps aux</code> olha só, cadê todos os meus processos? Sumiu? O bash agora tem PID 1.</p>
<p>É isso que se chama namespace, é um espaço separado de nomes, no caso de PIDs. Se abrir outro terminal e executar <code>ps aux</code> tá tudo aqui bonitinho e lá no final olha o comando unshare e a instância de bash rodando com um PID alto. A diferença é que o bash que acabamos de abrir não enxerga mais o mesmo &ldquo;/proc&rdquo; global. Ele tem um &ldquo;/proc&rdquo; local só pra ele, isolado e separado que começa do 1 de novo. De dentro daquele bash, vamos listar o que tem em &ldquo;/proc&rdquo;, só os dois PIDs que listou. No outro terminal normal se listar &ldquo;/proc&rdquo; tem todos os PIDs.</p>
<p>Entenderam? Namespace faz o processo puxar a informação de outro lugar. Estamos mentindo, mas ele continua executando no mesmo sistema, igual qualquer outro programa, tanto que tem um PID de verdade. Pra quem é de Javascript, uma analogia é a diferença de criar uma variável com o antigo &ldquo;var&rdquo; pra cair no escopo global da função ou usar &ldquo;let&rdquo; e manter a variável no escopo local do bloco, manja? Não é exatamente a mesma coisa, mas é parecido.</p>
<p>Com o recurso de namespaces podemos mentir pros processos pra não enxergar mais o resto do sistema do lado de fora e assim limitar o que podem fazer. Por padrão, todos os recursos do sistema operacional são globais, todo processo tem acesso a praticamente tudo que outro processo tem, salvo permissões de leitura e execução por usuário, que você tá mais acostumado a pensar, tipo, root tem acesso a tudo. Mas com <code>unshare</code> inclusive note que ele engana meu processo fazendo achar que é root. Se executarmos o comando <code>id</code> olha, sou o usuário zero.</p>
<p>Como executamos unshare com sudo, esse root tem o mesmo nível de privilégio do root de verdade, o que não é muito seguro. Vamos dar exit e executar a mesma linha de comando mas com a opção &ldquo;&ndash;user&rdquo; e pronto. Se executarmos &ldquo;id&rdquo; agora sou nobody, literalmente um zé ninguém. Mais útil vai ser rodar como root, só que não tendo acesso ao sistema de arquivos todo. Vamos consertar isso.</p>
<p>Se assistiu meus episódios de Linux, viu minha explicação sobre chroot. Um container de verdade não usa chroot, mas pra demonstração dos conceitos vai servir. Teve um palestrante que já demonstrou isso anos atrás e deixou alguns arquivos que vamos precisar. Vou deixar o link na descrição abaixo, vamos baixar o tarball &ldquo;rootfs.tar.gz&rdquo;.</p>
<p><code>wget https://github.com/ericchiang/containers-from-scratch/releases/download/v0.1.0/rootfs.tar.gz</code></p>
<p>Pronto, baixei lá no &ldquo;/tmp&rdquo; mesmo porque é só pra demonstração. Agora é só descomprimir com <code>tar xvfz</code>, esperar um segundo &hellip; e tá lá. Se listarmos o que tem, parece com a raíz do seu HD, não? É uma mini-distribuição do que chamamos de &ldquo;userland&rdquo;. Se listar o &ldquo;rootfs/boot&rdquo; não tem a kernel nem a initram que deveria. E o que fazemos com isso? É como na instalação do Gentoo, fazemos <code>sudo chroot rootfs /bin/bash</code>. Ele vai fazer &ldquo;change root&rdquo;, mudar a raíz, pra esse diretório rootfs e executar o bash dentro do sub-diretório &ldquo;rootfs/bin&rdquo; e não o meu de verdade.</p>
<p><code>ls</code> dá pau agora. Isso porque esse shell não carregou perfil nenhum, então não tem nenhuma variável de ambiente configurado. Vamos exportar o path de novo com <code>export PATH=/sbin:/bin:/usr/bin</code> e agora sim temos acesso às ferramentas nesses diretórios. É assim que sua distro Linux faz, só que isso já vem configurado automático pra você, mas na mão é assim que fazemos.</p>
<p>Essa variável provavelmente está no seu arquivo &ldquo;/etc/profile&rdquo; ou .bashrc ou .zshrc da vida. Executando <code>ls /</code> parece que não aconteceu nada, mas faz <code>ls /boot</code>, viu? Vazio. Estamos dentro do diretório &ldquo;rootfs&rdquo; que está sendo mascarado pra gente como se fosse a nova raíz. Esse shell não tem mais acesso ao meu sistema de arquivos de verdade.</p>
<p>Agora tentamos executar <code>ps</code> e dá pau. Isso porque não existe nenhum mount de diretórios especiais, como o &ldquo;/proc&rdquo; que é onde comandos como &ldquo;ps&rdquo; pesquisam processos. Veja o erro, ele manda justamente dar mount nisso. Mas agora é que vamos juntar esse comando de chroot com o unshare. Então damos exit e executamos o comando <code>sudo unshare -p -f --mount-proc=/tmp/rootfs/proc chroot rootfs /bin/bash</code></p>
<p>A opção &ldquo;-p&rdquo; é o jeito mais curto pra &ldquo;&ndash;pid&rdquo; e &ldquo;-f&rdquo; é pra &ldquo;&ndash;fork&rdquo;. Daí montamos o proc e damos chroot via unshare. Não podemos esquecer de exportar o PATH de novo. Pronto. Se fizermos <code>ls /boot</code> tá vazio. Se fizermos <code>ls /home</code>, tá vazio. Ou seja, esse não é seu sistema, ele está enxergando a raíz a partir do diretório &ldquo;/tmp/rootfs&rdquo;. Mas agora se rodarmos <code>ps aux</code>, sucesso! Só enxergamos os processos dentro do novo namespace. Não só escondemos os PIDs de todo o sistema como escondemos o sistema de arquivos e você pensa que está num sistema completamente diferente.</p>
<p>Esse é o ponto que confunde todo mundo. O comportamento dentro desse shell faz parecer que estou dentro de uma máquina virtual. Mas isso não é uma máquina virtual, não tem um hypervisor como KVM com um emulador como QEMU simulando uma máquina nova do zero e fazendo boot de um novo kernel. Por isso estou reforçando que o diretório falso &ldquo;/boot&rdquo; tá vazio, não tem outra kernel. Isso não é uma máquina virtual. E se voltarmos pra outro terminal e executarmos <code>ps aux</code> esse bash que abrimos dentro do chroot tá aqui, aparecendo normalmente como se fosse qualquer outro processo.</p>
<p>Se não ficou claro. Alt-tab pro shell dentro do chroot. Executando <code>ls /usr/bin</code> vamos abrir um programa que tem aqui. Olha só, veio um python naquele tarball, vamos executar. Esse é o REPL de python que vocês já devem ter visto. Alt-tab pro terminal normal e <code>ps aux</code> de novo. Olha só aparecendo aqui python. Se fosse uma máquina virtual, teria um  único processo, por exemplo de QEMU e só, eu não estaria vendo os processos dentro da máquina virtual misturado com os do meu sistema. E isso porque esses processos ESTÃO rodando no meu sistema normalmente, sem emulação, nem nada disso, é nativo.</p>
<p>Se não fosse assim, chamaríamos tudo de máquina virtual. Mas por isso inventamos esse termo novo chamado &ldquo;container&rdquo; justamente pra ser distinto. O processo está só sendo enganado, achando que está num outro sistema porque cortamos os olhos dele, as mãos dele, o nariz dele. E só apontamos tudo pra outros namespaces É isso que unshare e chroot fizeram com o shell do bash: isolaram num jarro, num container. Tudo que executa com unshare está na Matrix.</p>
<p>Nessa demonstração simples só escondemos os PIDs, o diretório &ldquo;/proc&rdquo; e o sistema de arquivos, mas dá pra mudar a rede, dá pra mudar todos os outros mounts como &ldquo;/dev&rdquo; ou &ldquo;/sys&rdquo;, dá pra fazer ele enxergar outros cgroups, outro clock do sistema, outros usuários e grupos, outro hostname e domainname, ou seja, o processo realmente vai achar que está em outra máquina, sem realmente estar. Ele pára de ter acesso ao resto do sistema e efetivamente roda &ldquo;isolado&rdquo;, que é todo o objetivo.</p>
<p>Namespaces são super flexíveis. Por exemplo, se isolar o container numa rede virtual, é assim que o processo num container consegue dar bind, digamos na porta 80, e um outro processo em outro container também consegue subir na porta 80, cada um numa rede isolada que daí configuro pra expor um pra fora como porta 4001 e outra como 4002. Mas também posso adicionar esses dois containers ambos no mesmo namespace de rede e assim crio uma rede virtual só entre os dois, sacaram? Pra isso serve o comando <code>nsenter</code> que é literalmente &ldquo;entrar num namespace&rdquo;, depois pesquisem.</p>
<p>Pesquisem no Google por &ldquo;container from scratch&rdquo; ou &ldquo;container do zero&rdquo; e vão esbarrar em artigos como do site da RedHat e diversos outros artigos explicando sobre <code>unshare</code>. Mas pra hoje esses exemplos que dei já servem pra continuar a explicação. As coisas começam a ficar confusas porque existem várias ferramentas que funcionam em cima dessa infraestrutura de cgroups e namespaces que expliquei. No começo, no mundo Linux surgiu Linux Containers ou LXC e depois Docker.</p>
<p>Em 2015 a empresa do Docker criou a Open Container Initiative ou OCI, um grupo com várias outras empresas pra padronizar e tornar mais portável todos os componentes que fazem containers funcionar, em particular o formato de imagens que é o formato OCI image, é o que você baixa do site dockerio.com. Os padrões estão todos na opencontainers.org e no mundo Linux a implementação desse padrão é a tecnologia &ldquo;runC&rdquo;. Vamos ver como funciona.</p>
<p>O runC precisa de um arquivo &ldquo;config.json&rdquo; que define as características do container. E vamos aproveitar aquele diretório &ldquo;rootfs&rdquo; ainda. O próprio comando runc gera esse arquivo executando <code>runc spec</code> dentro do diretório rootfs mesmo.</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;ociVersion&#34;</span><span class="p">:</span> <span class="s2">&#34;1.0.2-dev&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nt">&#34;process&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;terminal&#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;user&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">   <span class="nt">&#34;uid&#34;</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">   <span class="nt">&#34;gid&#34;</span><span class="p">:</span> <span class="mi">0</span>
</span></span><span class="line"><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;args&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">   <span class="s2">&#34;/bin/bash&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="p">],</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;env&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">   <span class="s2">&#34;PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">   <span class="s2">&#34;TERM=xterm&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="p">],</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;cwd&#34;</span><span class="p">:</span> <span class="s2">&#34;/&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;capabilities&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">   <span class="nt">&#34;bounding&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;CAP_AUDIT_WRITE&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;CAP_KILL&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;CAP_NET_BIND_SERVICE&#34;</span>
</span></span><span class="line"><span class="cl">   <span class="p">],</span>
</span></span><span class="line"><span class="cl">   <span class="nt">&#34;effective&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;CAP_AUDIT_WRITE&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;CAP_KILL&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;CAP_NET_BIND_SERVICE&#34;</span>
</span></span><span class="line"><span class="cl">   <span class="p">],</span>
</span></span><span class="line"><span class="cl">   <span class="nt">&#34;permitted&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;CAP_AUDIT_WRITE&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;CAP_KILL&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;CAP_NET_BIND_SERVICE&#34;</span>
</span></span><span class="line"><span class="cl">   <span class="p">],</span>
</span></span><span class="line"><span class="cl">   <span class="nt">&#34;ambient&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;CAP_AUDIT_WRITE&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;CAP_KILL&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;CAP_NET_BIND_SERVICE&#34;</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><span class="line"><span class="cl">  <span class="nt">&#34;rlimits&#34;</span><span class="p">:</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="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;RLIMIT_NOFILE&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;hard&#34;</span><span class="p">:</span> <span class="mi">1024</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;soft&#34;</span><span class="p">:</span> <span class="mi">1024</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><span class="line"><span class="cl">  <span class="nt">&#34;noNewPrivileges&#34;</span><span class="p">:</span> <span class="kc">true</span>
</span></span><span class="line"><span class="cl"> <span class="p">},</span>
</span></span><span class="line"><span class="cl"> <span class="nt">&#34;root&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;path&#34;</span><span class="p">:</span> <span class="s2">&#34;/tmp/rootfs&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;readonly&#34;</span><span class="p">:</span> <span class="kc">true</span>
</span></span><span class="line"><span class="cl"> <span class="p">},</span>
</span></span><span class="line"><span class="cl"> <span class="nt">&#34;hostname&#34;</span><span class="p">:</span> <span class="s2">&#34;runc&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nt">&#34;mounts&#34;</span><span class="p">:</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="nt">&#34;destination&#34;</span><span class="p">:</span> <span class="s2">&#34;/proc&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">   <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;proc&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">   <span class="nt">&#34;source&#34;</span><span class="p">:</span> <span class="s2">&#34;proc&#34;</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><span class="line"><span class="cl">   <span class="nt">&#34;destination&#34;</span><span class="p">:</span> <span class="s2">&#34;/dev&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">   <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;tmpfs&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">   <span class="nt">&#34;source&#34;</span><span class="p">:</span> <span class="s2">&#34;tmpfs&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">   <span class="nt">&#34;options&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;nosuid&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;strictatime&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;mode=755&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;size=65536k&#34;</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><span class="line"><span class="cl">  <span class="p">{</span>
</span></span><span class="line"><span class="cl">   <span class="nt">&#34;destination&#34;</span><span class="p">:</span> <span class="s2">&#34;/dev/pts&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">   <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;devpts&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">   <span class="nt">&#34;source&#34;</span><span class="p">:</span> <span class="s2">&#34;devpts&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">   <span class="nt">&#34;options&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;nosuid&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;noexec&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;newinstance&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;ptmxmode=0666&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;mode=0620&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;gid=5&#34;</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><span class="line"><span class="cl">  <span class="p">{</span>
</span></span><span class="line"><span class="cl">   <span class="nt">&#34;destination&#34;</span><span class="p">:</span> <span class="s2">&#34;/dev/shm&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">   <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;tmpfs&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">   <span class="nt">&#34;source&#34;</span><span class="p">:</span> <span class="s2">&#34;shm&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">   <span class="nt">&#34;options&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;nosuid&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;noexec&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;nodev&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;mode=1777&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;size=65536k&#34;</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><span class="line"><span class="cl">  <span class="p">{</span>
</span></span><span class="line"><span class="cl">   <span class="nt">&#34;destination&#34;</span><span class="p">:</span> <span class="s2">&#34;/dev/mqueue&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">   <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;mqueue&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">   <span class="nt">&#34;source&#34;</span><span class="p">:</span> <span class="s2">&#34;mqueue&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">   <span class="nt">&#34;options&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;nosuid&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;noexec&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;nodev&#34;</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><span class="line"><span class="cl">  <span class="p">{</span>
</span></span><span class="line"><span class="cl">   <span class="nt">&#34;destination&#34;</span><span class="p">:</span> <span class="s2">&#34;/sys&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">   <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;sysfs&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">   <span class="nt">&#34;source&#34;</span><span class="p">:</span> <span class="s2">&#34;sysfs&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">   <span class="nt">&#34;options&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;nosuid&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;noexec&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;nodev&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;ro&#34;</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><span class="line"><span class="cl">  <span class="p">{</span>
</span></span><span class="line"><span class="cl">   <span class="nt">&#34;destination&#34;</span><span class="p">:</span> <span class="s2">&#34;/sys/fs/cgroup&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">   <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;cgroup&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">   <span class="nt">&#34;source&#34;</span><span class="p">:</span> <span class="s2">&#34;cgroup&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">   <span class="nt">&#34;options&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;nosuid&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;noexec&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;nodev&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;relatime&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;ro&#34;</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><span class="line"><span class="cl"> <span class="p">],</span>
</span></span><span class="line"><span class="cl"> <span class="nt">&#34;linux&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;resources&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">   <span class="nt">&#34;devices&#34;</span><span class="p">:</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="nt">&#34;allow&#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;access&#34;</span><span class="p">:</span> <span class="s2">&#34;rwm&#34;</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><span class="line"><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;namespaces&#34;</span><span class="p">:</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="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;pid&#34;</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><span class="line"><span class="cl">    <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;network&#34;</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><span class="line"><span class="cl">    <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;ipc&#34;</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><span class="line"><span class="cl">    <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;uts&#34;</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><span class="line"><span class="cl">    <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;mount&#34;</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><span class="line"><span class="cl">    <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;cgroup&#34;</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><span class="line"><span class="cl">  <span class="nt">&#34;maskedPaths&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">   <span class="s2">&#34;/proc/acpi&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">   <span class="s2">&#34;/proc/asound&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">   <span class="s2">&#34;/proc/kcore&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">   <span class="s2">&#34;/proc/keys&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">   <span class="s2">&#34;/proc/latency_stats&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">   <span class="s2">&#34;/proc/timer_list&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">   <span class="s2">&#34;/proc/timer_stats&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">   <span class="s2">&#34;/proc/sched_debug&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">   <span class="s2">&#34;/sys/firmware&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">   <span class="s2">&#34;/proc/scsi&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="p">],</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;readonlyPaths&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">   <span class="s2">&#34;/proc/bus&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">   <span class="s2">&#34;/proc/fs&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">   <span class="s2">&#34;/proc/irq&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">   <span class="s2">&#34;/proc/sys&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">   <span class="s2">&#34;/proc/sysrq-trigger&#34;</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><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>Agora que vocês viram a demonstração com unshare, esse arquivo não deveria parecer muito alienígena. Inclusive foi por isso que fiz videos explicando Linux antes, porque precisa entender o sistema operacional pra entender como containers são configurados. Esse arquivo começa declarando a versão do padrão OCI, daí qual processo vamos rodar, que é o bash, e com qual usuário e grupo, no caso zero quer dizer root.  Olha como já configura o PATH pra mim. &ldquo;CWD&rdquo; é Current Working Directory que é o diretório onde o programa vai abrir.</p>
<p>Capabilities pula que vou explicar depois. Daí &ldquo;root&rdquo; quer dizer a raíz do sistema de arquivos, que vai ser dentro daquele /tmp/rootfs onde fizemos chroot. Hostname é o nome da máquina, que é definido pelo namespace UTS, assim o container pode ter um nome de máquina diferente do nome da máquina de verdade.</p>
<p>No atributo de mounts aparece a mesma coisa que fizemos com a opção &ldquo;&ndash;mount-proc&rdquo; do comando unshare, onde mandamos dar mount no sistema de arquivos especial de processos. Assim, dentro do container não vai mais enxergar os pids de verdade do lado de fora. Mas aqui tá mais completo porque vai dar mount pra &ldquo;/dev&rdquo;, pros pseudo terminais em &ldquo;/dev/pts&rdquo;, pra memória compartilhada que é &ldquo;/dev/shm&rdquo;, as filas de mensagem pra comunicação entre processos em &ldquo;/dev/mqueue&rdquo;, o &ldquo;/sys&rdquo; que espelha funcionalidades e informações da kernel e finalmente o mount de cgroup que mostrei no começo do video. Tudo isolado dos de verdade do lado de fora do container. Estamos deixando esse jarro o mais realista possível pro processo dentro.</p>
<p>E por fim quais namespaces vou isolar também que são os mesmos do unshare. Pid sabemos que esconde os processos, mas temos network pra rede virtual, ipc que é a fila pra comunicação interprocessos, uts que permite mudar o hostname interno e mount que permite criar mounts diferentes do seu sistema.</p>
<p>Com tudo configurado, podemos executar, a partir de rootfs, <code>sudo runc start hello</code>. Hello você pode substituir por um nome qualquer pra esse container, contanto que seja um nome único, daí podemos criar quantos containers quisermos. Como aconteceu antes quando rodamos unshare com chroot, caímos no bash dentro do jarro, com todos os seus sentidos sequestrados. RunC está mentindo pro bash, vamos ver?</p>
<p>Se digitarmos <code>id</code> estamos como root. Se executarmos <code>ls /boot</code>, tá vazio, estamos dentro do diretório rootfs como nova raíz de arquivos. Melhor ainda, se digitarmos <code>mount</code>, temos só os pontos de montagem que definimos no <code>config.json</code>. Ele não enxerga nenhum dos pontos de montagem de verdade do meu sistema. E se digitarmos <code>ps aux</code>, como antes, só enxergamos o bash que está rodando. É como se fosse uma máquina de verdade vazia. Estamos dentro da Matrix, mas repetindo, isso não é uma máquina virtual. Container, na prática é outro nome dado a um processo em execução com menos acessos que um processo normal, mas no fundo é só isso: a execução de um processo.</p>
<p>O que eu falei no começo do episódio? Vou repetir: Se fosse fazer um TLDR, uma única frase que resume a diferença, é até simples: uma máquina virtual roda um novo kernel em qualquer arquitetura, um container reaproveita o mesmo kernel, nas mesma arquitetura. É essa a diferença. Mas agora vamos ver na prática. Estou repetindo o tempo todo que &ldquo;/boot&rdquo; está vazio. Não tem outra kernel, estamos compartilhando a mesma kernel. Hypervisors compartilham o hardware de verdade, como CPU, GPU e dispositivos pra máquinas virtuais que rodam qualquer outro sistema operacional, qualquer nova kernel isolados um dos outros. Posso rodar Windows do lado de BSD. Mas um container compartilha o mesmo kernel que existe já carregado. Ele não pode rodar sistemas operacionais diferentes, só outra distro Linux.</p>
<p>Vamos voltar pro que eu disse no começo: como Windows e MacOS fazem pra ter containers de Docker de Linux? Eles sim, primeiro precisam usar o hypervisor deles, o Hyper-V ou o Hypervisor.framework pra bootar um Linux de verdade e em cima desse Linux rodar o RunC pra conseguir ter containers de Linux. Por isso Docker é sempre mais pesado em cima de Windows do que direto num Linux. Numa distro Linux não tem a necessidade de rodar um hypervisor antes. Entenderam essa diferença? É muito importante, porque sua performance e uso de recursos como RAM sempre vai ser pior no Docker pra Windows ou pra Mac. Sempre, não tem exceção.</p>
<p>Mas eu falei Docker e fiquei mostrando RunC, qualé? É porque depois que a OCI foi formada, o Docker, Podman, Kubernetes, todos que usam containers, provavelmente usam RunC por baixo, que é a infraestrutura que usa cgroups, unshare e outras coisas que vem em todo Linux. Mas se tem RunC então pra que precisamos de Docker?</p>
<p>A parte importante do Docker não é mais a capacidade de executar containers. Hoje em dia isso vem nativo na kernel do Linux através do RunC que mostrei. O grande lance de Docker é o ecossistema de imagens, o DockerHub. Como é o tutorial de hello world de Docker logo depois que instala? <code>docker run hello-world</code>, vamos executar no terminal. Ele imprime esse texto e termina. O que aconteceu?</p>
<p>A ferramenta começa checando no registry local se tem uma imagem com nome &ldquo;hello-world&rdquo;. Como não tinha, deu &ldquo;pull&rdquo;, como se fosse o comando <code>git clone</code> ou mais especificamente, <code>git pull</code>. Já explico porquê, mas na prática baixou o binário do site hub.docker.com. Podemos ir no site deles e ver o registro dessa imagem lá, tão vendo? Na dúvida ele baixa da tag &ldquo;latest&rdquo;. É o equivalente à branch &ldquo;main&rdquo; ou a antiga &ldquo;master&rdquo;.</p>
<p>Essa imagem baixada é que nem aquele nosso diretório &ldquo;/tmp/rootfs&rdquo; que eu tinha baixado o tarball, onde demos chroot. Com essa imagem o runC sobe um container. Pra ver o container que executou basta rodar o comando <code>docker container ls --all</code>. Sem essa opção a lista seria vazia porque o container executa e termina. Com essa opção &ldquo;all&rdquo; ele mostra mesmo os terminados. Lembra no comando &ldquo;runc&rdquo; que dei o nome de &ldquo;hello&rdquo; pro container? No comando de <code>docker run</code> poderia passar a opção &ldquo;&ndash;name&rdquo; mas como não fiz isso ele deu um nome aleatório pro container.</p>
<p>Agora uma coisa que a maioria que já usou Docker nunca deve ter feito. Vamos executar o comando <code>docker export NOME_CONTAINER &gt; rootfs.tar</code>. Sacaram? Podemos criar um diretório novo &ldquo;mkdir -p /tmp/rootfs2&rdquo; e fazer <code>tar xvf rootfs.tar --directory /tmp/rootfs2</code>. Agora damos &ldquo;cd&rdquo; pra dentro e não tem quase nada lá, alguns diretórios pra fazer mount de procs e outras coisas e esse executável &ldquo;hello&rdquo; que podemos executar diretamente com &ldquo;./hello&rdquo; e olha só, a mesma saída que vimos antes. É isso que o <code>docker run</code> executou e é assim que podemos pegar o conteúdo de uma imagem de Docker. Uma imagem de Docker não é muito diferente de um mero tarball.</p>
<p>Vamos entender melhor essa tal imagem. Pra começar, de volta no site do DockerHub vamos escolher a imagem do banco de dados Postgres. Agora vamos abrir do lado outro navegador com o arquivo Dockerfile no Github do repositório docker-library. Se clicarmos na tag &ldquo;latest&rdquo; podemos ver o que ele chama de layers. Sem ir linha a linha, só de bater o olho, conseguem ver onde um lado bate com o outro? Por exemplo, no Dockerfile tem esse comando &ldquo;ENV GOSU_VERSION&rdquo;. O comando ENV de Dockerfile serve pra configurar uma variável de ambiente, equivalente ao comando <code>export</code> num Bash. Todos esses &ldquo;/bin/sh&rdquo; são esses comandos RUN e assim por diante.</p>
<p>O que é um Dockerfile? É como se fosse um script de comandos que diz passo a passo como montar a tal imagem de Postgres. Voltando pra primeira linha, começa com o comando FROM. Não precisamos começar uma imagem do zero, podemos começar com uma que já existe e construir por cima. É como se fizéssemos <code>git clone</code> de um projeto e começássemos a adicionar coisas e ir fazendo <code>git commit</code>.</p>
<p>A grosso modo, cada comando do Dockerfile, esses em letra maiúscula no começo da linha, vão gerar um dos layers que vemos aqui no site do DockerHub. Com exceção de layers de configurar variável de ambiente, que é irrisório, a maioria dos comandos modifica ou baixa arquivos e por isso ocupa espaço. É que nem commit de Git. Por isso é comum num Dockerfile ter comandos RUN que executam vários comandos de shell concatenados, que nem logo no começo que fazemos <code>apt update; apt-get install ...; rm -Rf</code>. Por que?</p>
<p>Se o <code>rm -Rf</code> estivesse separado em outro comando RUN embaixo, o comando do <code>apt-get install</code> ia baixar e instalar pacotes, como em qualquer Debian ou Ubuntu que você usa. Digamos que instalou 50 megabytes, tô chutando. Mas vem um lixo opcional que não preciso, daí no comando seguinte rodo <code>rm</code> e apaga 10 megabytes. Mas a imagem vai ter um layer com 50 megabytes. Porque o <code>rm</code> ficou num layer separado.</p>
<p>Em vez disso eu baixo os 50 mega, na sequência já apago, e aí fecho a camada com 40 megabytes. Por isso fica tudo dentro do mesmo comando RUN, como se fosse num mesmo commit, entenderam? Quanto menos comando RUN no Dockerfile, melhor. Olha esse outro comando RUN embaixo, um monte de linhas um atrás do outro, e o resultado final de executar tudo é que vai no layer da imagem.</p>
<p>Essa é a importância da OCI que o Docker fundou. Ela define esse formato chamado imagem OCI. Dentro dessa imagem, que é um binariozão parecido com um zipão, tem alguns arquivos e diretórios obrigatórios. Como um <code>index.json</code> que tem o nome, ID, tamanho, lista de layers e histórico. Tem o <code>oci-config.json</code> que é o equivalente ao <code>config.json</code> do RunC que mostrei antes e, o mais importante, o diretório &ldquo;blobs&rdquo; que é parecido com diretório &ldquo;.git/objects&rdquo; de um repositório Git.</p>
<p>Ah, vocês não assistiram meu video de Git né? Lá eu expliquei que o importante é só o que tem no diretório &ldquo;.git&rdquo;, todo o resto pode jogar fora porque dá pra recriar tudo com o que tem aqui. Se listarmos <code>ls .git/objects</code> vem esse monte de subdiretórios. Vamos listar um deles e encontramos um arquivo com nome que é um SHA1. Esse é o SHA1 do commit de git. Os blobs da imagem de Docker é similar só que otimizados pra layers binários numa árvore, enquanto Git é otimizado pra árvore de commits de texto.</p>
<p>Lembram do formato QCOW2 que mostrei no episódio anterior? O QEMU Copy-on-Write versão 2 que é a imagem de HD virtual que o QEMU usa e onde instalamos o Windows virtualizado? É similar. O formato de imagem OCI também suporta copy-on-write. Que é o mesmo recurso que tem disponível em sistemas de arquivos modernos como o BTRFS. Um snapshot de BTRFS é similar com snapshot de QCOW2 que é parecido com layers de imagem OCI. COW permite criar layers só realmente com a diferença dos blocos que mudaram num arquivo.</p>
<p>Sabe diff que fazemos em Git e ele grava no commit só as linhas que foram adicionadas e as que foram removidas e não o arquivo todo? COW é como se fosse diff de arquivo binário. E lembra que eu venho repetindo em todos os videos da mini-série de armazenamento como arquivos são nada mais do que um conjunto de blocos binários organizados numa árvore? Se um arquivo tem 100 megabytes, divididos em 100 blocos de 1 megabyte, digamos que só 10 blocos desse arquivos foram alterados. Na próxima camada, ou commit, não precisa repetir os 100 megabytes inteiros do arquivo, só os 10 blocos modificados ocupando 10 megabytes. É por isso que imagens de Docker não são pesados.</p>
<p>Pra construir uma imagem nova só precisamos ter o arquivo Dockerfile como esse do Postgres e rodar o comando <code>docker build</code>. Se resolvermos mudar o Dockerfile a partir da metade, e repetirmos o comando <code>docker build</code>. Todas as camadas geradas pela primeira metade de comandos do arquivo permanecem as mesmas, jogamos fora as últimas camadas e ele executa os comandos a partir da metade. Por isso que quanto mais pro começo de um Dockerfile você mexer, mais o <code>docker build</code> vai ter que trabalhar. Se mexer só na última linha, só ela que vai ter que rodar.</p>
<p>Então uma imagem de OCI ou Docker é um conjunto de camadas binárias, como commits de um git. Pra montar dentro do container o Docker usa um sistema de arquivos chamado overlay2 que é um Union Filesystem. Union significa que unifica as diversas camadas num sistema de arquivos único. O container em si não sabe que tem essas camadas todas por baixo. Isso só importa pra construção da imagem, não pra rodar.</p>
<p>Eu menti agora pouco pra vocês. RunC é a infraestrutura de containers no Linux que sistemas como Kubernetes e Podman usam, mas o Docker propriamente dito na verdade usa outra coisa, que é o &ldquo;containerd&rdquo;. Pense assim, RunC tem menos funcionalidades, é mais simples, mais leve e pode rodar em userland, ou &ldquo;rootless&rdquo;, sem permissões de root ou daemons. Já containerd tem mais funcionalidades e foi feito pra funcionar com um daemon que sobe como root.</p>
<p>Por isso que quando se instala Docker, todo tutorial vai te falar pra executar &ldquo;systemctl start docker&rdquo; e habilitar o serviço pra subir no boot com &ldquo;systemctl enable docker&rdquo;. E isso pode se tornar um problema de segurança. Se sair pesquisando sobre buracos no Docker, vai achar várias formas de exploit dependendo de como o container foi configurado pra subir. O caso mais besta é subir um container com um comando como esse: <code>docker run -tid -v /:/mnt/ alpine</code>. Estão enxergando o erro?</p>
<p>O problema é essa opção &ldquo;-v&rdquo; que é pra mapear volumes. Lembra que quando estamos dentro da imagem o processo dentro não enxerga nada do sistema de arquivos fora? Pois bem, no mundo real o container é mais útil se ele tem &ldquo;algum&rdquo; acesso ao mundo externo. É melhor ser o Neo, saber que o mundo real existe, e ter superpoderes dentro da Matrix e um deles é conseguir enxergar arquivos do sistema de verdade.</p>
<p>Essa opção &ldquo;-v&rdquo; monta um diretório do lado de fora pra um diretório do lado de dentro, no caso do exemplo vai montar a raíz de verdade do meu HD pra um diretório &ldquo;/mnt&rdquo; no container. Mas isso é uma burrice, nunca faça um ponto de montagem tão amplo. Como o daemon que roda o container, o containerd que o Docker usa, sobe como root, ele tem permissão pra editar qualquer arquivo do sistema de verdade a partir desse ponto de montagem.</p>
<p>Daí é game-over, posso fazer o que quiser, inclusive editar o arquivo &ldquo;/etc/passwd&rdquo; pra injetar um novo usuário com permissão de root na máquina de verdade e invadir ela. Eu acho que ninguém em sã consciência jamais subiria um container de Docker num servidor de produção apontando pra raíz do HD do servidor. Só um sem noção faria isso. Seria certificado de estupidez.</p>
<p>É uma das razões de porque muitos de nós preferimos usar Podman em vez de Docker. Podman foi feito pra ter todos os mesmos comandos, mesmas opções e mesma sintaxe de Docker. Então comandos como <code>docker build</code> ou <code>docker run -it bla bla</code> posso rodar com <code>podman build</code> ou <code>podman run bla bla</code>. Se quiser posso fazer um alias no shell chamado docker que aponta pro podman e tudo vai funcionar praticamente igual. Podman usa o mesmo arquivo Dockerfile com a mesma sintaxe, a única diferença é aquele comando FROM no começo do arquivo.</p>
<p>Por exemplo, no caso do postgres começa com &ldquo;FROM debian&rdquo;. Pro Podman suportar precisa reescrever como &ldquo;FROM docker.io/debian&rdquo;. Só especificar o site da imagem porque ele suporta outros repositórios em vez de só o dockerhub. Outra diferença é que Docker é construído em cima do containerd, mas Podman usa o RunC como mostrei antes. Diferente do containerd não precisa ter um daemon rodando como root. Na verdade RunC, e por consequência o Podman, tem modo &ldquo;rootless&rdquo;. Ou seja, nem preciso iniciar com &ldquo;sudo&rdquo;. Portanto o container consegue rodar sem privilégio nenhum e por padrão é mais seguro. Veja documentação deles de como funciona esse modo porque não vou detalhar hoje.</p>
<p>E falando em segurança e permissões, pra concluir, faltou falar daquele bloco no arquivo do <code>config.json</code> que falei pra pular, lembram? Capacidade ou Linux Capabilities. Toda vez que se fala em permissões em Linux, automaticamente pensamos em coisas como os comandos &ldquo;chown&rdquo; pra mudar a propriedade de algum arquivo ou &ldquo;chmod&rdquo; pra mudar um arquivo pra ser só de leitura por exemplo. Ou, usar ou não usar &ldquo;sudo&rdquo; pra executar alguma coisa. E é isso, acabou.</p>
<p>Só que não. De cara vocês já aprenderam sobre cgroups e como eles limitam os recursos do processo, ou namespaces e como o sistema mente pros processos, independente se tem permissão de root ou não. E tem muito mais em cima. Linux capabilities é a habilidade da kernel de dar ou remover capacidades com o comando <code>setcap</code>.</p>
<p>Vou tentar não descer em detalhe desnecessário demais, mas num terminal o primeiro comando pra entender capabilities é o <code>capsh</code>. Execute num terminal e vemos dois campos no começo: Current e Bounding Set. Current vazio significa que um usuário normal começa sem nenhuma capacidade por default, mas tem várias capacidades que podem ser habilitadas, que é essa lista enorme chamada de Bounding Set.</p>
<p>Existem dois tipos de capacidades em Linux: capacidades efetivas e capacidades permitidas. Capacidades efetivas determinam quais operações um processo pode performar, e capacidades permitidas são quais podem ser habilitadas ou desabilitadas. Bounding Set é um sub-conjunto de capacidades permitidas. E quem controla esse set é o comando <code>prctl</code>, depois pesquise porque não quero entrar em detalhes sobre granularidade de segurança de Linux hoje.</p>
<p>Próximo comando vai ser <code>grep Cap /proc/$BASHPID/status</code>. Essa variável BASHPID é o pid do bash. Aí aparece essa lista bizarra, mas é fácil. O primeiro são inherited capabilities, ou capacidades herdadas no caso zero. Segundo são capacidades permitidas que eu falei, zero. Terceiro capacidades efetivas que também expliquei, zero. Finalmente capacidades bounding set.</p>
<p>Com o comando <code>capsh --decode=</code> esse hexadecimal e a lista é, obviamente, a mesma que vimos no comando <code>capsh --print</code> antes. Isso fica guardado no objeto proc do processo. E esse hexadecimal é um bit set. Um conjunto de bits. Se você não conhece a técnica de guardar propriedade num bit dentro de um inteiro e pegar os valores com uma máscara binária, um bit mask, pesquise. É essencial pra economizar bits em coisas como protocolos de rede.</p>
<p>De qualquer forma veja de novo aquele bloco de capabilities em <code>config.json</code>, agora você sabe o que é efetivo ou permitido. Esses cap_alguma coisa tem que ler no manual de capabilities. Só digitar <code>man capabilities</code> no terminal. Pronto, cap_audit_write é capacidade de escrever no log do sistema, cap_kill é capacidade de enviar sinais tipo SIGTERM ou SIGKILL e cap_net_bind_service é pra conseguir dar bind em portas privilegiadas abaixo de 1024, como a porta 80 sem precisar fazer sudo.</p>
<p>Com isso vocês tem pelo menos noção agora do que são containers do ponto de vista da kernel do Linux. Eles não são máquinas virtuais. É simplesmente um jarro que a kernel cria ao redor de um processo que roda nativamente pra restringir o que ele pode ver ou acessar, fazendo esse processo acreditar que está sozinho em outra máquina. A kernel usa cgroups e capabilities pra limitar os recursos e capacidades que esse processo tem acesso e usa namespaces pra limitar os sentidos desse processo, fazendo enxergar coisas como pids, mounts, rede, e tudo mais que são falsos, dentro da Matrix.</p>
<p>Docker é implementado em cima de containerd que é um serviço que precisa rodar como root. Podman e Kubernetes são implementados em cima de RunC que é mais leve e sem necessidade de um daemon auxiliar. Falando nisso, pode ser que Kubernetes ou Terraform confundam pra quem nunca mexeu. Qual a diferença disso pra Docker ou Docker Compose?</p>
<p>Vamos lá. Dockerfile vocês já entenderam, servem pra construir imagens OCI. Docker Compose é hiper útil, tem o Podman Compose que é exatamente a mesma coisa pra quem escolher usar Podman. Ambos começam com um arquivo <code>docker-compose.yml</code>. Deixa eu dar um exemplo besta aqui. Digamos que está trabalhando num projeto em Node. Comece criando um Dockerfile que constrói a imagem do seu projeto Node com seguinte conteúdo:</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-docker" data-lang="docker"><span class="line"><span class="cl"><span class="c"># Use the latest LTS (long-term support) version of Node.js as the base image</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">FROM</span><span class="w"> </span><span class="s">docker.io/node:lts</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="c"># Set the working directory to the project root</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">WORKDIR</span><span class="w"> </span><span class="s">/app</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="c"># Copy the package.json and package-lock.json files to the working directory</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">COPY</span> package*.json ./<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="c"># Install the dependencies</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">RUN</span> npm install<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="c"># Copy the rest of the project files to the working directory</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">COPY</span> . .<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="c"># Expose the app&#39;s port</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">EXPOSE</span><span class="w"> </span><span class="s">3000</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="c"># Run the app when the container is started</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">CMD</span> <span class="p">[</span> <span class="s2">&#34;npm&#34;</span><span class="p">,</span> <span class="s2">&#34;start&#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>Começamos a partir da imagem do Node versão lts em vez do mais novo que é latest, lembram? Projetos em produção precisam de estabilidade e não da versão mais nova que pode ser a mais bugada. Sempre use a mesma versão de tudo na sua máquina de desenvolvimento e nos servidores de produção e isso começa pela image de docker.</p>
<p>Dentro do container, o código fonte do seu projeto vai ficar nesse WORKDIR, em /app.  Daí copiamos o arquivo package.json pra dentro do container e rodamos <code>npm install</code> pra criar o <code>node_modules</code> e baixar todas as dependências. Terminamos de copiar todos o códigos fonte do projeto com esse comando COPY. Além disso esse container vai expôr pra fora a porta 3000 que é onde o servidor de node vai dar bind quando rodar o comando que está em CMD, no caso <code>npm start</code>.</p>
<p>Não esquecer de também criar um arquivo <code>.dockerignore</code> praquele comando COPY não copiar o <code>node_modules</code> que está no seu ambiente de desenvolvimento. Dentro do container já rodamos <code>npm install</code> então não precisa copiar tudo de novo. É importante entender onde ficam os arquivos em cada ambiente.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>node_modules
*.log</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>Só com isso já dá pra fazer <code>docker build . -t seuusuario/seuprojeto</code> E já dá pra rodar com <code>docker run -p 3000:3000 seuusuario/seuprojeto</code>. Digamos que seu projeto precisa ter Redis e Postgres instalado. Isso costuma ser um problema, porque se você for um programador bem ocupado e experiente, na sua máquina deve ter diversos projetos. E alguns mais antigos podem depender de Postgres mais antigo, outros de Postgres mais novo. Mas se instalar na sua máquina via o gerenciador de pacotes da sua distro, sempre vai estar com a versão mais nova só.</p>
<p>Pra ter várias versões de node, muita gente usa o NVM ou o meu favorito que é o ASDF, mas e pra ter várias versões de postgres, de redis, de mysql, de kafka e todos esses outros componentes? E é pra isso que entra o Docker Compose. Este é um exemplo de arquivo <code>docker-compose.yml</code> que também ficaria na raíz do seu projeto:</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">version</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;3&#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">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">app</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">build</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">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;3000:3000&#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">DATABASE_HOST=db</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">DATABASE_USER=app_user</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">DATABASE_PASSWORD=app_password</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">REDIS_HOST=redis</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">db</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">redis</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">db</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">docker.io/postgres:latest</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">POSTGRES_USER=app_user</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">POSTGRES_PASSWORD=app_password</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">redis</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">docker.io/redis:latest</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>Docker Compose orquestra e sobe vários containers Docker que ele chama de services. O primeiro é sua aplicação.  Ele começa com o atributo build sendo ponto, que é o diretório atual. Vai subir a imagem que seria construída com o Dockerfile que tá no seu projeto nesse diretório. Daí configuramos o projeto com variáveis de ambiente, por exemplo dizemos que DATABASE_HOST se chama &ldquo;db&rdquo;. Normalmente você colocaria &ldquo;localhost&rdquo;, mas cada serviço é como se estivesse rodando numa máquina separada graças ao Namespace UTS que permite que cada um tenha seu hostname separado, por isso o service de Postgres vai se chamar &ldquo;db&rdquo;.</p>
<p>Logo embaixo começa o bloco &ldquo;db&rdquo; onde dizemos que é pra baixar a imagem <code>docker.io/postgres:latest</code> e configuramos o usuário e senha, que é o mesmo que o serviço de node da sua aplicação vai usar. Mesma coisa no terceiro service que chamamos de &ldquo;redis&rdquo; e baixa imagem docker.io/redis. Latest é a versão mais nova, mas num projeto de verdade é bom fixar uma versão em específico pra não confundir.</p>
<p>Veja também que o serviço app, que é sua aplicação, tem um bloco dizendo &ldquo;depends_on&rdquo;, ou seja, o docker compose só vai subir sua aplicação depois que os serviços de postgres e redis carregarem. E pronto. Mesmo que você seja um programador que acabou de entrar no projeto, depois de fazer <code>git clone</code> do repositório, é só rodar <code>docker compose up</code> e pronto! Vai subir todos os serviços e ao final pode ir no navegador a apontar pra localhost:3000 e que vai estar tudo de pé e funcionando. O ideal é manter essa configuração atualizada, porque o que mais acontece é ficar ajustando só na sua máquina e não subir a atualização pro repositório, daí pessoas novas entram na equipe e baixam uma configuração desatualizada e dá vários problemas. Mantenha tudo atualizado.</p>
<p>De bônus, lembra do video de otimização web onde explico sobre coisas como NGINX? Podemos simular o que eu falei naquele video com docker compose. Não é uma coisa que faríamos num projeto de verdade mas só pra ilustrar como aquele diagrama conceitual viraria código de 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="nt">version</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;3&#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">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">app_1</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">build</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">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;3000&#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">DATABASE_HOST=db</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">DATABASE_USER=app_user</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">DATABASE_PASSWORD=app_password</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">REDIS_HOST=redis</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">db</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">redis</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">app_2</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">build</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">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;3000&#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">DATABASE_HOST=db</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">DATABASE_USER=app_user</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">DATABASE_PASSWORD=app_password</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">REDIS_HOST=redis</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">db</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">redis</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">db</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">docker.io/postgres:latest</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">POSTGRES_USER=app_user</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">POSTGRES_PASSWORD=app_password</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">redis</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">docker.io/redis:latest</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">load_balancer</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">docker.io/nginx:latest</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;8080:80&#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">./nginx.conf:/etc/nginx/nginx.conf</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">app_1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">app_2</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>Os serviços app, redis e postgres são iguaizinhos o do exemplo anterior mas agora adicionei um &ldquo;load balancer&rdquo; que puxa a imagem do nginx. Lembram que expliquei naquele video que ele é quem realmente responde na porta 80? E pro nginx funcionar precisa ter um arquivo <code>nginx.conf</code> no seu projeto que instruímos o docker compose copiar pra dentro do container de nginx. Esse arquivo diz o seguinte:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>user                 nginx;
worker_processes     auto;

events {
    worker_connections  65535;
    multi_accept        on;
}

http {
    charset              utf-8;
    sendfile             on;
    tcp_nopush           on;
    tcp_nodelay          on;
    client_max_body_size 4m;

    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    map $remote_addr $proxy_forwarded_elem {

        # IPv4 addresses can be sent as-is
        ~^[0-9.]&#43;$        &#34;for=$remote_addr&#34;;

        # IPv6 addresses need to be bracketed and quoted
        ~^[0-9A-Fa-f:.]&#43;$ &#34;for=\&#34;[$remote_addr]\&#34;&#34;;

        # Unix domain socket names cannot be represented in RFC 7239 syntax
        default           &#34;for=unknown&#34;;
    }

    map $http_forwarded $proxy_add_forwarded {

        # If the incoming Forwarded header is syntactically valid, append to it
        &#34;~^(,[ \\t]*)*([!#$%&amp;&#39;*&#43;.^_`|~0-9A-Za-z-]&#43;=([!#$%&amp;&#39;*&#43;.^_`|~0-9A-Za-z-]&#43;|\&#34;([\\t \\x21\\x23-\\x5B\\x5D-\\x7E\\x80-\\xFF]|\\\\[\\t \\x21-\\x7E\\x80-\\xFF])*\&#34;))?(;([!#$%&amp;&#39;*&#43;.^_`|~0-9A-Za-z-]&#43;=([!#$%&amp;&#39;*&#43;.^_`|~0-9A-Za-z-]&#43;|\&#34;([\\t \\x21\\x23-\\x5B\\x5D-\\x7E\\x80-\\xFF]|\\\\[\\t \\x21-\\x7E\\x80-\\xFF])*\&#34;))?)*([ \\t]*,([ \\t]*([!#$%&amp;&#39;*&#43;.^_`|~0-9A-Za-z-]&#43;=([!#$%&amp;&#39;*&#43;.^_`|~0-9A-Za-z-]&#43;|\&#34;([\\t \\x21\\x23-\\x5B\\x5D-\\x7E\\x80-\\xFF]|\\\\[\\t \\x21-\\x7E\\x80-\\xFF])*\&#34;))?(;([!#$%&amp;&#39;*&#43;.^_`|~0-9A-Za-z-]&#43;=([!#$%&amp;&#39;*&#43;.^_`|~0-9A-Za-z-]&#43;|\&#34;([\\t \\x21\\x23-\\x5B\\x5D-\\x7E\\x80-\\xFF]|\\\\[\\t \\x21-\\x7E\\x80-\\xFF])*\&#34;))?)*)?)*$&#34; &#34;$http_forwarded, $proxy_forwarded_elem&#34;;

        # Otherwise, replace it
        default &#34;$proxy_forwarded_elem&#34;;
    }

    upstream www {
        least_conn;
        server app_1:3000;
        server app_2:3000;
    }
    
    server {
    
        listen 80;
    
        location / {
            proxy_pass http://www;

            proxy_http_version                 1.1;
            proxy_cache_bypass                 $http_upgrade;
            
            # Proxy headers
            proxy_set_header Upgrade           $http_upgrade;
            #proxy_set_header Connection        $connection_upgrade;
            proxy_set_header Host              $host;
            proxy_set_header X-Real-IP         $remote_addr;
            proxy_set_header Forwarded         $proxy_add_forwarded;
            proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_set_header X-Forwarded-Host  $host;
            proxy_set_header X-Forwarded-Port  $server_port;
        }
    
    }
}</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>O bloco server, tem a configuração do servidor que vai escutar na porta 80 e fazer o tal proxy reverso pra <a href="http://app"target="_blank" rel="noopener">http://app</a> e app está configurado acima, são os hostnames app_1 e app_2 na porta 3000 de cada container. Lembrem que containers tem rede virtual separada, namespace de portas separado. Então cada um, quando carregar, se penduram na porta 3000 e o nginx vai fazer round robin entre esses dois servidores.</p>
<p>app_1 e app_2 herdam do serviço app que é sua aplicação em Node. Aqui estamos subindo duas instâncias pra ter balanceamento de carga entre os dois. Note que o serviço load_balancer depende desse app_1 e app_2 estarem de pé, e esses dois, por herdarem de app, dependem de redis e postgres estarem de pé e é assim que o docker compose sabe quem tem que subir primeiro pra tudo estar funcionando. E pronto! Isso é uma pequena demonstração de uma aplicação node com dois processos sendo balanceados por um nginx.</p>
<p>Docker Compose foi feito pra infraestrutura bem simples, em particular pra rodar na sua máquina de desenvolvimento e facilitar a vida de programadores. Ele até serve pra fazer deploy em servidores pequenos, tipo numa Digital Ocean da vida. Antigamente tinha um projeto chamado Docker Machine que servia pra conversar com provedores de servidores, mas acho que ninguém usa mais.</p>
<p>Veja como nesse arquivo, pra subir duas instâncias da aplicação, precisou manualmente digitar app_1, app_2, colocar eles no depends_on do load_balancer. Imagina se precisasse subir 10 instâncias. Ou se precisasse configurar replica de Postgres. Ou se precisasse configurar uma fila e vários servidores separados de jobs assíncronos como falei no video do ChatGPT. Esse arquivo não escala, ia ficar complicado ainda mais porque também não sabe só aumentar ou diminuir só os containers de web. Ele não tem noção do que está rodando nos servidores já e só dar manutenção.</p>
<p>É pra isso que entra Kubernetes e Terraform. Kubernetes é um conjunto de tecnologias de infraestrutura elástica. A parte que se aproxima de Docker é o que ele chama de pods, que são containers que rodam com RunC. Mas o Kubernetes faz bem mais, configura VPC que são redes privadas virtuais. Eu posso criar uma rede isolada só pros containers web, outra rede isolada separada só pra servidores de jobs, por exemplo. Sabe lidar com balanceador de carga, o Ingress, adicionar e remover pods dinamicamente, e sabe que está ou na AWS ou no Google Cloud ou na Azure e com isso consigo adicionar serviços exclusivos como o RDS da AWS como banco de dados ou o Cloud SQL do Google Cloud. É bem mais flexível e, por isso, mais complicado.</p>
<p>Entenda, um IaaS ou Infraestrutura como Serviço, como a AWS, me fornece os componentes básicos de infraestrutura. O Kubernetes é melhor pra lidar mais com os pods, os containers, depois que já tem a infra de máquinas e rede pronta. Pra subir essa infra inicial, podemos ou fazer na mão via interface web, o que não recomendamos, ou orquestrar com Terraform.</p>
<p>Por exemplo, máquinas virtuais que o AWS chama de EC2. Esses servidores tem tamanhos diferentes, por exemplo t2.nano que só tem 1 vcpu ou t2.xlarge de 4 vcpus ou a1.metal com 16 cpus, ou m4.16xlarge de 64 vcpus, cada um com um preço diferente por hora. Tem planos diferentes pra S3, pra RDS, pra SQS, e assim vai. E isso só na AWS. Pra organizar tudo, usamos Terraform. Deixa mostrar só alguns trechos de um exemplo de arquivo de configuração, que pode ficar super comprido.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code># Set up the provider for the desired cloud provider
provider &#34;aws&#34; {
  region = &#34;us-east-1&#34;
}

# Create a VPC to host the resources
resource &#34;aws_vpc&#34; &#34;app_vpc&#34; {
  cidr_block = &#34;10.0.0.0/16&#34;
}

# Create a public subnet in the VPC
resource &#34;aws_subnet&#34; &#34;public_subnet&#34; {
  vpc_id            = aws_vpc.app_vpc.id
  cidr_block        = &#34;10.0.1.0/24&#34;
  availability_zone = &#34;us-east-1a&#34;
}

# Create an Internet Gateway and attach it to the VPC
resource &#34;aws_internet_gateway&#34; &#34;app_igw&#34; {
  vpc_id = aws_vpc.app_vpc.id
}

# Create a route table and a route for the public subnet
resource &#34;aws_route_table&#34; &#34;public_route_table&#34; {
  vpc_id = aws_vpc.app_vpc.id
  route {
    cidr_block = &#34;0.0.0.0/0&#34;
    gateway_id = aws_internet_gateway.app_igw.id
  }
}

# Associate the public subnet with the route table
resource &#34;aws_route_table_association&#34; &#34;public_subnet_association&#34; {
  subnet_id      = aws_subnet.public_subnet.id
  route_table_id = aws_route_table.public_route_table.id
}

# Create an EC2 instance to host the Node.js application
resource &#34;aws_instance&#34; &#34;app_server&#34; {
  ami           = &#34;ami-12345678&#34;
  instance_type = &#34;t2.micro&#34;
  vpc_id        = aws_vpc.app_vpc.id
  subnet_id     = aws_subnet.public_subnet.id
  key_name      = &#34;app-key&#34;
  user_data     = &#34;${file(&#34;user_data.sh&#34;)}&#34;
}

# Create a security group for the EC2 instance
resource &#34;aws_security_group&#34; &#34;app_server_sg&#34; {
  name        = &#34;app_server_sg&#34;
  description = &#34;Security group for the application server&#34;
  vpc_id      = aws_vpc.app_vpc.id
  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = &#34;tcp&#34;
    cidr_blocks = [&#34;0.0.0.0/0&#34;]
  }
}

# Create an RDS instance for Postgres
resource &#34;aws_db_instance&#34; &#34;app_db&#34; {
  engine               = &#34;postgres&#34;
  engine_version       = &#34;11.5&#34;
  instance_class       = &#34;db.t2.micro&#34;
  name                 = &#34;app_db&#34;
  username             = &#34;app_user&#34;
  password             = &#34;app_password&#34;
  vpc_security_group_ids = [aws_security_group.app_db_sg.id]
  vpc_id               = aws_vpc.app_vpc.id
  subnet_group_name    = &#34;app_db_subnet_group&#34;
}

# Create a security group for the RDS instance
resource &#34;aws_security_group&#34; &#34;app_db_sg&#34; {
  name        = &#34;app_db_sg&#34;
  description = &#34;Security group for the application database&#34;
  vpc_id      = aws_vpc.app_vpc.id
  ingress {
    from_port   = 5432
    to_port     = 5432
    protocol    = &#34;tcp&#34;
    cidr_blocks = [&#34;10.0.1.0/24&#34;]
  }
}

# Create a security group for Redis
resource &#34;aws_security_group&#34; &#34;app_redis_sg&#34; {
  name        = &#34;app_redis_sg&#34;
  description = &#34;Security group for Redis&#34;
  vpc_id      = aws_vpc.app_vpc.id
  ingress {
    from_port   = 6379
    to_port     = 6379
    protocol    = &#34;tcp&#34;
    cidr_blocks = [&#34;10.0.1.0/24&#34;]
  }
}

# Create an Elasticache cluster for Redis
resource &#34;aws_elasticache_cluster&#34; &#34;app_redis&#34; {
  engine            = &#34;redis&#34;
  cluster_id        = &#34;app_redis&#34;
  node_type         = &#34;cache.t2.micro&#34;
  num_cache_nodes   = 1
  security_group_ids = [aws_security_group.app_redis_sg.id]
  vpc_id            = aws_vpc.app_vpc.id
}

# Create an Nginx load balancer
resource &#34;aws_lb&#34; &#34;app_lb&#34; {
  name            = &#34;app_lb&#34;
  internal        = false
  security_groups = [aws_security_group.app_lb_sg.id]
  subnets         = [aws_subnet.public_subnet.id]
  load_balancer_type         = &#34;application&#34;
  idle_timeout               = 60
  enable_deletion_protection = false
}

# Create a security group for the Nginx load balancer
resource &#34;aws_security_group&#34; &#34;app_lb_sg&#34; {
  name        = &#34;app_lb_sg&#34;
  description = &#34;Security group for the application load balancer&#34;
  vpc_id      = aws_vpc.app_vpc.id
  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = &#34;tcp&#34;
    cidr_blocks = [&#34;0.0.0.0/0&#34;]
  }
}

# Create a target group for the Nginx load balancer
resource &#34;aws_lb_target_group&#34; &#34;app_tg&#34; {
  name     = &#34;app_tg&#34;
  port     = 80
  protocol = &#34;HTTP&#34;
  vpc_id   = aws_vpc.app_vpc.id
  target_type = &#34;instance&#34;
}

# Register the EC2 instance with the target group
resource &#34;aws_lb_target_group_attachment&#34; &#34;app_tg_attachment&#34; {
  target_group_arn = aws_lb_target_group.app_tg.arn
  target_id        = aws_instance.app_server.id
  port             = 80
}

# Create a listener for the Nginx load balancer
resource &#34;aws_lb_listener&#34; &#34;app_listener&#34; {
  load_balancer_arn = aws_lb.app_lb.arn
  port              = &#34;80&#34;
  protocol          = &#34;HTTP&#34;
  default_action {
    type             = &#34;forward&#34;
    target_group_arn = aws_lb_target_group.app_tg.arn
  }
}</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>Veja, o Terraform sabe que o provedor é AWS, e em qual região geográfica, no caso leste dos Estados Unidos, tipo Nova Iorque. Veja configuração de rede privada, subnets, gateway, tabela de rotas, e mais.</p>
<p>Daí contrato um servidor EC2, tamanho t2.micro, e configuro pra rede que tinha declarado antes. Embaixo configuramos grupo de segurança. Depois tenho configuração de banco de dados, depois de Redis que é o serviço que a AWS chama de Elasticache, depois tem o load balancer. E esse arquivo de exemplo é bem simplório e incompleto. Entre Kubernetes e Terraform, tem bastante detalhe pra realmente deployar uma aplicação em produção que esteja configurado de forma eficiente e com segurança.</p>
<p>Numa configuração de verdade, se tudo estiver direitinho declarado, seria só digitar <code>terraform init</code>  pra fazer download e instalar os plugins necessários. Daí <code>terraform plan</code> pra criar o plano do deployment pra esse provedor e a gente checar se tá tudo realmente certo. Coisa que já tá instalada ele não vai instalar de novo, vai só reconfigurar e instalar o que falta.  E no final fazer <code>terraform apply</code> pra realmente contratar os serviços, configurar e colocar nos lugares certos na infra.</p>
<p>Video de hoje não é sobre devops mas já que estou falando de containers e serviços, achei legal só mostrar um exemplo pra quem nunca viu. Mas o ponto é: compare uma configuração longa de Terraform com um simples Docker Compose. Nem chega perto, porque o objetivo das duas ferramentas é diferente. Um é pra orquestrar uma infra de verdade, a outra é pra rodar só na sua máquina pra facilitar desenvolvimento. Pra configurar uma AWS use Kubernetes e Terraform. Pra configurar serviços na sua máquina de desenvolvimento, use Docker Compose. Todo bom projeto deveria ter isso.</p>
<p>Pra finalizar, não vou mostrar mas queria só explicar outras tecnologias de containers que existem. Expliquei sobre imagens OCI de Docker, mas tem outro projeto que recomendo que pesquisem chamado OSTree. Lembra que falei que uma imagem meio que parece montado com commits binários de um Git? Existe de fato um projeto que é um Git de sistema de arquivos, o OSTree. A idéia é a seguinte: imagina que você tem 100 servidores numa AWS da vida, coisa grande. Imagina se sai atualização do Ubuntu Server que roda lá, o que você vai fazer? Mandar rodar <code>apt update; apt upgrade -y</code> e rezar pra não dar pau?</p>
<p>Na verdade não, vai montar imagens com ostree. O sistema de arquivos de todo servidor vai ser como um Git gigante. Daí sai atualização, você atualiza só em uma imagem. O OSTree nos servidores vai baixar a nova imagem e trocar a imagem antiga pela imagem nova. É tipo dar um git pull e trocar pro branch mais novo com um git checkout manja? É uma explicação grosseira, mas é bom saber que isso existe.</p>
<p>Outra coisa é sobre capacidades. Além de Linux capabilities que expliquei antes existem outra formas de limitar acesso a recursos da máquina, especialmente no contexto de segurança. Sabe em aplicativo Android ou iOS hoje em dia que pergunta se você quer dar acesso da camera pra um app ou acesso à sua localização e coisas assim? No mundo Linux isso é responsabilidade de componentes como Seccomp de Secure Computing Mode, que é outra funcionalidade da kernel do Linux pra restringir chamadas de sistema. Ele normalmente é usado em combinação com outros mecanismos de segurança, mas o seccomp em si oferece a infra na kernel pra aplicar filtros nos programas. Se alguém tentar fazer alguma coisa que não deveria, a kernel recusa.</p>
<p>Em cima disso temos projetos como AppArmor e SE-Linux. Olhando só por cima, eles se complementam. Ambos servem pra forçar políticas de segurança nos programas. O AppArmor permite especificar que recursos, como arquivos, dispositivos e sockets de rede um programa tem permissão pra usar. SE-Linux significa Security Enhanced Linux que também força políticas, por exemplo, pra acesso a hardware externo. É ele quem pode proibir um aplicativo de usar sua webcam.</p>
<p>Além disso, do jeito que falei, parece que todo container vai usar Namespaces de Linux com o comando unshare, mas existem outras formas de fazer containers, como o projeto Bubblewrap que é um container mais simples e usa outro recurso da kernel do Linux que são namespaces unprivileged e seccomp-bpf.</p>
<p>Mencionei ostree, seccomp, apparmor, se-linux e bubblewrap porque se você juntar todos eles num único projeto, começa a surgir com coisas como Flatpak. Sabe, aquele instalador de aplicativos? Tecnicamente é como se fosse um Docker ou Podman, mas feito pra funcionar melhor com aplicativos, especialmente aplicativos gráficos, com janela e que precisam acessar seus arquivos, USB e se comunicar com outros programas, precisando ter acesso a serviços como dbus. Tem gente que fica pensando em usar Docker pra instalar um Gimp ou LibreOffice. Não né, pra isso serve Flatpak.</p>
<p>Flatpak roda aplicativos como Spotify, Discord, Zoom, dentro de containers semi-isolados. Menos isolados que um container de Docker, mas um pouco mais seguros do que se fosse instalado direto via apt ou pacman. E o Snap de Ubuntu é similar, um gerenciador de containers. Em vez dos pacotes serem tipo zips como expliquei no video de Slackware, Flatpak e Snap baixam imagens, como imagens de Docker, só que em vez de imagem OCI é OSTree.</p>
<p>Entenda que Flatpak e Snap são primos de Docker e Kubernetes. As tecnologias por baixo também dependem da kernel do Linux e sempre caímos em cgroups, namespaces, capabilities e algum formato de HD virtual dentro de um linguição de bytes binários que chamamos de imagens. Espero que tenha dado uma luz no entendimento de containers e espero que ninguém mais tenha a pachorra de confundir containers com máquinas virtuais. Se ficaram com dúvidas mandem nos comentários abaixo, se curtiram o video deixem um joinha, assinem o canal e compartilhem o video com seus amigos. A gente se vê, até mais!</p>
]]></content:encoded><category>containers</category><category>linux</category><category>devops</category><category>akitando</category></item><item><title>[Akitando] #48 - Entendendo "Devops" para Iniciantes em Programação (Parte 2) | Série "Começando aos 40"</title><link>https://akitaonrails.github.io/2019/04/17/akitando-48-entendendo-devops-para-iniciantes-em-programacao-parte-2-serie-comecando-aos-40/</link><guid isPermaLink="true">https://akitaonrails.github.io/2019/04/17/akitando-48-entendendo-devops-para-iniciantes-em-programacao-parte-2-serie-comecando-aos-40/</guid><pubDate>Wed, 17 Apr 2019 17:00:00 -0300</pubDate><description>&lt;div class="embed-container"&gt;
&lt;iframe
src="https://www.youtube.com/embed/mcwnQVAn0pw"
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&gt;
&lt;/iframe&gt;
&lt;/div&gt;
&lt;p&gt;Hoje finalmente vamos usar tudo que aprendemos até agora pra explicar as diferenças entre hypervisors e containers, e agora sim, falar um pouco mais de Docker e Kubernetes.&lt;/p&gt;
&lt;p&gt;Precisamos explicar o que foi essa mudança no mundo de sysadmins de ter que lidar com hardware pra um mundo onde hardware essencialmente se tornou software pra muitos casos, especialmente em Web e como empresas como a Amazon AWS e Heroku ajudaram a mudar inclusive a forma como programamos.&lt;/p&gt;</description><content:encoded><![CDATA[

<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/mcwnQVAn0pw"
    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>Hoje finalmente vamos usar tudo que aprendemos até agora pra explicar as diferenças entre hypervisors e containers, e agora sim, falar um pouco mais de Docker e Kubernetes.</p>
<p>Precisamos explicar o que foi essa mudança no mundo de sysadmins de ter que lidar com hardware pra um mundo onde hardware essencialmente se tornou software pra muitos casos, especialmente em Web e como empresas como a Amazon AWS e Heroku ajudaram a mudar inclusive a forma como programamos.</p>
<p>Links:</p>
<ul>
<li>Goodbye Docker and Thanks for all the Fish (<a href="https://technodrone.blogspot.com/2019/02/goodbye-docker-and-thanks-for-all-fish.html"target="_blank" rel="noopener">https://technodrone.blogspot.com/2019/02/goodbye-docker-and-thanks-for-all-fish.html</a>)</li>
<li>The Twelve-Factor App (<a href="https://12factor.net"target="_blank" rel="noopener">https://12factor.net</a>)</li>
</ul>
<p>=== Script</p>
<p>Olá pessoal, Fabio Akita</p>
<p>Este é o décimo terceiro episódio da série Começando aos 40, e parte 2 do tema de devops. Recapitulando, semana passada eu expliquei como saímos das hospedagens compartilhadas e colocations dos anos 90 para as VPSs e opções baseadas em tecnologias de virtualização. Entendemos as diferenças de full virtualization, paravirtualização e um pouco de containers e hoje vamos entrar na melhor década do século XXI até agora pra gente. Pra muita gente a revolução na área veio com Docker e pra outras pessoas esse período pode estar chegando ao seu fim. Vamos ver como!</p>
<p>(&hellip;)</p>
<p>Como eu disse no episódio anterior, o mundo de infraestrutura não estava preparado para o boom que começou a acelerar principalmente a partir de 2005. Unicórnios estavam se proliferando, crescendo em demanda muito rápido e nenhum data center estava preparado para eles. Aliás, no episódio anterior eu mencionei VPS mas não expliquei direito o que são. Virtual Private Servers, basicamente são serviços onde você escolhe um tipo de máquina virtual, quanto de CPU, quanto de memória, de disco e que sistema operacional e paga uma mensalidade por máquina. Na época eu gostava de usar os da Linode mas tinha vários outros. Era como alugar uma máquina num data center, só que virtual, então mais barato e mais fácil.</p>
<p>A própria Amazon, no começo dos anos 2000 estava passando por problemas de escala. Cada novo projeto levava 3 meses só pra configurar a infraestrutura, as máquinas, os bancos de dados, a rede. E fazendo tudo do zero toda vez. Sendo um ecommerce estamos falando de um negócio de margem baixa, por isso cada centavo conta. E nesse caso foi natural juntar anos de experiência criando essa infraestrutura do zero pra criar um serviço pra automatizar e gerenciar todos esses recursos, mitigando essa necessidade de fazer tudo do zero o tempo todo e foi assim que em 2006 a Amazon lança os produtos EC2 pra máquinas virtuais, o S3 pra blob storage e o SQS que é o Simple Queue pra filas - um componente importante pra escalabilidade como já expliquei nos episódios sobre concorrência. Logo depois a AWS lança o EBS que é o Elastic Block Storage que são volumes montáveis nas máquinas EC2 pra ser a segunda opção de armazenamento.</p>
<p>A grande vantagem de máquinas EC2 é você poder criar, ou provisionar uma máquina, criar um volume EBS, anexar esse volume à máquina EC2, e de dentro dela montar o volume no sistema operacional, formatar e usar como se fosse um disco normal. Dessa forma tudo que você instalar ou configurar ficaria persistido nesse volume. Se amanhã você quisesse criar uma segunda máquina bastava criar um snapshot da máquina e carregar num segundo EC2 ou se quisesse aumentar a capacidade computacional da máquina, podia literalmente apagar a máquina, criar outra maior e anexar o mesmo volume e tudo funcionaria praticamente plug and play.  Descrevendo desse jeito se parece com qualquer VPS, então o que foi tão diferente assim?</p>
<p>A AWS fez duas coisas muito diferentes dos VPS: cobrança e controle. Primeiro foi o modelo de bilhetagem onde você pagaria por hora de uso dos recursos, ou por megabytes usados no caso do S3. Em vez de pagar uma mensalidade pela máquina você pagaria pelo uso real. Esse tipo de granularidade, mais próxima da minutagem de celulares nunca tinha sido tentada em hospedagens. Ou seja, se você decidisse que só esta semana precisa de uma máquina maior, podia fazer a mudança, depois voltar pro tipo de máquina menor, com facilidade e pagando só pelas horas usadas.</p>
<p>Mais importante: você podia controlar tudo via APIs, sem precisar entrar num dashboard. De repente você queria criar e apagar máquinas com mais frequência, pra tirar vantagem desse modelo. Uma vantagem de ser a Amazon é que eles começaram rapidamente a replicar o modelo em múltiplas regiões geográficas diferentes, permitindo que grandes empresas pudessem escalar rapidamente e com redundância entre diferentes data centers, com a facilidade de enviar um comando numa API. Esse tipo de granularidade de cobrança e de controle simplesmente não existia.</p>
<p>Acho que aqui podemos dividir a história da infra de web em 2. O antes e depois da AWS, com o advento de lidar comercialmente com máquinas da mesma forma como lidamos com software. Deixa eu explicar. Desde sempre na história da informática tínhamos uma polarização: desenvolvedores de software gostam e precisam mexer no software o tempo todo, pra corrigir bugs, pra adicionar novas funcionalidades que vão atrair mais pessoas. Já um sysadmin, ou dba ou administrador de redes, depois que terminam de configurar tudo, eles querem mexer na infraestrutura o menos possível, de preferência nunca mais. Software vs Hardware, soft vs hard.</p>
<p>Isso porque infraestrutura como vocês puderam ver até agora sempre foi um House of Cards … não, não esse … esse outro, isso, literalmente. Racks, roteadores, cabeamento, HDs, metal e parafusos. Um monte de peças de lego montados um em cima do outro. Assopre muito perto e a casa despenca. Basta uma linha de configuração errada e você comprometeu a segurança e integridade de tudo. Basta um comando de formatar o drive errado e você perdeu todos os dados.</p>
<p>Isso me lembrou de um caso que eu vi acontecer anos atrás. Um operador de data center fez um script às pressas pra resolver um problema. No Bash você tem várias variáveis globais que começa com cifrão, não lembro o que ele queria fazer mas no final ele digitou uma letra errado. Esse script foi enviado via SSH pra dezenas de servidores reais. Talvez tenha sido o $0 que pega o último comando rodado. De repente dezenas de máquinas ficaram sem o Bash! Logar remoto via SSH não funcionava porque não tinha mais o Bash pra abrir um novo terminal. A única forma de voltar a ter acesso nas máquinas foi ir até o data center e várias pessoas com pen drives conectarem fisicamente, espetar os pen drives, e copiar o binário do bash de volta. Foi meio tenso.</p>
<p>Por isso um bom sysadmin precisa ser uma pessoa paranóica, que checa e recheca o que está fazendo, que vai fazer o possível pra ter redundância de tudo, que vai fazer backup do backup, que vai checar que o backup de fato funciona pra restaurar, que vai monitorar cada minuto de cada servidor pra ver se alguma coisa está acontecendo. E mesmo assim, num momento de emergência, depois de varar 2 noites, uma letra errada num script pode gerar uma catástrofe.</p>
<p>Enfim, dependendo do seu projeto, podia levar dias ou até meses pra coordenar a configuração de uma nova infraestrutura pra um projeto. Mexer nesse house of cards … depois de tudo pronto era proibido, você tinha que tratar essas coisas como se fosse material nuclear: só pessoal autorizado podia criar um novo índice numa tabela num banco de dados ou instalar qualquer pacote novo. Rodar scripts direto no servidor, nem pensar. No fim dos anos 90 era caos, mas à medida que entramos nos anos 2000 e o mundo enterprise começou a adotar tecnologias de web, a cultura corporativa foi sendo adotada primeiro.</p>
<p>Foi só depois da recuperação da crise da internet de 2001, depois que os unicórnios que falei antes começaram a aparecer, que essa cultura entrou em conflito com as necessidades de escalar mais rápido. A cultura de infraestrutura dessa época, regada a coisas como ITIL era insuficiente. Não é a mesma coisa mas ITIL era como se fosse o Waterfall no mundo de software ou os atuais Pseudo Agile pra gestão de projetos, um conjunto de processos e rituais burocráticos e corporativistas.</p>
<p>Dentro de um lugar como Twitter ou Amazon você teria guerra civil entre desenvolvedores e sysadmins se as coisas continuassem do mesmo jeito. Em projetos pequenos, os VPS ajudaram porque era fácil dar máquinas virtuais pra desenvolvedores, mas eles ainda não tinham tudo que se precisava pra escalar. As ferramentas de gestão de configuração como Chef ou Puppet também ajudaram os sysadmins porque agora o que levava dias pra configurar levaria horas ou minutos.</p>
<p>Mas a divisão entre os dois lados continuaria, um desenvolvedor de software simplesmente não tem conhecimento ou experiência suficiente pra ganhar acesso de entrar numa máquina de produção. Ninguém quer correr o risco de alguém não autorizado entrar e deixar uma brecha de segurança pra trás. E da forma como se fazia software até esse ponto, você fazia deployment da aplicação no servidor e alguém precisava entrar na máquina via um terminal pra acertar arquivos de configuração, acertar tabelas e índices no banco, rodar alguma rotina extra pra instalar dependências que faltaram e coisas assim.</p>
<p>Mas quando a AWS liberou máquinas virtuais como um serviço tivemos uma quebra de paradigma. A somatória das tecnologias que falei até agora facilitaram uma coisa que antes era muito difícil: destruir e recriar máquinas em poucos segundos. Pra começar, a AWS era muito maior que qualquer VPS. Só isso já eliminou a necessidade de precisar ter máquinas físicas pra maioria das médias e até grandes empresas que não tinham políticas antiquadas e aceitavam mais riscos.</p>
<p>Agora os sysadmins ganharam a capacidade de criar imagens e oferecer essas imagens aos desenvolvedores rapidamente. Era muito fácil criar ambientes de teste separados do ambiente de produção mas ambos configurados exatamente iguais, de forma que o que funcionava em ambiente de teste garantidamente funcionaria nas máquinas de produção. E o melhor, a custos muito menores e com menos necessidade de planejar com tanta antecedência porque não havia coisas como mensalidades que são compromissos longos.</p>
<p>Entenda uma mudança de papéis: antes os sysdadmins lidavam diretamente com bare metal. Instalavam os servidores reais em racks físicos e ligavam cabos de redes e configuravam direto na máquina com teclado e monitor conectado no hardware. Com as VPS e depois a AWS, nasceu uma segunda categoria de sysadmins que não precisavam mais ter contato com equipamento físico, a menos que trabalhassem em data centers. As empresas não precisavam mais se preocupar em comprar hardware e sim em contratar serviços.</p>
<p>Com as tecnologias de virtualização e containerização que falei no episódio anterior, nós essencialmente empacotamos uma máquina virtual como um software. E foi criado um novo modelo de negócios em cima dessa nova categoria de software. Por agora vamos só entender que no momento em que o provisionamento de máquinas virtuais se tornou programático, via APIs, passamos a tratar a infraestrutura como software. Assim nasce a Infraestrutura como Serviço ou IaaS.</p>
<p>Surgiram esforços open source pra clonar os conceitos proprietários da AWS como o Eucalyptos que a Canonical adotou mas hoje já meio que morreu, e o OpenStack que foi adotado por centenas de empresas e veio evoluindo bastante nos últimos anos e é composto por dezenas de componentes como o Nova que orquestra hypervisors como o Hyper-V ou vSphere, o Neutron que cuida de tudo relacionado com rede incluindo detecção de invasão, enfim, dezenas de componentes.</p>
<p>E o OpenStack só iniciou mesmo a partir de 2010, ou seja 4 anos depois da AWS, só pra vocês entenderem que o que a AWS inventou em 2006 era difícil de replicar. Levou anos pra aparecer concorrentes de verdade. Foi só depois de 2010 que ganhamos boas opções comerciais como Microsoft Azure e Google Cloud. Eles estão competindo bem e as opções são equiparáveis na maioria dos produtos. Como a gama de opções de cada uma é muito grande então diferentes pessoas vão ter críticas de coisas diferentes em diferentes cenários. Não tem um vencedor hegemônico.</p>
<p>Até aqui eu dei uma rápida passada pelos conceitos de paravirtualização e virtualização acelerada por hardware como as instruções VT-X além das ferramentas de gestão de configuração como Chef e finalmente a primeira geração de orquestração de infraestrutura como OpenStack que ajuda a levar um ambiente como a AWS pra dentro de grandes empresas que precisem de garantias totais de segurança como agências governamentais ou empresas que lidam com dados de alta periculosidade ou privacidade como dados médicos ou financeiros sensíveis. Ou sei lá, os projetos de uma bomba nuclear. Você certamente não deveria colocar essas coisas no Dropbox.</p>
<p>Essa vertente de IaaS privado é mais conhecido como Cloud Privado e existem várias empresas que oferecem soluções como o OpenShift da RedHat, o CloudFoundry da Pivotal. Assim você consegue ter a flexibilidade de infraestrutura virtualizada que pode ser rapidamente utilizada pelos projetos internos sem precisar da antiga burocracia de infraestrutura e se mantendo num perímetro controlado fora da internet pública.</p>
<p>Até aqui vimos até onde as tecnologias de virtualização chegaram no mundo comercial. Mas containers ainda não entraram na história. Vamos voltar um pouco pra 2006, que foi quando o Google desenvolveu outro projeto, o Process Container que foi uma evolução em cima dos Jails de BSD ou Zonas de Solaris ou mesmo os OpenVZ e Vserver de Linux. Repetindo: a ideia não é virtualização como Vmware ou Parallels ou Virtualbox, e sim isolamento dos processos e dos recursos da máquina.</p>
<p>Esse projeto foi renomeado depois pra Control groups ou cgroups. A ideia ainda é fazer a kernel mentir pros processos. Desse jeito você pode ter dois processos rodando na mesma máquina, no mesmo sistema operacional, mas um sem saber da existência do outro, quase como se estivessem em máquinas completamente diferentes. Se o objetivo é só isolar um processo, todas as opções de jail que culminaram no cgroups são ordens de grandeza mais leves do que virtualização, porque você ainda está rodando somente uma kernel em vez de múltiplas kernels virtualizadas.</p>
<p>Vamos rever conceitos: um sistema operacional é um conjunto de binários. A primeira coisa depois do boot loader é a kernel, é o primeiro grande binário que carrega com os privilégios máximos do Ring-0 e é responsável por carregar todo o resto do sistema, que por sua vez são conjuntos de programas como os drivers, supervisores como systemd, daemons e assim por diante. Em distribuições Linux, mesmo rodando praticamente o mesmo binário de kernel, o que diferencia um RedHat de um Ubuntu ou Arch é esse conjunto de binários que vai empacotado na distribuição e como eles são configurados, incluindo preferências em estrutura de diretórios ligeiramente diferentes, sistema de pacotes como yum ou apt ou pacman. Mas tudo começa com o kernel e os drivers, daí ele monta o volume de disco como o “barra” que é o root e de lá vai carregando todo o resto no userland ou Ring-3.</p>
<p>O tal cgroups consegue isolar os recursos da máquina e fazer os processos em userland enxergar só o que ele mandar enxergar. Ele vai dizer quanto de CPU, de memória o processo vai enxergar mas em particular, assim como o conceito do chroot que falei antes, ele pode mascarar o volume de disco e carregar um root diferente do sistema hospedeiro. Se você carregar um outro root com outro conjunto de programas userland, na prática você pode ter um hospedeiro que é um RedHat e dentro do jail do cgroups carregar os binários que formam um Ubuntu ou qualquer outra distro.</p>
<p>Voltando ao cgroups do Google, em 2008 tivemos o lançamento do LXC ou Linux Containers, que é um conjunto que inclui coisas como daemons e ferramentas de linha de comando que engloba não só o cgroups que passou a vir no kernel do Linux como namespaces que permite etiquetar processos. O cgroups consegue criar os limites pra grupos inteiros de processos. Quando você cria um namespace de processos, seu processo vira o PID, ou process ID, número 1 e os filhos desse processo vão seguindo nessa sequência. Diferentes namespaces vão ter diferentes processos PID 1. Você tem namespaces de rede, daí seu processo pode se ligar a uma porta virtual, assim vários processos podem responder numa porta 80 virtual, na mesma máquina, e uma bridge ou ponte virtual de rede vai cuidar pra que isso seja possível, fazendo tipo um NAT entre os containers e a rede externa. E tem namespaces de mounts, onde você pode montar e desmontar volumes dentro do container, sem afetar os mounts do sistema hospedeiro.</p>
<p>Lembram como eu expliquei que o sistema operacional mascara os endereços reais de memória pra uma memória virtual e o endereço virtual 0 de memória que um processo vê não é o endereço 0 real da memória? Ou como um chroot mascara o filesystem e cada processo enxerga roots no disco em lugares diferentes? Assim dois processos nunca vão pisar um em cima do outro? Pense em cgroups e namespaces como o equivalente pra todos os outros recursos da máquina, incluindo recursos de rede e I/O. Eu sei que estou sendo repetitivo mas é importante deixar claro: os containers de LXC parecem muito com um sistema operacional, mas eles compartilham a kernel do mesmo sistema, muito diferente de hypervisors que gerenciam máquinas virtuais inteiras, cada um com seu kernel. Um jail que restringe processos e compartilha o mesmo kernel entre vários jails é o que chamamos hoje de um container.</p>
<p>Esses anos entre 2006 e 2008 pra mim foram muito interessantes. Não quero entrar em muitos detalhes aqui mas eu acho que hoje em dia não preciso mais explicar o que é o GitHub ou GitLab, porque qualquer desenvolvedor que se preza já sabe usar Git. Se você ainda tinha alguma dúvida, Git é obrigatório, tem que saber! Mas antes de 2006 isso não era verdade, muitos usavam Subversion ou outras alternativas como BitKeeper, as porcarias de SourceSafe ou Clearcase, ou mesmo CVS. Mas a partir de 2007 o Git, que é uma ferramenta para gerenciar repositórios de código fonte versionados e distribuídos começou a ganhar terreno rápido, e com o início da popularização do Github em 2008 na comunidade Ruby on Rails, ela criou as sementes não só pra versionar código de aplicações mas também pra servir como mecanismo para automatizar muita coisa.</p>
<p>Também em 2008 outra empresa surgiu na comunidade Ruby on Rails pra resolver o problema que eu vim discutindo até agora de instalar, configurar e manter infraestruturas em máquinas virtuais. Pense somar um provedor de VPS com as capacidades de Linux containers via os cgroups e integrando com um processo de deployment usando repositórios Git. Esse é o Heroku. Se você nunca usou, deveria. É a recomendação pra todo mundo que não tem um sysadmin trabalhando tempo integral ou pra desenvolvedores que não tem experiência com infraestrutura.</p>
<p>Como já disse antes, o AWS EC2, principalmente em 2008, ainda não é muito diferente de um VPS do ponto de vista de um desenvolvedor. São máquinas virtuais. Você programa em PHP, ou Python ou Ruby. Mas pra subir sua aplicação precisa saber instalar e configurar banco de dados, servidor web, rede e tudo mais. Ou seja, ou vai entrar manualmente via SSH e configurar tudo na mão, ou vai usar um Capistrano ou Puppet pra fazer isso. E se você é iniciante isso não é trivial.</p>
<p>Mas e se existisse um serviço por cima desse AWS onde você pudesse simplesmente criar uma conta, cadastrar uma nova aplicação, daí rodar um “git push heroku” que é o comando de git pra subir seu código num repositório Git, e de repente toda sua aplicação é instalada, configurada e está pronta pra rodar, inclusive com banco de dados e outros serviços? Isso é o Heroku. Eles conseguiram tirar vantagem de cgroups e criar seu próprio padrão de containers e montar um modelo de negócios, 5 anos antes de um Docker aparecer. O que você chama de Dockerfile hoje, mais ou menos é o que o Heroku chamou de buildpacks. Todo mundo tentou fazer algo parecido mas levou anos pra chegar perto. Como o Heroku nunca abriu seu padrão de containers pro mundo open source, precisou o Docker aparecer anos depois pra finalmente termos um concorrente.</p>
<p>O que o Heroku conseguiu fazer foi tornar a infraestrutura trivial pra desenvolvedores. Lembram como eu passei o episódio inteiro da semana passada falando como era complicado montar uma infra com confiabilidade e segurança? É o melhor meio do caminho entre uma VPS ou AWS puro e um Kubernetes porque eles acertaram a usabilidade e foram inteligentes no modelo de negócios.</p>
<p>A partir de 2006 o mundo de tech startups explodiu, inspirada por todos os novos unicórnios crescendo como se não houvesse amanhã. Todo mundo queria conseguir subir suas coisas nas novidades da AWS mas ninguém tinha conhecimento suficiente de infraestrutura pra ficar lidando com CF Engine ou Puppet. Pior ainda, um desenvolvedor não sabe como de fato configurar um banco de dados. Não basta só instalar porque a configuração padrão costuma ser ruim, então você precisa saber tunar esse banco pra caber no seu servidor corretamente. Ninguém sabe configurar replicação master slave. E por aí vai.</p>
<p>Por causa disso se abriu um nicho pra tech startups que não existia até então. Se você quer enviar e-mails, jamais tente montar seu próprio servidor de e-mail como a gente fazia no começo dos anos 2000. Em vez disso contrate um serviço como Sendgrid ou Mailgun. Em vez de montar seu próprio servidor de Redis, com redundância, backup e tudo, contrate uma RedisLabs. Em vez de montar seu próprio servidor de cache com Memcached, contrate a Memcachier. Em vez de ter que lidar com repositório pra upload de imagens ou outros arquivos e ter que programar coisas como processamento de imagens, contrate a Cloudinary. E por aí vai.</p>
<p>Se a AWS inaugurou a categoria de IaaS ou infraestrutura como serviço, o Heroku inaugurou a categoria de PaaS ou plataforma como serviço e finalmente, empresas como New Relic, Papertrail, CloudAMQP, Sendgrid, Cloudinary, e centenas de outros inauguraram a terceira categoria: o famoso SaaS ou Software as a Service.</p>
<p>E o que o Heroku fez de inteligente foi desde o começo integrar esses diversos SaaS em sua plataforma de tal forma que fosse simples tanto via o dashboard web quanto via linha de comando adicionar esses serviços aos nossos projetos, além de integrar a cobrança de tudo numa fatura só. Mas eles mantiveram pra eles alguns dos serviços mais críticos como o banco de dados Postgresql e depois o Redis. E por isso era tão difícil tentar implementar um clone de Heroku você mesmo. A parte de containers, buildpacks e deployment usando um comando como git push não era tão difícil subir algo parecido num VPS como Linode ou DigitalOcean. Mas não adiantava nada fazer deployment da sua aplicação e não ter um banco de dados confiável. Daí você caiu no mesmo problema de antes de ter que configurar o banco de dados e, principalmente, manter ele rodando.</p>
<p>Eu diria o seguinte, se você é desenvolvedor de software, não tem experiência real como sysadmin, e precisa subir sua aplicação web mas não sabe nem como começar a configurar um Kubernetes, vá pro Heroku. É muito menos dor de cabeça. E não faça a conta de centavos por hora vezes horas por mês e comparar com o preço do Heroku e dos outros SaaS. Considere quanto custa um sysadmin pra configurar e dar manutenção na infra toda por toda a vida da sua aplicação. Não tente fazer infra sozinho se nunca fez isso antes, é sempre um desastre. E se sua aplicação web não monetiza o suficiente pra pagar a conta, você está fazendo alguma coisa muito errada.</p>
<p>Mas, além de ficar aqui fazendo propaganda de graça pro Heroku eu queria falar de outra coisa importante. O Heroku foi inteligente também educando os desenvolvedores. Nós desenvolvedores que crescemos no fim dos anos 90, com coisas como Java ou mesmo PHP, em ambiente corporativo, estávamos mais preocupados em codar de qualquer jeito dentro do prazo e não se preocupar tanto com a qualidade ou peso da aplicação. A métrica era praticamente linhas de código por hora. Se a aplicação ficasse pesada era só culpar a infra e mandar instalar numa máquina maior e foda-se.</p>
<p>Em 2008, no Heroku os containers ou dynos como eles chamavam vinham em vários tamanhos, contanto que esse tamanho fosse 512Mb de RAM e acho que só depois colocaram 1GB de RAM e anos depois opções maiores. Nós rapidamente começamos a fazer aplicações web mais e mais complexas e de repente não cabia em 512Mb!! Código mal feito roda na máquina do desenvolvedor, mas quebra em produção, em particular em recursos mais restritos como nos dynos do Heroku. Aliás, dynos é como o Heroku chama seus containers.</p>
<p>Eu sempre achei isso sensacional, porque é muito fácil fazer um código porco. E é sempre bom não contar com a muleta de só aumentar a máquina e mostrar imediatamente que a aplicação que tá mal feita mesmo. Quando você não tem outra opção, vai precisar programar direito. Então o Heroku ajudava a separar programador bom de programador ruim. Inclusive o co-fundador do Heroku, Adam Wiggins publicou um guia chamado the twelve factors ou os 12 fatores, que vou deixar nos links na descrição abaixo, que descreve como uma aplicação web deveria ser programada e organizada pra ser escalável. O framework Ruby on Rails desde sempre seguia esses 12 fatores e com a popularização do Heroku todo framework web que se preza passou a seguir, independente de rodar no Heroku ou não porque esses 12 fatores são universais pra apps minimamente bem feitas. E não era tão simples, um Wordpress da vida dava trabalho pra se adequar em todos os fatores na época.</p>
<p>Graças a isso uma nova geração de técnicas, otimizações, procedimentos e convenções começaram a mudar a forma como a gente programava. Em vez de pensar em rodar numa única máquina grande, tínhamos que parar pra pensar em rodar em múltiplos containers menores. Entendemos que a forma de escalar massivamente não é verticalmente aumentando a máquina, mas sim horizontalmente com mais containers. E na intenção de quebrar aplicações grandes pra escalar que nasceu também a idéia de micro-serviços. Eu sou contra quebrar sua aplicação em dezenas de micro-serviços sem motivos. Mas um bom motivo é quando uma aplicação grande não cabe num container, então fica óbvio que precisa ou cortar fora a gordura que não precisa, ou particionar em mais de uma aplicação em containers diferentes.</p>
<p>Pulando a história pra 2013 finalmente surge o tal Docker que inicialmente usou o Linux Containers ou LXC e adicionou novas ferramentas. Em particular, ele se aproveita do fato de você poder montar um root separado do sistema hospedeiro no container e passou a usar o Union File System. Pense um filesystem que consegue montar uma imagem que é somente de leitura como o novo root desse container, e toda vez que você tenta escrever ele cria uma segunda camada por “cima” vamos dizer assim.</p>
<p>Lembram quando expliquei sobre o conceito de Copy on Write pra memória no caso de processos que fazem fork no Linux? É a mesma coisa só que pra disco. Por isso as imagens de Docker são incrementais. Se você usa Git, pense se você pudesse montar um repositório de Git como o root do seu container e toda vez que você modifica ou escreve alguma coisa é como se fizesse um novo commit. Assim você consegue inclusive voltar um desses commits pra versão anterior. Mais importante: você consegue empacotar esse filesystem inteiro num arquivo, que são as imagens que seu cliente de docker baixa.</p>
<p>O Docker depois trocou o LXC, que se usava via a biblioteca libvirt, por uma que eles mesmos fizeram, a libcontainer. Mas acho que o mais importante do Docker foi padronizar a receita pra descrever as imagens no que hoje conhecemos como Dockerfiles. Esses Dockerfiles são parecidos com as promessas de um CF Engine ou as receitas de um cookbook de Chef. É um arquivo onde você declara como essa máquina vai ser configurada, que pacotes vão ser instalados, você pode inclusive declarar que comandos rodar da primeira vez que a imagem é criada ou toda vez que ela é inicializada. E também padronizar a interface de uso de linha de comando que hoje até o Kubernetes meio que copia.</p>
<p>Tem gente que ainda confunde Docker com alguma coisa parecida com um Virtualbox, mas espero que com o que expliquei até agora tenha ficado mais claro. Docker não é full virtualization, não é paravirtualization e não é um hypervisor. Virtualização é literalmente falsificar o hardware físico. Nesse hardware virtual você pode rodar múltiplos sistemas operacionais diferentes, ou seja, múltiplas kernels diferentes, incluindo kernels que não são do Linux como Windows ou BSD ou Mac e inclusive de processadores diferentes de Intel ou AMD como os fazem os emuladores de videogame.</p>
<p>Docker é um conjunto de ferramentas pra organizar e facilitar o uso de de cgroups, namespaces e union filesystem, tudo que você podia usar sozinho, mas a casca do Docker por cima obviamente torna tudo ordens de grandeza mais simples. Portanto você deve ter entendido que a idéia toda é rodar os programas de todo mundo no mesmo sistema operacional, na mesma máquina, sem rodar outro sistema operacional por cima. Na prática é uma máquina só, um sistema só, rodando os programas lado a lado mas com a kernel mentindo pra todos eles acharem que não tem ninguém mais.</p>
<p>Mas se você já usou Docker já deve ter instalado em cima de um RedHat ou Ubuntu e dentro dos containers ter conseguido instalar e rodar outra distro Linux, totalmente diferente, como Arch ou Alpine e inclusive rodar um pacman e instalar pacotes, ou seja, fica parecendo que é um VMware ou outro hypervisor onde você instala outro sistema operacional. Como isso é possível se eu acabei de dizer que não é virtualização e estamos compartilhando o mesmo sistema operacional?</p>
<p>Falando de forma bem simplificada, uma distro Linux é o conjunto do binário da kernel do Linux e de programas como o gerenciador de pacotes, o binário do Bash e outros utilitários e serviços. Tirando a kernel que roda no Ring-0, o tal kernel space, todo o resto roda em userland. Agora pense uma distro que eu removo o kernel, só sobra os programas em userland. E eu falei que num container nós compartilhamos a mesma kernel. Ou seja, se o container montar um diretório pra onde estão os programas de userland que definem um Ubuntu, então dentro desse container, pra todos os efeitos e propósitos, é um Ubuntu, mesmo que do lado de fora estamos rodando o Docker num Fedora. O Ubuntu dentro do container vai usar a kernel compartilhada do Fedora. Essa é a mágica dos containers.</p>
<p>Obviamente rodar todos os programas userland de um Ubuntu inteiro dentro de um container é pesado e um desperdício de recursos, especialmente se eu for inventar de querer rodar 10 containers, todos com os mesmos userland de Ubuntu repetidos. Por isso temos outras distros mais adequadas pra rodar num container como o CoreOS ou Alpine, que são userlands ordens de grandeza menores e só com os binários essenciais pra rodar a maioria dos programas que precisamos. A idéia do Docker é que o seu programa dentro do container não dependa de nada do sistema operacional hospedeiro e rode suas próprias dependências. A idéia de um container não é rodar dezenas de programas dentro dele, mas sim somente em um. Um container só pro Postgres, outro container só pro Redis, outro container só pro NGINX.</p>
<p>Por isso mesmo que não existe Docker de Windows ou de Mac. A kernel do Windows não tem cgroups e namespace nem union file system, nem o Mac e acho que nenhum UNIX BSD também. Mas então como eu consigo rodar Docker no Windows ou no Mac? Na realidade você primeiro tem que iniciar um hypervisor, seja tipo 1 como o Hyper-V ou o novo Hypervisor Framework no MacOS a partir da versão 10.10 Yosemite, ou mesmo um hypervisor tipo 2 como Virtualbox ou Vmware workstation.</p>
<p>Daí você cria uma máquina virtual no hypervisor e daí instala uma distro Linux com Docker. Agora, de dentro dessa máquina virtual você pode criar containers e compartilhar a kernel desse Linux. Daí o cliente de Docker rodando no Windows se comunica com o Docker rodando dentro da máquina virtual, por exemplo, via a rede interna TCP. Esse é o truque todo! Por isso, em hardwares físicos equivalentes, o Docker vai ser mais performático se o sistema operacional hospedeiro for também um Linux. Se for um Windows ou Mac você vai perder performance porque vai precisar também rodar um Linux num hypervisor antes.</p>
<p>A idéia de um container não é ser um sistema operacional independente. Como já disse o correto é rodar somente um único processo por container. Por isso você tem hoje soluções como o Docker Compose que permite que você declare num arquivo texto pra subir vários containers, cada qual com seu Dockerfile e dizer que volumes eles vão montar pra compartilhar arquivos, ou que portas eles vão expor que o outro container vai conseguir enxergar.</p>
<p>Em paralelo a isso, digamos que você montou seu Dockerfile e seu container funciona bonitinho na sua máquina de desenvolvimento, mas agora eu quero subir essa mesma imagem na minha conta na AWS EC2 ou DigitalOcean ou outro provedor de máquinas virtuais. Pra isso existe o Docker Machine, que vai facilitar o processo de subir ou fazer deployment dos seus containers num servidor na internet. Nesse caso o provedor precisa fornecer o suporte a Docker em suas máquinas virtuais ou você mesmo instalar o serviço de Docker na máquina virtual que provisionar.</p>
<p>Só fazer alguns containers não é suficiente. Você quer ter controle maior pra subir uma infraestrutura inteira com dezenas de máquinas virtuais e containers, além de configuração de rede, políticas de segurança e outras coisas. Organizar todos esses recursos virtuais e não virtuais de uma forma coordenada requer mais que só um vmware ou docker, por isso existem as soluções que chamamos de orquestradores, como o Docker Swarm, ou o Mesos, ou Marathon ou Nomad. Mas em 2019 quem realmente se tornou o padrão de fato é o Kubernetes do Google, que está disponível em todo provedor de infraestrutura como serviço, desde o Google Cloud, Microsoft Azure ou AWS.</p>
<p>Essas soluções se confundem com o próprio Docker já que num Kubernetes você instala imagens compatíveis com as de Docker e o runtime de containers por um tempo foi o próprio Docker. Na prática imagine que você está no Google Cloud, que é concorrente da Amazon AWS. Você tem máquinas virtuais rodando em cima de alguma coisa como o Citrix XenServer, que do ponto de vista de um servidor, é como se fosse seu Virtualbox ou Vmware que você roda no seu desktop ou notebook. O Kubernetes enxerga isso como Nodes ou nós. E dentro desses nodes você pode rodar múltiplos containers Docker que o Kubernetes enxerga como containers, agrupados dentro de Pods. Mais do que isso você tem um registro central de toda a infraestrutura de tal forma que se um container tiver problemas e cair ou node cair, o sistema central sabe como reconstruir.</p>
<p>Se você está iniciando, parece algo que um Docker compose e Docker machine resolveriam, mas o Kubernetes, ou seus concorrentes como o Swarm ou Mesos, são mais complexos que isso. Além de containers, temos que lidar com storage, com endereços de IP externos e internos, regras de firewall, clusters. Ele está preparado pra cenários com múltiplas zonas; por exemplo distribuir parte dos seus containers na zona Estados Unidos Central e outros containers na zona Asia sul caso você tenha público tanto na América quanto na Ásia e quer oferecer a melhor performance pros dois públicos. E existe o suporte a cluster de federações, dessa forma você pode manter parte da sua infraestrutura no Kubernetes do Google Cloud que é GKE e outra parte no Kubernetes da AWS que é o EKS. Ou seja, em provedores diferentes de cloud.</p>
<p>Em resumo o Docker se tornou uma organização, um ecossistema, e um conjunto de ferramentas, mas por baixo dos panos os componentes que realmente importam e possibilitam isso vem na kernel do Linux, que é o cgroups e o namespace. Todo o resto é só uma convenção de uso e um padrão. Inclusive surgiu uma iniciativa do próprio Docker pra padronizar o runtime de containers e o formato das imagens, que é o Open Container Initiative ou OCI. Diversos projetos concorrentes ao Docker surgiram por conta do runtime-spec e image-spec como gvisor, clearcontainers ou katacontainers. Eu mencionei que o Kubernetes usa o runtime de containers do próprio Docker mas pouco a pouco já se vê instalações de Kubernetes que não usam mais o Docker.</p>
<p>O Heroku teve um papel importante em educar e demonstrar como montar uma plataforma como serviços viável. E o Docker teve um papel importante em consolidar o uso de containers Linux e padronizar seu manuseio. Mas com a abrupta popularização do Kubernetes, se não tomar cuidado, o futuro do próprio Docker é incerto já que o Kubernetes está comendo a fatia de mercado de orquestração. De qualquer maneira, se você quer ser um bom sysadmin nos tempos modernos, no mínimo precisa saber como funciona Docker e seus outros produtos e Kubernetes. E não basta treinar só na sua máquina com minikube, você precisa entrar num Google Cloud e usar o kubernetes lá. Isso é o mínimo hoje, além do que eu já venho falando desde o começo desta série: estude e aprenda Linux de verdade!</p>
<p>E por hoje vamos parar por aqui, eu falei rapidamente de como saltamos do mundo primitivo de hospedagens compartilhadas e deployment de sites via FTP pras evolução das tecnologias de virtualização e containers, até chegar no Docker e Kubernetes. Mas isso ainda é só o pico do iceberg. Eu acho que pra fechar o tema de devops, semana que vem vou tentar falar especificamente sobre banco de dados e outro termos hipster, os famigerados NoSQL e talvez uma pincelada na arquitetura moderna de Web e o comportamento das diferentes linguagens de programação nessa arquitetura de cloud.</p>
<p>Mesmo estando num mundo de Kubernetes, as opções todas que eu falei desde o começo como hospedagens compartilhadas, servidores físicos em colocation, VPS, todas ainda existem e muita gente ainda programa do jeito antigo e tem muita dificuldade de entender porque as coisas são diferentes hoje. Por isso eu precisava que vocês entendessem onde esse monte de nomes como cgroups, hypervisors se encaixa no contexto geral. Se ficaram com dúvidas - e eu imagino que ficaram - mandem nos comentários abaixo pra galera ajudar, se curtiram mandem um joinha, continuem ajudando o canal compartilhando com seus amigos, não deixem de assinar o canal e clicar no sininho pra não perder a continuação dessa história! A gente se vê semana que vem, até mais!</p>
]]></content:encoded><category>devops</category><category>containers</category><category>cloud</category><category>akitando</category></item><item><title>SmallTips: Rodando o GitLab CI Runner Localmente</title><link>https://akitaonrails.github.io/2018/04/28/smalltips-rodando-o-gitlab-ci-runner-localmente/</link><guid isPermaLink="true">https://akitaonrails.github.io/2018/04/28/smalltips-rodando-o-gitlab-ci-runner-localmente/</guid><pubDate>Sat, 28 Apr 2018 18:35:00 -0300</pubDate><description>&lt;p&gt;Se tem uma coisa que você sempre deve fazer é manter sua suíte de testes. A cada bug corrigido, a cada nova funcionalidade, adicione novos testes.&lt;/p&gt;
&lt;p&gt;Nas últimas semanas trabalhei muito pesado, literalmente codificando sem parar, sete dias por semana. Lembra daquela história de &amp;ldquo;para de reclamar que não tem tempo, o que você faz da meia-noite às 6 da manhã?&amp;rdquo;? Pois é, isso não se aplica a mim.&lt;/p&gt;
&lt;p&gt;E é justamente quando você está com sono e exausto que começa a cometer erros. Mesmo que o seu código rode localmente, você vai se ferrar em uma dessas sessões de código às 3 da manhã.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Se tem uma coisa que você sempre deve fazer é manter sua suíte de testes. A cada bug corrigido, a cada nova funcionalidade, adicione novos testes.</p>
<p>Nas últimas semanas trabalhei muito pesado, literalmente codificando sem parar, sete dias por semana. Lembra daquela história de &ldquo;para de reclamar que não tem tempo, o que você faz da meia-noite às 6 da manhã?&rdquo;? Pois é, isso não se aplica a mim.</p>
<p>E é justamente quando você está com sono e exausto que começa a cometer erros. Mesmo que o seu código rode localmente, você vai se ferrar em uma dessas sessões de código às 3 da manhã.</p>
<p>A suíte de testes foi minha salvação nesses momentos — meu copiloto, me trazendo de volta à razão sempre que a noite ficava pesada demais.</p>
<p>Mantenho meu próprio servidor GitLab para todos os projetos de clientes e internos da empresa. É só um pouco de paranoia, para garantir que meu código fica comigo. E o GitLab é uma ferramenta fantástica, ainda mais porque tem <a href="https://about.gitlab.com/features/gitlab-ci-cd/"target="_blank" rel="noopener">seu próprio sistema de Integração Contínua</a>.</p>
<p>Basta adicionar o arquivo <code>.gitlab-ci.yml</code> ao projeto, criar uma branch, fazer o push e abrir um Merge Request — o CI entra em ação, com <a href="https://docs.gitlab.com/ee/ci/yaml/#jobs"target="_blank" rel="noopener">jobs paralelos</a> se você quiser. Mesmo quando eu esquecia de rodar os testes localmente, o CI não me deixava passar.</p>
<p>Manter o CI funcionando exige que você pare e cuide das specs periodicamente. Às vezes você vai se perguntar: por que esse teste passa na minha máquina mas continua falhando no servidor de CI?</p>
<p>É aí que fica muito útil rodar a imagem Docker do CI localmente, para resolver de vez aquelas inconsistências de ambiente.</p>
<p>Você consegue fazer isso rodando o próprio GitLab CI Runner. Ele lê o <code>.gitlab-ci.yml</code> do seu projeto e executa tudo localmente via Docker. Qualquer problema que aparece no servidor vai se reproduzir localmente também. Com a vantagem de que você não precisa entrar em fila caso haja outros jobs aguardando execução (muitos desenvolvedores trabalhando, poucas máquinas disponíveis para processar tudo na hora). E você não fica poluindo essa fila com jobs desnecessários.</p>
<p>Se você seguiu minhas recomendações e instalou a excelente distro Arch Linux (ou meu derivado favorito, Manjaro Gnome), pode instalar o runner pelo AUR assim:</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>Caso contrário, verifique os repositórios da sua distro ou baixe o binário <a href="https://gitlab.com/gitlab-org/gitlab-runner/blob/master/docs/install/bleeding-edge.md#download-the-standalone-binaries"target="_blank" rel="noopener">aqui</a>. Por exemplo:</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>Com o runner instalado, suponha que você tenha um trecho assim no seu <code>.gitlab-ci.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">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>Você pode configurar vários jobs de teste rodando em paralelo. Recomendo separar: testes JS de front-end, testes unitários de back-end com Rspec/minitest, testes de feature com Capybara, verificações de segurança com Brakeman, por exemplo. Cada um roda em paralelo e você não precisa esperar tudo terminar se só quer ver o resultado dos testes de JS. Feedback mais rápido, correção imediata.</p>
<p>Localmente, a partir da raiz do projeto, basta rodar:</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>Claro, assumindo que você já tem o Docker instalado e configurado. Se não tiver, leia <a href="https://wiki.archlinux.org/index.php/Docker#Installation"target="_blank" rel="noopener">o excelente artigo do Arch Wiki sobre Docker</a> — basicamente é instalar o pacote Docker e se adicionar ao grupo docker.</p>
<p>É isso! Super simples, quase sem fricção, e o runner faz tudo que o servidor GitLab CI faria.</p>
<p>Um porém: infelizmente, parece que você <a href="https://gitlab.com/gitlab-org/gitlab-runner/issues/2409"target="_blank" rel="noopener">não consegue salvar cache ou artefatos</a> entre execuções (gems, pacotes npm, etc.), então toda vez que o runner local roda, ele começa do zero. Isso o torna menos útil para uso contínuo, mas ainda é mais rápido do que fazer push para o servidor e esperar sua vez numa fila cheia.</p>
<p>Já me salvou algumas vezes.</p>
]]></content:encoded><category>devops</category><category>containers</category><category>testes</category></item><item><title>Minhas Notas sobre Deploy Produção do Ruby on Rails no Google Cloud Kubernetes Engine</title><link>https://akitaonrails.github.io/2018/01/09/minhas-notas-deploy-producao-ruby-on-rails-google-cloud-kubernetes/</link><guid isPermaLink="true">https://akitaonrails.github.io/2018/01/09/minhas-notas-deploy-producao-ruby-on-rails-google-cloud-kubernetes/</guid><pubDate>Tue, 09 Jan 2018 20:13:00 -0200</pubDate><description>&lt;p&gt;Estou usando o Google Cloud com Kubernetes Engine há dois meses e pouco, do zero à produção. Na prática, não levei um mês para montar tudo, mas levei mais um mês para descobrir algumas arestas bem chatas.&lt;/p&gt;
&lt;p&gt;TL;DR: O Google está fazendo um trabalho razoavelmente bom como contrapeso para a AWS não se acomodar. Se você já sabe tudo sobre AWS, eu te encorajo a testar o Google Cloud. Provavelmente por memória muscular, eu ainda me sentisse mais confortável com a AWS, mas agora que me forcei a passar pelo processo de aprendizado, estou bastante confiante com Google Cloud e Kubernetes para a maioria dos meus cenários.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Estou usando o Google Cloud com Kubernetes Engine há dois meses e pouco, do zero à produção. Na prática, não levei um mês para montar tudo, mas levei mais um mês para descobrir algumas arestas bem chatas.</p>
<p>TL;DR: O Google está fazendo um trabalho razoavelmente bom como contrapeso para a AWS não se acomodar. Se você já sabe tudo sobre AWS, eu te encorajo a testar o Google Cloud. Provavelmente por memória muscular, eu ainda me sentisse mais confortável com a AWS, mas agora que me forcei a passar pelo processo de aprendizado, estou bastante confiante com Google Cloud e Kubernetes para a maioria dos meus cenários.</p>
<p>Aviso completo: não sou especialista, então leve o que digo com um grão de sal. É um daqueles assuntos que tenho muita vontade de falar, mas também sou muito cuidadoso com a escolha das palavras para você não ter uma ideia errada sobre as soluções propostas.</p>
<p>O objetivo desse exercício é principalmente guardar alguns trechos e pensamentos para consulta futura. Então tenha em mente que isso também não é um tutorial passo a passo. Minha primeira intenção foi ir por esse caminho, mas aí percebi que seria quase como escrever um livro inteiro, então não dessa vez.</p>
<p>Para ter sucesso com algo como Google Cloud e Kubernetes, você <strong>precisa</strong> ter experiência em infraestrutura. Se você nunca instalou boxes Linux de nível servidor do zero, se nunca fez otimizações de servidor, se não está confortável com componentes bare-metal do lado do servidor, não tente um deploy real em produção. Sua aposta mais segura ainda é algo como o Heroku.</p>
<p>Você tem que ser o tipo de pessoa que gosta de fuçar nas coisas (como provavelmente você já me leu fazendo em <a href="http://www.akitaonrails.com/linux"target="_blank" rel="noopener">posts anteriores</a> do blog).</p>
<p>Não sei tudo, mas sei o suficiente. Então só precisei descobrir quais peças se encaixariam melhor nas minhas necessidades. Você <strong>precisa</strong> definir suas necessidades antes de tentar escrever seu primeiro arquivo YAML. Planejamento é crucial.</p>
<p>Antes de tudo, isso é o que eu queria/precisava:</p>
<ul>
<li>Camada de aplicação web escalável, onde eu pudesse fazer tanto rolling updates (para atualizações com <strong>zero downtime</strong>) quanto escalamento horizontal automático e manual dos servidores.</li>
<li>Storage persistente montável <strong>com</strong> snapshots/backups automáticos.</li>
<li>Banco de dados robusto gerenciado (Postgresql) com backups automáticos e <strong>replicação fácil</strong> para instâncias somente leitura.</li>
<li>Solução gerenciada para armazenar secrets (como o suporte a ENV do Heroku). Nunca armazene configuração de produção no código-fonte.</li>
<li>Suporte a imagens Docker sem precisar construir infraestrutura customizada para deploy.</li>
<li>Endereços IP externos estáticos para integrações que exigiam IP fixo.</li>
<li>Terminação SSL para poder conectar ao CloudFlare (CDN é obrigatório, mas não suficiente — em 2018 precisamos de algum nível de proteção contra DDoS).</li>
<li>Segurança suficiente por padrão, para que tudo fique — em teoria — bloqueado a menos que eu decida abrir.</li>
<li>Alta disponibilidade em diferentes regiões e zonas de data center.</li>
</ul>
<p>É fácil fazer deploy de uma aplicação web demo simples. Mas eu não queria uma demo, queria uma solução de nível produção para o longo prazo. Melhorias na minha implementação são muito bem-vindas, então fique à vontade para comentar abaixo.</p>
<p>Alguns dos problemas para quem está começando:</p>
<ul>
<li>A documentação é <em>muito</em> extensa, e você vai encontrar <em>quase</em> tudo — se souber o que está procurando. Também tenha em mente que Azure e AWS também implementam Kubernetes com algumas diferenças, então parte da documentação não se aplica ao Google Cloud e vice-versa.</li>
<li>Existem muitos recursos em estágios alpha, beta e stable. A documentação acompanha bem, mas a maioria dos tutoriais com alguns meses pode não funcionar mais como esperado (esse aqui incluído — estou assumindo Kubernetes 1.8.4-gke).</li>
<li>Existe um conjunto de palavras que se aplicam a conceitos que você já conhece mas têm nomes diferentes. Se acostumar com o vocabulário pode atrapalhar no começo.</li>
<li>Parece que você está brincando com Lego. Muitas peças que você pode misturar e combinar. É fácil de bagunçar. Isso significa que você pode construir uma configuração adaptada às suas necessidades. Mas se você só copiar e colar de tutoriais, você <strong>vai</strong> ficar preso.</li>
<li>Você pode fazer <em>quase</em> tudo através de arquivos YAML e linha de comando, mas não é trivial reutilizar a configuração (para ambientes de produção e staging, por exemplo). Existem ferramentas de terceiros que lidam com bits YAML parametrizáveis e reutilizáveis, mas eu faria tudo à mão primeiro. Nunca, jamais, tente templates automatizados em infraestrutura sem saber exatamente o que eles fazem.</li>
<li>Você tem 2 ferramentas de linha de comando <em>pesadas</em>: <code>gcloud</code> e <code>kubectl</code>, e a parte confusa é que elas nomeiam algumas coisas de forma diferente mesmo sendo as mesmas &ldquo;coisas&rdquo;. Pelo menos, <code>kubectl</code> é próximo do <code>docker</code>, se você estiver familiarizado com ele.</li>
</ul>
<p>Mais uma vez, isso <strong>NÃO</strong> é um tutorial passo a passo. Vou anotar alguns passos mas não tudo.</p>
<h3>Camada Web Escalável (a própria aplicação web)<span class="hx:absolute hx:-mt-20" id="camada-web-escalável-a-própria-aplicação-web"></span>
    <a href="#camada-web-escal%c3%a1vel-a-pr%c3%b3pria-aplica%c3%a7%c3%a3o-web" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>A primeira coisa que você precisa ter é uma aplicação web totalmente compatível com os 12 fatores.</p>
<p>Seja Ruby on Rails, Django, Laravel, Node.js ou o que for. Deve ser uma aplicação totalmente shared-nothing, que não depende de escrever nada no sistema de arquivos local. Uma que você possa facilmente desligar e iniciar instâncias independentemente. Sem sessão no estilo antigo em memória local ou em arquivos locais (prefiro evitar session affinity). Sem uploads para o sistema de arquivos local (se for necessário, você terá que montar um storage persistente externo) — sempre prefira enviar streams binários para serviços de storage gerenciados.</p>
<p>Você precisa ter um <a href="https://tomanistor.com/blog/cache-bust-that-asset/"target="_blank" rel="noopener">pipeline adequado que produza cache-busting através de fingerprinting de assets</a> (e gostando ou não, o Rails ainda tem a melhor solução out-of-the-box no seu Asset Pipeline). Você não quer se preocupar com invalidar caches em CDNs manualmente.</p>
<p>Instrumente sua aplicação, adicione o <a href="https://rpm.newrelic.com"target="_blank" rel="noopener">New Relic RPM</a>, adicione o <a href="https://rollbar.com"target="_blank" rel="noopener">Rollbar</a>.</p>
<p>De novo, isso é 2018 — você não quer fazer deploy de código ingênuo com injeção de SQL (ou qualquer outro tipo de input), sem <code>eval</code> sem verificação no seu código, sem brecha para CSRF ou XSS, etc. Vá em frente, compre a licença do <a href="https://brakemanpro.com/"target="_blank" rel="noopener">Brakeman Pro</a> e adicione ao seu pipeline de CI. Posso esperar&hellip;</p>
<p>Como isso não é um tutorial, vou assumir que você é mais do que capaz de se cadastrar no Google Cloud e encontrar seu caminho para configurar um projeto, configurar sua região e zona.</p>
<p>Demorei um pouco para entender a estrutura inicial no Google Cloud:</p>
<ul>
<li>Você começa com um <a href="https://cloud.google.com/resource-manager/docs/creating-managing-projects"target="_blank" rel="noopener">Projeto</a>, que é o guarda-chuva para tudo que sua aplicação precisa.</li>
<li>Em seguida você cria <a href="https://cloud.google.com/kubernetes-engine/docs/concepts/cluster-architecture"target="_blank" rel="noopener">&ldquo;clusters&rdquo;</a>. Você pode ter um cluster de produção ou staging, por exemplo. Ou um cluster web e um cluster de serviços separado para coisas não-web, e assim por diante.</li>
<li>Um cluster tem um <a href="https://cloud.google.com/kubernetes-engine/docs/concepts/cluster-architecture#master"target="_blank" rel="noopener">&ldquo;cluster-master&rdquo;</a>, que é o controlador de tudo o mais (os comandos <code>gcloud</code> e <code>kubectl</code> falam com suas APIs).</li>
<li>Um cluster tem muitas <a href="https://cloud.google.com/kubernetes-engine/docs/concepts/cluster-architecture#nodes"target="_blank" rel="noopener">&ldquo;node instances&rdquo;</a>, as &ldquo;máquinas&rdquo; propriamente ditas (ou, mais precisamente, instâncias de VM).</li>
<li>Cada cluster também tem pelo menos um <a href="https://cloud.google.com/kubernetes-engine/docs/concepts/node-pools"target="_blank" rel="noopener">&ldquo;node pool&rdquo;</a> (o &ldquo;default-pool&rdquo;), que é um conjunto de node instances com a mesma configuração, o mesmo <a href="https://cloud.google.com/compute/docs/machine-types"target="_blank" rel="noopener">&ldquo;machine-type&rdquo;</a>.</li>
<li>Por fim, cada node instance executa um ou mais &ldquo;pods&rdquo; que são containers leves como LXC. É aqui que sua aplicação realmente vive.</li>
</ul>
<p>Este é um exemplo de <a href="https://cloud.google.com/sdk/gcloud/reference/container/clusters/create"target="_blank" rel="noopener">criação de 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>Como mencionei, ele também cria um <code>default-pool</code> com um <a href="https://cloud.google.com/compute/docs/machine-types"target="_blank" rel="noopener">machine-type</a> de <code>n1-standard-4</code>. Escolha qual combinação de CPU/RAM você vai precisar para sua aplicação específica com antecedência. O tipo que escolhi tem 4 vCPUs e 15GB de RAM.</p>
<p>Por padrão ele começa com 3 nodes, então escolhi 2 inicialmente mas com auto-scaling até 5 (você pode atualizar isso depois se precisar, mas garanta que tem espaço para crescimento inicial). E você pode continuar adicionando node-pools extras para node instances de tamanhos diferentes — digamos, para workers do Sidekiq fazerem processamento pesado em background. Então você deve criar um Node Pool separado com um machine-type diferente para seu conjunto de node instances, por exemplo:</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>Esse outro pool controla 1 node do tipo <code>n1-highcpu-8</code> que tem 8 vCPUs com 7,2 GB de RAM. Mais CPUs, menos memória. Existe uma categoria <code>highmem</code> que tem menos CPUs com muito mais memória. De novo, saiba o que você quer com antecedência.</p>
<p>A parte importante aqui é o <code>--node-labels</code> — é assim que vou mapear o deployment para escolher entre Node Pools (neste caso, entre o <code>default-pool</code> e o <code>large-pool</code>).</p>
<p>Após criar um cluster, você precisa executar o seguinte comando para buscar suas credenciais:</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>Isso configura o comando <code>kubectl</code> também. Se você tiver mais de um cluster (digamos, um <code>my-web-production</code> e <code>my-web-staging</code>), precisa ter muito cuidado de sempre fazer <code>get-credentials</code> para o cluster correto primeiro, caso contrário pode acabar executando um deploy de staging no cluster de produção.</p>
<p>Como isso é confuso, modifiquei meu ZSH PROMPT para sempre mostrar com qual cluster estou lidando. Adaptei do <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>Como você acabará tendo múltiplos clusters em uma aplicação grande, recomendo fortemente adicionar esse PROMPT ao seu shell.</p>
<p>Agora, como você faz deploy da sua aplicação nos pods dentro dessas node instances?</p>
<p>Você precisa ter um <code>Dockerfile</code> no repositório do seu projeto de aplicação para gerar uma imagem Docker. Este é um exemplo para uma aplicação Ruby on Rails:</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>No Google Cloud Web Console, você encontrará um <a href="https://cloud.google.com/container-registry/"target="_blank" rel="noopener">&ldquo;Container Registry&rdquo;</a>, que é um Registry Docker Privado.</p>
<p>Você precisa adicionar a URL remota à sua configuração local assim:</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>Agora você pode fazer <code>git push gcloud master</code>. Recomendo também adicionar <a href="https://cloud.google.com/container-builder/docs/running-builds/automate-builds"target="_blank" rel="noopener">triggers</a> para taggear suas imagens. Adiciono 2 triggers: um para taggeá-la com <code>latest</code> e outro para taggeá-la com um número de versão aleatório. Você vai precisar deles mais tarde.</p>
<p>Depois de adicionar o repositório do registry como um remote na sua configuração git (<code>git remote add</code>) e fazer push nele, ele deve começar a construir sua imagem Docker com as tags adequadas que você configurou com os triggers.</p>
<p>Certifique-se de que sua aplicação Ruby on Rails não tem nada nos initializers que exija uma conexão com o banco de dados, pois ele não está disponível. Isso é algo com que você pode travar quando seu build Docker falha por causa da tarefa <code>assets:precompile</code> que carregou um initializer que acidentalmente chama um Model — e isso dispara o <code>ActiveRecord::Base</code> tentando se conectar.</p>
<p>Além disso, certifique-se de que a versão do Ruby no <code>Dockerfile</code> corresponde à do <code>Gemfile</code>, caso contrário também vai falhar.</p>
<p>Reparou no estranho <code>config/application.yml</code> acima? Esse é do <a href="https://github.com/laserlemon/figaro"target="_blank" rel="noopener">figaro</a>. Também recomendo usar algo para facilitar a configuração de variáveis ENV no seu sistema. Não gosto dos secrets do Rails, e ele não é exatamente amigável para a maioria dos sistemas de deploy depois que o Heroku popularizou as ENV vars. Fique com ENV vars. O Kubernetes também vai agradecer por isso.</p>
<p>Agora, você pode sobrescrever qualquer variável ENV do arquivo YAML de Deployment do Kubernetes. Agora é uma boa hora para mostrar um exemplo disso. Você pode nomeá-lo <code>deploy/web.yml</code> ou como preferir e — claro — comitá-lo no seu repositório de código-fonte.</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"># ... obviamente reduzi as muitas ENV vars por brevidade</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"># ... esta parte abaixo é obrigatória para 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>Tem muita coisa acontecendo aqui. Então deixa eu quebrar um pouco:</p>
<ul>
<li>O <code>kind</code> e o <code>apiVersion</code> são importantes — você precisa ficar de olho na documentação se eles mudarem. Isso é o que chamamos de <a href="https://kubernetes.io/docs/concepts/workloads/controllers/deployment/"target="_blank" rel="noopener">Deployment</a>. Antigamente existia um Replication Controller (você encontrará em tutoriais antigos), mas não está mais em uso. A recomendação é usar um <a href="https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/"target="_blank" rel="noopener">ReplicaSet</a>.</li>
<li>Nomeie as coisas corretamente — aqui você tem <code>metadata:name</code> com <code>web</code>. Preste muita atenção também ao <code>spec:template:metadata:labels</code> onde estou rotulando cada pod com um label de <code>app: web</code>, você vai precisar disso para poder selecionar esses pods mais tarde na seção Service abaixo.</li>
<li>Em seguida tenho o <code>spec:strategy</code> onde configuramos o Rolling Update, então se você tem 10 pods, ele vai terminar um, inicializar o novo e continuar fazendo isso, sem nunca derrubar tudo de uma vez.</li>
<li><code>spec:replicas</code> declara quantos Pods quero de uma vez. Você terá que calcular manualmente o machine-type do node-pool e então dividir quantos CPUs/RAM totais você tem pelo quanto precisa para cada instância de aplicação.</li>
<li>Lembra da imagem Docker que geramos acima com a tag &rsquo;latest&rsquo;? Você a referencia em <code>spec:template:spec:containers:image</code></li>
<li>Estou usando o Passenger com configuração de produção (confira a <a href="https://www.phusionpassenger.com/library/config/reference/"target="_blank" rel="noopener">documentação do Phusion</a>, não copie isso só assim).</li>
<li>Na seção <code>spec:template:spec:containers:env</code> posso sobrescrever as ENV vars com os secrets reais de produção. E você vai notar que posso hard-codar valores ou usar essa estranha construção:</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>Aqui está referenciando um armazenamento de <a href="https://kubernetes.io/docs/concepts/configuration/secret/"target="_blank" rel="noopener">&ldquo;Secret&rdquo;</a> que nomeei &ldquo;my-env&rdquo;. E é assim que você cria o seu:</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>Leia a documentação pois você pode carregar arquivos de texto em vez de declarar tudo pela linha de comando.</p>
<p>Como disse antes, prefiro usar um serviço gerenciado para o banco de dados. Você pode definitivamente carregar sua própria imagem Docker, mas realmente não recomendo. O mesmo vale para outros serviços similares a banco de dados como Redis, Mongo. Se você vem da AWS, o <a href="https://cloud.google.com/sql/docs/postgres/"target="_blank" rel="noopener">Google Cloud SQL</a> é como o RDS.</p>
<p>Após criar sua instância PostgreSQL você não consegue acessá-la diretamente da aplicação web. No final, você tem um boilerplate para uma segunda imagem Docker, um <a href="https://cloud.google.com/sql/docs/mysql/connect-kubernetes-engine"target="_blank" rel="noopener">&ldquo;CloudSQL Proxy&rdquo;</a>.</p>
<p>Para isso funcionar você precisa primeiro criar uma 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>Após criar a instância PostgreSQL ela vai pedir para você baixar uma credencial JSON, então tenha cuidado e salve-a em algum lugar seguro. Não preciso dizer que você também deve gerar uma senha forte e segura. Em seguida precisa criar secrets extras:</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>Esses são referenciados nesta parte do 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>Veja que você precisa adicionar o nome do banco de dados (&ldquo;my-db&rdquo; neste exemplo) na cláusula <code>-instance</code> no comando.</p>
<p>A propósito, o <code>gce-proxy:latest</code> se referia à versão 1.09 na época em que esse post foi publicado. Mas já havia uma versão 1.11. Essa me deu dores de cabeça, derrubando conexões e adicionando um timeout super longo. Então voltei para a 1.09 (latest) e tudo funcionou como esperado. Fique atento! Nem tudo que é novinho é bom. Em infraestrutura, você quer ficar com o que é estável.</p>
<p>Você também pode querer a opção de carregar uma instância CloudSQL separada em vez de tê-la em cada pod, para que os pods pudessem se conectar a apenas um proxy. Pode ser interessante ler <a href="https://github.com/GoogleCloudPlatform/cloudsql-proxy/issues/49"target="_blank" rel="noopener">essa thread</a> sobre o assunto.</p>
<p>Parece que nada é exposto a nada a menos que você diga explicitamente. Então precisamos expor esses pods através do que chamamos de <a href="https://kubernetes.io/docs/concepts/services-networking/service/#type-nodeport"target="_blank" rel="noopener">Node Port Service</a>. Vamos criar um arquivo <code>deploy/web-svc.yaml</code> também:</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>É por isso que destaquei a importância do <code>spec:template:metadata:labels</code>, para que possamos usá-lo aqui no <code>spec:selector</code> para selecionar os pods corretos.</p>
<p>Agora podemos fazer deploy desses 2 assim:</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>E você pode ver os pods sendo criados com <code>kubectl get pods --watch</code>.</p>
<h2>O Load Balancer<span class="hx:absolute hx:-mt-20" id="o-load-balancer"></span>
    <a href="#o-load-balancer" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Muitos tutoriais vão expor esses pods diretamente através de um Service diferente, chamado <a href="https://kubernetes.io/docs/tasks/access-application-cluster/create-external-load-balancer/"target="_blank" rel="noopener">Load Balancer</a>. Não tenho tanta certeza sobre como isso se comporta sob pressão e se tem terminação SSL, etc. Então decidi ir com tudo com um <a href="https://kubernetes.io/docs/concepts/services-networking/ingress/"target="_blank" rel="noopener">Ingress</a> Load Balancer usando o <a href="https://www.nginx.com/products/nginx/kubernetes-ingress-controller/"target="_blank" rel="noopener">NGINX Controller</a>.</p>
<p>Primeiro de tudo, decidi criar um node-pool separado para ele, por exemplo, assim:</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>Assim como quando criamos o exemplo de <code>large-pool</code>, aqui você precisa cuidar de adicionar <code>--node-labels</code> para que o controller seja instalado aqui em vez do <code>default-pool</code>. Você vai precisar saber o nome da node instance, podemos fazer isso assim:</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>Vamos salvar assim por enquanto:</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>Você pode reservar manualmente um IP externo e dar um nome a ele assim:</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>Para fins de exemplo, digamos que ele gerou um IP reservado &ldquo;111.111.111.111&rdquo;. Então vamos buscá-lo e salvá-lo por enquanto assim:</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>Por fim, vamos vincular esse endereço à node instance do load balancer:</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>Feito isso, podemos adicionar o resto da configuração de Deployment do Ingress. Vai ser um pouco longo, mas é basicamente boilerplate. Vamos começar definindo outra aplicação web que chamaremos de <code>default-http-backend</code>, usada para responder a requisições HTTP caso nossos pods web não estejam disponíveis por algum motivo. Vamos chamá-la de <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>Não precisa mudar nada aqui — e a essa altura você já deve estar familiarizado com o template de Deployment. Você já sabe que precisa expô-lo através de um NodePort, então vamos adicionar um <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>De novo, não precisa mudar nada. Os próximos 3 arquivos são as partes importantes. Primeiro, criaremos um NGINX Load Balancer — vamos chamá-lo de <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>Preste atenção ao <code>nodeSelector</code> para usar o node label que adicionamos ao criar o novo node-pool.</p>
<p>Você pode querer mexer nos labels, no número de réplicas se precisar. Mas aqui você vai notar que ele monta um volume que nomeei como <code>tls-dhparam-vol</code>. Esses são os <a href="https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html#Forward_Secrecy_&amp;_Diffie_Hellman_Ephemeral_Parameters"target="_blank" rel="noopener">Parâmetros Efêmeros Diffie Hellman</a>. É assim que geramos:</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>Note também que estou usando a versão &ldquo;0.9.0-beta_5&rdquo; para a imagem do controller. Funciona bem, sem problemas até agora. Mas fique de olho nas release notes para versões mais novas e faça seus próprios testes.</p>
<p>De novo, vamos expor esse controller Ingress através do Load Balancer Service. Vamos chamá-lo de <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>Lembra do IP externo estático que reservamos acima e salvamos na ENV var <code>LB_INGRESS_IP</code>? Esse é o que precisa colocar na seção <code>spec:loadBalancerIP</code>. Esse também é o IP que você vai adicionar como &ldquo;registro A&rdquo; no seu serviço DNS (digamos, mapeando seu &ldquo;<a href="https://www.my-app.com.br"target="_blank" rel="noopener">www.my-app.com.br</a>&rdquo; no CloudFlare).</p>
<p>Por fim, podemos criar a configuração do Ingress em si — vamos criar um <code>deploy/ingress.yml</code> assim:</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>Cuidado com as annotations acima que conectam tudo. Ele vincula o serviço NodePort que criamos para os pods web com o nginx ingress controller e adiciona terminação SSL através daquele <code>spec:tls:secretName</code>. Como você cria isso? Primeiro precisa comprar um certificado SSL — usando o CloudFlare como exemplo novamente.</p>
<p>Quando terminar a compra, o provedor deve te dar os arquivos secretos para download (guarde-os em segurança! uma pasta pública do Dropbox não é segura!). Então precisa adicioná-los à infraestrutura assim:</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>Agora que editamos um monte de arquivos, podemos fazer deploy de todo o stack do load balancer:</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>Essa configuração do NGINX Ingress é baseada no <a href="https://zihao.me/post/cheap-out-google-container-engine-load-balancer/"target="_blank" rel="noopener">post do blog do Zihao Zhang</a>. Existem também exemplos no <a href="https://github.com/kubernetes-incubator/external-dns/blob/master/docs/tutorials/nginx-ingress.md"target="_blank" rel="noopener">repositório kubernetes incubator</a>. Vale a pena conferir também.</p>
<p>Se você fez tudo certo até aqui, <code>https://www.my-app-com.br</code> deve carregar sua aplicação web. Pode ser interessante verificar o Time to First-Byte (TTFB). Você pode fazer isso passando pelo CloudFlare assim:</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>Ou, se estiver com TTFB lento, pode contornar o CloudFlare fazendo isso:</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>O TTFB deve ficar em torno de 1 segundo ou menos. Qualquer coisa muito acima disso pode indicar um problema na sua aplicação. Verifique os machine types da sua node instance, o número de workers carregados por pod, a versão do CloudSQL proxy, a versão do NGINX controller e assim por diante. Isso é um processo de tentativa e erro, pelo que sei. Cadastre-se em serviços como <a href="https://loader.io"target="_blank" rel="noopener">Loader</a> ou mesmo <a href="https://www.webpagetest.org"target="_blank" rel="noopener">Web Page Test</a> para ter insights.</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>Agora, com tudo funcionando, como realizamos o Rolling Update que mencionei no início? Primeiro você faz <code>git push</code> para o repositório do Container Registry e aguarda a construção da imagem Docker.</p>
<p>Lembra que disse para deixar um trigger tagear a imagem com um número de versão aleatório? Vamos usá-lo (você pode ver isso na lista de Build History no Container Registry, no console do Google Cloud):</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>Você deve usar o mesmo nome e imagem declarados no <code>deploy/web.yml</code> acima. Isso vai começar a fazer rollout da atualização adicionando um novo pod, então encerrando um pod e assim por diante, sem downtime para seus usuários.</p>
<p>Rolling updates precisam ser executados com cuidado. Por exemplo, se seu novo deploy exige uma migração de banco de dados, você precisa adicionar uma janela de manutenção (ou seja: faça isso quando houver pouco ou nenhum tráfego, como de madrugada). Então você pode executar o comando migrate assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>kubectl get pods # para obter o nome de um pod

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

# você também pode entrar em bash em um pod assim, mas lembre que este é um container efêmero, então arquivos que você editar e escrever ali desaparecem no próximo 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>Para fazer redeploy de tudo sem recorrer ao rolling update, você precisa fazer isso:</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>Você encontrará uma explicação mais completa no <a href="https://tachingchen.com/blog/Kubernetes-Rolling-Update-with-Deployment/"target="_blank" rel="noopener">blog do Ta-Ching</a>.</p>
<h2>Bônus: Auto Snapshots<span class="hx:absolute hx:-mt-20" id="bônus-auto-snapshots"></span>
    <a href="#b%c3%b4nus-auto-snapshots" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Um item que tinha na minha lista de &ldquo;queria/precisava&rdquo; no início é a capacidade de ter storage persistente montável com backups/snapshots automáticos. O Google Cloud fornece metade disso por enquanto. Você pode criar discos persistentes para montar nos seus pods, mas não tem um recurso para fazer backup automático dele. Pelo menos tem APIs para criar snapshots manualmente.</p>
<p>Para este exemplo, vamos criar um novo disco SSD e formatá-lo primeiro:</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>O último comando é para copiarmos o nome de uma node instance. Digamos que seja <code>gke-my-web-app-default-pool-123-123</code>. Vamos anexar o disco <code>my-data</code> a ela:</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>O último comando faz SSH na instância. Podemos listar os discos anexados com <code>sudo lsblk</code> e você verá o disco de 500GB, provavelmente como <code>/dev/sdb</code>, mas certifique-se de que está correto porque vamos formatá-lo!</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>Agora podemos sair da sessão SSH e desanexar o disco:</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>Você pode montar esse disco nos seus pods adicionando o seguinte ao seu yaml de deployment:</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>Agora, vamos criar um arquivo de deployment CronJob como <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>Como já fizemos antes, você precisará criar outra Service Account com permissões de editor na seção &ldquo;IAM &amp; admin&rdquo; do console do Google Cloud, depois baixar a credencial JSON e, por fim, fazer upload dela assim:</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>Note também que, como um cron job normal, existe um parâmetro de schedule que você pode querer alterar. No exemplo, &ldquo;0 4 * * *&rdquo; significa que o snapshot vai rodar todo dia às 4h da manhã.</p>
<p>Confira o <a href="https://github.com/grugnog/google-cloud-auto-snapshot"target="_blank" rel="noopener">repositório original</a> dessa solução para mais detalhes.</p>
<p>E por agora é isso!</p>
<p>Como disse no início, este não é um procedimento completo, apenas destaques de algumas das partes importantes. Se você é novo no Kubernetes, acabou de ler sobre Deployment, Service, Ingress, mas ainda tem ReplicaSet, DaemonSet e muito mais para explorar.</p>
<p>Acho que já está longo demais para adicionar uma explicação de configuração de <a href="https://cloud.google.com/sql/docs/mysql/high-availability"target="_blank" rel="noopener">Alta Disponibilidade multi-região</a>, então vamos deixar por aqui.</p>
<p>Correções ou sugestões são muito bem-vindas, pois ainda estou no processo de aprendizado, e tem um monte de coisas que eu mesmo ainda não sei.</p>
]]></content:encoded><category>rails</category><category>cloud</category><category>containers</category><category>devops</category></item><item><title>Escolhendo MatterMost no lugar de Rocket.chat e Slack</title><link>https://akitaonrails.github.io/2016/08/13/escolhendo-mattermost-no-lugar-de-rocket-chat-e-slack/</link><guid isPermaLink="true">https://akitaonrails.github.io/2016/08/13/escolhendo-mattermost-no-lugar-de-rocket-chat-e-slack/</guid><pubDate>Sat, 13 Aug 2016 22:41:00 -0300</pubDate><description>&lt;p&gt;Eu sou um tecnólogo, basicamente um nerd. Então, quando fico obcecado com alguma questão técnica, por menor que seja, simplesmente não consigo dormir tranquilo até encontrar uma solução razoável.&lt;/p&gt;
&lt;p&gt;Meu time, meus clientes, todos nós usamos Slack felizes por mais de 2 anos, como acredito que muitos times pelo mundo também usaram. Apesar de ninguém nunca ter reclamado, eu sempre me incomodava com pequenos detalhes. Em primeiro lugar, como tudo que tem valor, custa. Ou pago USD 6.67/mês/usuário, ou aceito as restrições do plano gratuito. E, como a maioria dos outros times, fui levando com as restrições enquanto deu.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Eu sou um tecnólogo, basicamente um nerd. Então, quando fico obcecado com alguma questão técnica, por menor que seja, simplesmente não consigo dormir tranquilo até encontrar uma solução razoável.</p>
<p>Meu time, meus clientes, todos nós usamos Slack felizes por mais de 2 anos, como acredito que muitos times pelo mundo também usaram. Apesar de ninguém nunca ter reclamado, eu sempre me incomodava com pequenos detalhes. Em primeiro lugar, como tudo que tem valor, custa. Ou pago USD 6.67/mês/usuário, ou aceito as restrições do plano gratuito. E, como a maioria dos outros times, fui levando com as restrições enquanto deu.</p>
<p>Por exemplo, para me livrar dos avisos para fazer upgrade porque batemos no limite de uploads, criei uma pequena ferramenta chamada <a href="https://github.com/akitaonrails/slack_cleanup"target="_blank" rel="noopener">slack_cleanup</a> (primeiro em Elixir e depois em <a href="https://github.com/akitaonrails/cr_slack_cleanup"target="_blank" rel="noopener">Crystal</a>, só por exercício). Isso ajudou por um tempo.</p>
<p>Nosso time foi crescendo, de forma constante e frequente, assim como os clientes. Quanto mais usuários adicionamos, mais conversas, mais rápido batemos nas restrições. Histórico se perde com mais frequência, precisamos limpar uploads com mais frequência. Isso cansa muito rápido.</p>
<p>Uma coisa que valorizo acima de tudo é conhecimento. Como pequeno exemplo, eu mesmo mantenho múltiplos backups de todos os meus emails, todos os meus projetos, todos os assets que produzi nos últimos 20 anos. Caramba, tenho um sistema Drobo de 6 TB em Raid-5 bem na minha mesa de casa e mais 3 HDs externos de 1TB para backup. Perdi muito pouco ao longo dos anos.</p>
<p>Realmente me incomoda quando perco informação.</p>
<ul>
<li>
<p>Gmail Business, DropBox, buckets do AWS S3, por serem serviços externos, não me preocupam, pois mantenho cópias de tudo offline. Então, se essas contas forem comprometidas de uma hora para outra, tenho múltiplas cópias.</p>
</li>
<li>
<p>GitHub me incomodava um pouco porque, embora eu tenha múltiplas cópias dos repositórios, perderia todo o histórico de Pull Requests e Issues. Esse é um dos motivos pelos quais migrei para o meu próprio GitLab e ajudei a ajustar o processo de import para puxar esse histórico de Pull Requests.</p>
</li>
<li>
<p>O Slack me incomoda muito pelos motivos já mencionados, e por isso na semana passada testamos primeiro o MatterMost e depois subimos o 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>Saímos do Slack para o Rocket.chat e, alguns dias depois, estamos migrando de novo, agora para o Mattermost.</p>
<p>Sim, isso foi trabalhoso. Meu time não ficou muito feliz em deixar o Slack. O Slack é realmente elegante, completo, bonito, redondinho, um produto web de verdade para esta geração. Qualquer alternativa precisa ser pelo menos quase tão bonita e ter funcionalidades sem bugs, incluindo webhooks.</p>
<p>O Mattermost atende quase perfeitamente, mas a Team Edition open source carecia de uma funcionalidade importante para mim: a possibilidade de impedir que membros comuns renomeiem e/ou apaguem grupos privados. Sim, esperamos que adultos se comportem, mas quando você tem times remotos, clientes remotos, usuários externos sem nenhum compromisso com a empresa, isso vira uma dor de cabeça.</p>
<p>Sim, eu poderia, e provavelmente deveria, usar a Enterprise Edition paga. Mas só por essa pequena funcionalidade, achei caro demais. Isso me motivou a deixar pra lá e instalar o Rocket.chat. Movi todo o meu time pra lá (quase 50 pessoas, porque ainda não migrei os clientes). Esse seria o fim da história.</p>
<p>Mas o Rocket.chat tem uma infraestrutura complexa para lidar (você precisa ter no mínimo 3 máquinas, ou pagar a mais por um SaaS de Mongodb decente). Aliás, <a href="http://www.akitaonrails.com/2016/08/09/moving-away-from-slack-into-rocket-chat-good-enough"target="_blank" rel="noopener">no meu post anterior</a> expliquei por que vocês, pessoal de MEAN, não devem ser desleixados ao lidar com Mongodb. Resumindo: Mongodb não foi pensado para rodar em uma única instância, você precisa ter um replica set. Se você tem um Mongodb em instância única, está fazendo errado.</p>
<p>E o mais problemático: o client-side é simplesmente pesado demais. Frequentemente faz a CPU disparar em máquinas não tão potentes. É notavelmente, e mensuravelmente, mais lento de navegar na UI, comparado ao Slack. A UI do MatterMost era muito mais rápida e muito mais responsiva.</p>
<p>Eu estava quase disposto a relevar a falta de uma suíte de testes decente. Estava disposto a tentar conviver com Meteor e CoffeeScript. Estava disposto a lidar com a manutenção complexa do MongoDB.</p>
<p>Mas a lentidão na resposta atingindo vários membros do meu time é um show-stopper, um problema gigante. Desligamos a feature beta de videochat (já que ela sempre dispara picos de CPU em todos os usuários), mas vários membros ainda tinham uma experiência ruim com uma UI lenta demais e devoradora de recursos.</p>
<p>O MatterMost, baseado em React, escrito em ES6, devidamente estruturado, com suítes de teste client-side suficientemente boas, era um candidato mais adequado. Então decidi pensar de verdade no problema original e cheguei a uma solução simples: <a href="http://www.akitaonrails.com/2016/08/12/hackeando-o-mattermost-team-edition"target="_blank" rel="noopener">adicionar uma função PLPGSQL simples</a> para ser disparada sempre que alguém tentasse apagar um canal. Funcionou direitinho. E isso me motivou a chamar meu time de novo e propor essa nova mudança: acredito que todo mundo embarcou, já que o MatterMost estava muito mais rápido nas máquinas deles.</p>
<p>Sei que estou soando muito duro com o Rocket.chat, e essa não é minha intenção. Se não tivéssemos outras opções, ainda iríamos para o Rocket.chat. Mas como o Mattermost provou ser a melhor escolha, foi decisão fácil.</p>
<h3>Instalação do MatterMost<span class="hx:absolute hx:-mt-20" id="instalação-do-mattermost"></span>
    <a href="#instala%c3%a7%c3%a3o-do-mattermost" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Como sempre, não vou cansar você com instruções já disponíveis online. Se quiser instalar tudo manualmente, siga <a href="https://docs.mattermost.com/install/prod-ubuntu.html"target="_blank" rel="noopener">este tutorial</a>, mas uma opção melhor seria <a href="https://docs.mattermost.com/install/prod-docker.html"target="_blank" rel="noopener">o deployment via Docker</a>. Você pode até instalar/atualizar <a href="http://docs.gitlab.com/omnibus/gitlab-mattermost/"target="_blank" rel="noopener">junto com o GitLab</a>.</p>
<p>Garanta que você tenha NGINX na frente do servidor e que tenha tanto um domínio ou subdomínio devidamente registrado, quanto SSL - use Let&rsquo;s Encrypt.</p>
<p>Como quero continuar mexendo e experimentando com o codebase em uma instalação ativa, instalei tudo manualmente e tenho esta estrutura de diretórios:</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>Tenho um usuário sudo restrito chamado <code>mattermost</code> e tenho um diretório principal <code>mattermost</code> apontando para os pacotes binários que você encontra na <a href="https://www.mattermost.org/download/"target="_blank" rel="noopener">página oficial de download</a>.</p>
<p>Note que tenho uma cópia de <code>mattermost/config/config.json</code> no diretório home. Deixo lá para que, toda vez que eu baixe uma nova versão e refaça o symlink para <code>mattermost</code>, eu possa simplesmente fazer:</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>Garanta também que você ajuste pelo menos o seguinte na 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>Se quiser, você pode mandar os uploads de arquivos para algum bucket S3 que você tenha, e só preencher os detalhes da AWS. Mas se escolher tê-los localmente, mude o diretório para algum lugar fora da pasta <code>mattermost</code>, já que a cada upgrade você vai trocar a pasta. Tanto com AWS EC2 quanto com Digital Ocean você sempre pode escolher adicionar um volume secundário que sobreviva às VMs, então mesmo se chegar a um ponto de ter centenas de usuários simultâneos e quiser escalar horizontalmente, pode ter todas as máquinas apontando para um volume compartilhado (AWS EFS, por exemplo).</p>
<p>Por falar nisso, nessa configuração, atualizar seria assim:</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>A razão de eu querer dessa forma é poder ajustar o código e empurrar as alterações manualmente.</p>
<p>Para sua máquina de desenvolvimento, você deve seguir <a href="https://docs.mattermost.com/developer/developer-setup.html"target="_blank" rel="noopener">estas instruções</a>. Se você está em OS X e escolhe usar o Docker Toolbox, lembre que você não precisa mais do VirtualBox, pois ele vai usar o hypervisor nativo do OS X agora. Na minha máquina, tive que adicionar <code>dockerhost</code> manualmente no meu <code>/etc/hosts</code> porque o <code>boot2docker</code> estava falhando em obter meu IP.</p>
<p>Aí você pode simplesmente clonar o código do 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>Lembre-se de sempre fazer checkout da versão estável correta (v3.3.0 na época em que postei este artigo originalmente) que você tem instalada no seu servidor. De novo, não vou cansar você com o que já está documentado nos links acima, mas você precisa ter Go 1.6(.3), Docker, Docker-Composer, Docker-Machine, tudo já instalado.</p>
<p>Você pode executar <code>make run</code> e, depois que terminar (e, como sempre, o npm vai garantir que isso demore muito), pode abrir <code>http://localhost:8065</code> para brincar localmente.</p>
<p>Mais importante: você pode mexer nos componentes JSX do React em <code>platform/webapp/components</code>, por exemplo, no <code>channel_header.jsx</code>, e adicionar coisas assim:</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="c1">// linha 493 da v3.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>E sabe o que isso faz? Remove as opções &ldquo;Rename Group&rdquo; e &ldquo;Delete Group&rdquo; do menu do canal se o usuário não for um system admin. Agora, como colocar isso no seu servidor?</p>
<p>Antes de mais nada, você tem que editar o <code>Makefile</code> nesta seção:</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>Note que o desenvolvedor que fez esse arquivo provavelmente usa Linux. Isso porque esse arquivo gera o binário Linux-ELF como <code>bin/platform</code>, enquanto o binário OS X Mach-O fica em <code>bin/darwin_amd64/platform</code>. Agora, se como eu você está em OS X, tem que inverter isso e fazer a versão Linux apontar para <code>bin/linux_amd64/platform</code>, senão os pacotes vão ter o binário errado.</p>
<p>Aí você vai notar que existe esta seção bem no fim:</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>Se você está nas versões mais recentes do Docker-Toolbox, não precisa mais do boot2docker, ele vai usar o docker-machine. Então essa linha não funciona, e a forma mais fácil de fazer o <code>make test</code> rodar é trocar o <code>dockerhost</code> por <code>localhost</code> na configuração de PostgreSQL do <code>config/config.json</code>.</p>
<p>Se os testes estão todos passando, você pode rodar <code>make package</code> para gerar todos os pacotes, e então enviar o <code>dist/mattermost-team-edition-linux-amd64.tar.gz</code> para o seu servidor e descompactar no local correto.</p>
<p>Como nota pra noobs em Go como eu, eu tinha meu <code>GOPATH</code> apontando para <code>/Users/akitaonrails/.go</code> e meu projeto estava em <code>/Users/akitaonrails/codeminer42/mattermost/platform</code>. Me disseram que isso está incorreto e foi a fonte de muitas horas de frustração minha. O projeto PRECISA ESTAR DENTRO DO GOPATH.</p>
<p>Então mudei meu <code>GOPATH</code> para <code>/Users/akitaonrails/Sites/go</code> e o projeto para <code>/Users/akitaonrails/Sites/go/src/github.com/mattermost/platform</code> e agora funciona. Provavelmente eu também teria feito symlink do caminho do projeto para dentro do GOPATH. Mas agora tudo compila e roda direitinho.</p>
<p>É assim que vou continuar experimentando por enquanto, até me sentir confortável o suficiente para automatizar todo o processo depois. Isso deve funcionar bem para meu time por ora.</p>
<p>É claro, o snippet acima é apenas um <strong>dirty hack</strong>, não tente contribuir assim. Uma implementação adequada exigiria que eu pelo menos criasse uma nova configuração no <code>config.json</code>, por exemplo <code>TeamSettings.MembersCanManageChannel</code> como <code>false</code>, e mudasse o <code>api/context.go</code> por volta da linha 347 para algo como:</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>Depois mudar o componente <code>webapp/channel_header.jsx</code> no front-end React (junto com testes unitários decentes em <code>webapp/tests/client_channel.test.jsx</code>), garantir que o <code>make test</code> passa, e finalmente abrir um feature request para o core team.</p>
<p>Mas a ideia aqui era só mostrar que não foi tão difícil resolver o show-stopper para o meu cenário particular, tanto usando o <a href="http://www.akitaonrails.com/2016/08/12/hackeando-o-mattermost-team-edition"target="_blank" rel="noopener">trigger SQL</a> para proteger o banco quanto o hack para arrumar a Web UI.</p>
<h3>Conclusão<span class="hx:absolute hx:-mt-20" id="conclusão"></span>
    <a href="#conclus%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Mas e a feature mais importante de todas? Suporte a RightGIF? Ainda não existe nada tão simples quanto um comando rightgif do Slack. Felizmente, dá pra compensar a maior parte dessas pequenas faltas instalando um <a href="https://www.npmjs.com/package/hubot-mattermost"target="_blank" rel="noopener">servidor Hubot</a>, e ligando a um usuário para que você possa conversar com o bot e fazer ele fazer coisas pra você (programar um alarme, buscar um gif, traduzir texto, etc). Como ressalva, o adaptador do hubot exige o uso do <a href="https://github.com/loafoe/mattermost-client"target="_blank" rel="noopener">mattermost-client</a>, que precisa ser sincronizado com as releases da plataforma do servidor para funcionar direito, então tome cuidado ao atualizar.</p>
<p>No geral, o Mattermost é uma ótima escolha. Também não é para amadores; o ambiente de desenvolvimento exige que você domine seu Docker. Exige que você saiba configurar Go-lang direito. Exige que você siga os <a href="https://docs.mattermost.com/developer/contribution-guide.html"target="_blank" rel="noopener">procedimentos adequados para contribuir</a>, como deveria ser. O projeto em si é um único codebase dividido em aproximadamente 2 aplicações, uma API HTTP em Go e um front-end em React para consumir as APIs. Tudo no projeto é automatizado através do uso adequado de imagens Docker (para mysql, postgresql, instâncias openldap) e Makefiles para rodar a suíte de testes, criar pacotes, etc.</p>
<p>E ele tem algumas conveniências do Slack que o Rocket.chat ainda não tem, como um atalho simples para trocar de canal (Cmd-K), a capacidade de responder mensagens e organizá-las como threads, suporte a Markdown adequado e mais completo. No geral, as funcionalidades são redondas e bem acabadas. O que está lá é sólido e funciona; é sempre ruim ter funcionalidades pela metade.</p>
<p>Com esses hacks em mente, posso recomendar fortemente que você use Mattermost. E como já disse em posts anteriores, isso vai além de uma questão de custo. Se você é um time pequeno, sem desenvolvedores internos ou alguém que possa manter sua própria instalação, definitivamente deveria pagar pelo suporte Enterprise do Mattermost; é acessível e bem mais barato que o Slack.</p>
<p>Por enquanto, é a melhor escolha, tanto em termos de acabamento geral, funcionalidades completas, UI simples e responsiva, qualidade de código e o cuidado geral com a engenharia.</p>
]]></content:encoded><category>reviews</category><category>containers</category></item></channel></rss>