<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Devops | AkitaOnRails.com</title><link>https://akitaonrails.github.io/en/tags/devops/</link><description>Fabio Akita's blog — tech, career, and assorted geek topics. English edition.</description><generator>Hugo -- gohugo.io</generator><language>en</language><lastBuildDate>Mon, 19 Oct 2026 23:20:51 +0000</lastBuildDate><atom:link href="https://akitaonrails.github.io/en/tags/devops/index.xml" rel="self" type="application/rss+xml"/><item><title>Stop Making Excuses and Ship More! With AI, the Premise Changed. Get It.</title><link>https://akitaonrails.github.io/en/2026/09/22/stop-making-excuses-and-ship-more-the-premise-changed/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2026/09/22/stop-making-excuses-and-ship-more-the-premise-changed/</guid><pubDate>Tue, 22 Sep 2026 16:00:00 -0300</pubDate><description>&lt;p&gt;Today I fired off a string of posts on X hammering this point, and, like every time I talk about fast deploys, a bunch of people read it as &amp;ldquo;this guy is saying to ship any garbage to production without testing.&amp;rdquo; That&amp;rsquo;s not it. Let me lay it out calmly, because the argument is narrower and more boring than the version that spreads panic.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;It&amp;rsquo;s better to deploy faster, because fixing things afterward became fast and cheap.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Today I fired off a string of posts on X hammering this point, and, like every time I talk about fast deploys, a bunch of people read it as &ldquo;this guy is saying to ship any garbage to production without testing.&rdquo; That&rsquo;s not it. Let me lay it out calmly, because the argument is narrower and more boring than the version that spreads panic.</p>
<blockquote>
  <p>It&rsquo;s better to deploy faster, because fixing things afterward became fast and cheap.</p>

</blockquote>
<p>That sentence is the whole article. Everything else is just me justifying why it&rsquo;s true now, even though it wasn&rsquo;t true a few years ago, and showing how I&rsquo;ve been acting on it myself since February.</p>
<h2>The Premise Changed, and Most People Haven&rsquo;t Noticed<span class="hx:absolute hx:-mt-20" id="the-premise-changed-and-most-people-havent-noticed"></span>
    <a href="#the-premise-changed-and-most-people-havent-noticed" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>I started saying this <a href="https://x.com/AkitaOnRails/status/2102436061083267441"target="_blank" rel="noopener">on X</a> like this:</p>
<blockquote>
  <p>Fixing got cheap. Stop holding things back. Just ship it and fix it after. &ldquo;Ugh, but I risk shipping a bug.&rdquo; The wrong premise is thinking that staring at every line is going to make any difference at scale. And idle code doesn&rsquo;t improve on its own. Only used code lets you find out if it needs improving.</p>

</blockquote>
<p>And I followed up <a href="https://x.com/AkitaOnRails/status/2102476036709450066"target="_blank" rel="noopener">in the second post</a>, because the first read of anything I write in 280 characters always comes back angrier and more literal than intended:</p>
<blockquote>
  <p>Get the concept (this is an X post, not a blog post, stop reading it literally). I&rsquo;m not saying AI doesn&rsquo;t produce bugs. It does, obviously. But at this point, not more than you, the human, also produce. [&hellip;] The old excuse was that when a bug ships to production, it sits there longer than it should, because it never becomes a sprint priority to fix. Now THAT&rsquo;S NOT TRUE ANYMORE, the premise changed: fixing the bug afterward is FAST and CHEAP.</p>

</blockquote>
<p>That&rsquo;s the entire point. It isn&rsquo;t about AI being infallible. It&rsquo;s about the cost of fixing a mistake having collapsed, while most teams still run their review process as if that cost were still high.</p>
<p>With today&rsquo;s frontier models, Opus, Sol, K3, and company, the LLM makes fewer mistakes than most human developers I&rsquo;ve seen go through a code review. When it does err, the overwhelming majority of the time the mistake isn&rsquo;t the model&rsquo;s. It&rsquo;s whoever was driving the harness. The LLM does what you ask, and it doesn&rsquo;t do what you didn&rsquo;t ask.</p>
<p>If you don&rsquo;t know what to ask for, you won&rsquo;t produce anything useful, no matter how good the model is. That&rsquo;s exactly why only a real software engineer can get real production-grade software out of an agent: someone has to know how to break down the problem, validate the result, and recognize when the request itself was poorly framed. AI doesn&rsquo;t replace that judgment. It multiplies what that judgment already knew how to do.</p>
<h2>This Should Have Been the Norm for 25 Years<span class="hx:absolute hx:-mt-20" id="this-should-have-been-the-norm-for-25-years"></span>
    <a href="#this-should-have-been-the-norm-for-25-years" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The discomfort I feel with bureaucratic review processes isn&rsquo;t new, and the industry should have solved it a long time ago. Kent Beck and Ron Jeffries invented Extreme Programming in 1997, on Chrysler&rsquo;s C3 project, and Beck published the book in 1999 with continuous integration, TDD, and small-batch delivery as core practices. In February 2001, seventeen developers locked themselves in a resort in Snowbird, Utah, and walked out with the Agile Manifesto. That&rsquo;s twenty-five years ago.</p>
<p>Martin Fowler&rsquo;s mantra for continuous integration has always been &ldquo;if it hurts, do it more often,&rdquo; and the book <em>Continuous Delivery</em>, by Jez Humble and Dave Farley, formalized the idea that small, frequent deploys reduce risk, not increase it. DORA&rsquo;s annual State of DevOps reports have documented this with numbers for over a decade:</p>
<ul>
<li>elite teams: deploy on demand, multiple times a day;</li>
<li>low-performing teams: deploy once a month, sometimes once every six months.</li>
</ul>
<p>And the finding that&rsquo;s hardest to swallow if you&rsquo;re afraid of deploying fast: historically, more frequent deploys <strong>do not increase</strong> the failure rate. <em>They reduce it.</em> Teams that deploy more, on average across the last decade of reports, fail less and recover faster when they do fail.</p>
<p>Even with all of this documented, measured, and published for over a decade, a large chunk of the industry keeps damming up change into big batches, with slow manual review, and calling that &ldquo;prudence.&rdquo; It was never a settled matter even inside the agile community itself: in 2014, Kent Beck, Martin Fowler, and DHH (Rails&rsquo; creator) spent weeks publicly debating whether TDD was dead, without reaching a clean consensus. In other words, the very community that invented these practices still argues today about how much rigor is enough.</p>
<p>What I&rsquo;m saying is simpler than that whole debate: whatever your testing rigor level is, an LLM reviewing your diff before merge is fast, and reverting a bad deploy became trivial with containers and git. The math that&rsquo;s left is: how much time do you spend holding back code, versus how much time it would take to revert if it went wrong? If the second number is smaller, you&rsquo;re paying a toll for fear, not for prudence.</p>
<h2>This Isn&rsquo;t &ldquo;Ship Anything&rdquo;<span class="hx:absolute hx:-mt-20" id="this-isnt-ship-anything"></span>
    <a href="#this-isnt-ship-anything" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>This is the part that always gets lost: I&rsquo;m not saying to deploy with zero engineering practice underneath. I&rsquo;m saying the opposite. This is only safe <strong>after</strong> you already have basic software engineering working, which I document exhaustively on this blog for years: automated tests, CI running on every push, real revert capability, observability to know when something broke.</p>
<blockquote>
  <p>80/20. Ask the LLM to do a minimal review and ship it. If it breaks, revert (that&rsquo;s what containers, git, etc. are for). If you can&rsquo;t revert, your infra sucks, go fix it yesterday.</p>

</blockquote>
<p>That last sentence is the whole filter. If your infrastructure doesn&rsquo;t let you revert fast, the problem isn&rsquo;t deploy speed. It&rsquo;s your infrastructure, and it needs fixing before any conversation about going faster. Nobody gets a pass to skip having real rollback capability. What changes is that, once you have it, holding back code waiting on human review became pure waste.</p>
<h2>The Minimum Recommended Environment<span class="hx:absolute hx:-mt-20" id="the-minimum-recommended-environment"></span>
    <a href="#the-minimum-recommended-environment" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>I&rsquo;ve already said &ldquo;basic software engineering&rdquo; several times in this piece without spelling out what that means in practice. Here&rsquo;s the list, no hedging. Nothing on it is optional, and none of it got expensive to maintain:</p>
<ul>
<li>unit tests covering isolated logic, TDD or not, as long as they exist;</li>
<li>scenario and integration tests covering concurrency, partial failure, and conflict, the things unit tests alone don&rsquo;t catch;</li>
<li>CI that builds and tests on every change, ideally for every platform you actually ship to;</li>
<li>hostile automated review before merge, like <code>pr-audit</code>, which treats a contributor&rsquo;s claim as unverified until proven;</li>
<li>feature flags to control the blast radius of who sees a change first;</li>
<li>real staging as a gate before any real user sees the change;</li>
<li>fast revert capability, with git and containers, and observability to know when something broke;</li>
<li>in compliance, financial, or health environments, one extra layer: heavier scenario testing, and more than one lock per identified failure mode.</li>
</ul>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/09/22/deploy-environment-diagram-en.png" alt="Diagram of the minimum pipeline for fast, safe deploys: commit, automated tests, cross-platform CI, feature flag, staging, and production, with a rollback arrow looping from production back to tests and a feedback arrow looping from production back to commit"  loading="lazy" /></p>
<p>What the diagram tries to make clear is that speed and safety aren&rsquo;t competing with each other here. They come from the same stack of layers, and each layer in that stack is what gives you permission to stop holding back code waiting for manual review.</p>
<h2>Where This Math Doesn&rsquo;t Work Out<span class="hx:absolute hx:-mt-20" id="where-this-math-doesnt-work-out"></span>
    <a href="#where-this-math-doesnt-work-out" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>I&rsquo;m not going to pretend I didn&rsquo;t get good pushback in the replies on X, because several of them hit on something real that I skipped while writing fast for Twitter. It&rsquo;s worth answering for real instead of just validating whoever agreed.</p>
<ol>
<li>
<p><strong>Reverting code doesn&rsquo;t undo a consequence that already left your system.</strong> If the bug already processed a wrong payment, moved money to the wrong account, or controlled an elevator, reverting the deploy doesn&rsquo;t undo what already happened. &ldquo;If you can&rsquo;t revert, your infra sucks&rdquo; covers the technical side of deploy and rollback, not the case where the damage already left your system before you could react. Payments, health, and anything touching physical safety fall into this category, and there the review bar before shipping has to get stricter, not looser, because the cost of reverting stopped being technical.</p>
</li>
<li>
<p><strong>Databases break the &ldquo;reverting is easy&rdquo; analogy.</strong> Reverting a binary is trivial. Reverting a migration that already ran in production, already moved data, and maybe already dropped a column, is not. There&rsquo;s a pattern for this, <em>expand-contract</em>: add the new shape without removing the old one, wait for every consumer to migrate, only then remove the old one. This isn&rsquo;t about trusting AI more or less. It&rsquo;s about state that doesn&rsquo;t undo with <code>git revert</code>.</p>
</li>
<li>
<p><strong>Libraries and public packages don&rsquo;t run on the same math as a closed application.</strong> When my own code breaks, I fix it on the spot. When a lib that hundreds of projects depend on breaks, the people who eat the cost are people who don&rsquo;t even know I exist, with a much slower reaction time than mine. That&rsquo;s why my <code>pr-audit</code> skill, which I describe further below, treats semver classification as a version gate: a breaking change ships in a major, never in a patch, because there the real review isn&rsquo;t about catching a bug, it&rsquo;s about not blindsiding whoever depends on you without warning.</p>
</li>
<li>
<p><strong>App stores knock the speed down too</strong>, and my own frank_yomik example hides that instead of solving it. It publishes the APK straight as a GitHub Release, without going through the Play Store. If you distribute through Google&rsquo;s or Apple&rsquo;s official store, their review sits in the middle of your process, and you don&rsquo;t control how long it takes. This article is about removing the bottleneck that&rsquo;s inside your own control. The app store isn&rsquo;t inside it.</p>
</li>
<li>
<p><strong>The most honest point of all: every piece of evidence I bring here comes from projects where I&rsquo;m the sole owner and I decide alone.</strong> I have no way to prove, with my own numbers, that the same pace works inside a large company, with Legal, Compliance, and a product manager in the middle. What I&rsquo;d argue generalizes is the principle, not the exact pace: build real revert capability, automate the mechanical checking, and measure the result against real usage, not against your own sense of security. How much speed you apply on top of that depends on how much a mistake costs in your specific context, and nobody from outside answers that for you.</p>
</li>
</ol>
<p>On the first point specifically, it&rsquo;s worth opening up more, because &ldquo;stricter bar&rdquo; can&rsquo;t stay vague. It means real scenario testing, not just unit tests. ai-memory itself has more than a thousand integration tests spread across its modules, and most of them don&rsquo;t ask &ldquo;does this function return the right value?&rdquo; They ask things like:</p>
<ul>
<li>what happens when two writes race for the same file at the same time;</li>
<li>what happens when the disk fails mid-way through a delete operation;</li>
<li>what happens when two sessions try to consolidate the same data at the same time;</li>
<li>what happens when the process dies before it finishes writing.</li>
</ul>
<p>An environment with compliance, financial, or health requirements needs this level of testing: every identified failure mode has to have a mitigation, and more than one layer of checking, never a single lock.</p>
<p>And here&rsquo;s the point that holds up this article&rsquo;s entire argument even in those environments: this bar doesn&rsquo;t change depending on who wrote the code. A human developer&rsquo;s mistake takes down a payment system just as well as an LLM&rsquo;s mistake does, and nobody gets a free pass for loose review just because a senior human wrote the line. The bar of scenario testing and double-layer protection is about the problem domain, not about who, or what, produced the code.</p>
<p>Feature flags and staging environments are the other half of that double layer, and they also changed cost. Flags used to be a pain to maintain: dozens of them, each one needing to be born, monitored, and removed once it stops making sense, and it&rsquo;s easy to pile up dead flags nobody remembers the purpose of. That got cheap too: I ask the agent to audit every flag in the project, find which ones have been stuck at the same value for months, and generate the cleanup PR. Maintenance that used to take a whole afternoon turns into a task that takes minutes.</p>
<p>With that solved, the layered defense becomes simple to assemble: staging to catch obvious errors before any user sees them, feature flags to limit the blast radius of who sees the change first, and scenario tests to cover what staging alone doesn&rsquo;t simulate. Three layers, none of them expensive to maintain.</p>
<h2>How I&rsquo;ve Been Doing This Since February<span class="hx:absolute hx:-mt-20" id="how-ive-been-doing-this-since-february"></span>
    <a href="#how-ive-been-doing-this-since-february" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>I started accelerating my output with AI in February this year, and the question I wanted to answer was simple: can you use the new generation of frontier LLMs to code full-time, at real production pace, not just for a demo? The answer turned into my <a href="/en/2026/09/15/new-llm-benchmark-v4-retesting-all-top-llms-part-1/">biggest LLM benchmark</a>, more than 110 articles published since then, and this, as I <a href="https://x.com/AkitaOnRails/status/2102176881139138806"target="_blank" rel="noopener">wrote recently</a>:</p>
<blockquote>
  <p>Since February I&rsquo;ve produced more than 110 articles on my blog. Most of it came from the 30+ open source projects I built and maintain (43 if you count other people&rsquo;s contributing projects, like Flea or Omarchy). 1.6 million lines of code produced. By myself (with agents, of course).</p>

</blockquote>
<p>The projects I use the most in my own day to day, and that survived the &ldquo;am I still maintaining this months later&rdquo; test, are three:</p>
<ul>
<li><a href="https://aijail.io"target="_blank" rel="noopener">ai-jail</a>: an OS sandbox for my coding agents;</li>
<li><a href="https://aimemorybr.netlify.app"target="_blank" rel="noopener">ai-memory</a>: long-term memory for the agents;</li>
<li>ai-usagebar: a bar widget to monitor LLM usage.</li>
</ul>
<p>Two of them already have a decent landing page:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/09/22/aijail-hero.png" alt="ai-jail’s landing page, an operating system sandbox for AI agents"  loading="lazy" /></p>
<p>ai-jail went live today. ai-memory is up at <a href="https://aimemorybr.netlify.app"target="_blank" rel="noopener">aimemorybr.netlify.app</a> because the <code>aimemory.io</code> domain transfer will still take a few more days.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/09/22/aimemory-hero.png" alt="ai-memory’s landing page, a long-term memory system for coding agents"  loading="lazy" /></p>
<p>Both run on Netlify. That&rsquo;s not a tool recommendation, it&rsquo;s just where I already had an account set up. The point that matters here isn&rsquo;t the platform, it&rsquo;s the flow: to put a new version live, the command is <code>git push</code>. There&rsquo;s no manual step between the commit and the updated site.</p>
<p>My newsletter, <a href="https://themakitachronicles.com"target="_blank" rel="noopener">The M.Akita Chronicles</a>, already has more than 15,000 subscribers:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/09/22/themakitachronicles-hero.png" alt="The M.Akita Chronicles newsletter’s subscription page"  loading="lazy" /></p>
<p>And the newest effort is converting blog posts into podcast episodes. The whole thing runs inside Discord, with a bot that generates the preview and waits for my command:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/09/22/discord-podcast-1.png" alt="Discord bot announcing that a podcast episode preview is ready, with commands to publish, schedule, or reject it"  loading="lazy" /></p>
<p>One <code>/publish-podcast 2</code> later, the episode is published and the transcript already has a URL:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/09/22/discord-podcast-2.png" alt="Discord bot confirming that the podcast episode was published, with a link to the transcript"  loading="lazy" /></p>
<p>From finished text to published audio, the entire process is me typing a slash command in a Discord conversation. That&rsquo;s the effort bar I use to measure whether a process is good enough: if publishing takes more than one command, some step is still too manual.</p>
<h3>Does the Quality of the Generated Code Hold Up?<span class="hx:absolute hx:-mt-20" id="does-the-quality-of-the-generated-code-hold-up"></span>
    <a href="#does-the-quality-of-the-generated-code-hold-up" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Every single day someone shows up worried that automating too much with LLMs is going to dump bad code, or worse, dangerous code, into production without anyone noticing. Given that the engineering underneath is the one I described in the previous section, automated tests, CI, feature flags, staging, fast rollback, my actual experience doesn&rsquo;t back up that fear.</p>
<p>I&rsquo;ve produced more than 1.6 million lines of code with LLMs since February. <strong>I have never once seen an LLM do something randomly destructive that I didn&rsquo;t ask for.</strong> Quite the opposite: in my <a href="/en/2026/09/15/new-llm-benchmark-v4-retesting-all-top-llms-part-1/">LLM Benchmark v4</a>, I deliberately built an adversarial scenario, a subagent planting fourteen real CVE-based sabotage items, hidden inside a project that grows across seven sprints. Most of the tested models found and fixed the sabotage on their own, without me having to point it out.</p>
<p>That&rsquo;s anecdotal. It doesn&rsquo;t prove anything beyond what that specific benchmark measures. But after all that volume, the impression that sticks is consistent: today&rsquo;s LLMs produce stable code most of the time, comparable to what a decent human developer would produce, just a lot faster, and on average, more accurate.</p>
<h2>Cross-Platform CI/CD With Zero Effort<span class="hx:absolute hx:-mt-20" id="cross-platform-cicd-with-zero-effort"></span>
    <a href="#cross-platform-cicd-with-zero-effort" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>I went and counted how many of my <code>ai-*</code> and <code>frank-*</code> projects (18 repositories total, among the ones with a README) already have automated build and deploy. Fourteen have CI configured, and most of them build and publish to multiple operating system platforms from a push made on a Linux machine.</p>
<p>The most complete example is ai-memory itself: a single tag push triggers a workflow that, in the same run:</p>
<ul>
<li>builds Linux x86_64 and arm64;</li>
<li>builds macOS arm64 and x86_64;</li>
<li>builds Windows x86_64, packaged with PowerShell;</li>
<li>publishes a multi-architecture Docker image;</li>
<li>pushes the package to Arch Linux&rsquo;s AUR.</li>
</ul>
<p>ai-usagebar follows the same build pattern for Linux and Windows, and also publishes two AUR packages, to crates.io, and to a Scoop bucket for Windows.</p>
<p>Then there&rsquo;s frank_yomik, my manga reader with OCR and translation via a local LLM, which is the example I wanted to show that Android fits into the same pipeline. A <code>v*</code> tag push triggers five parallel builds:</p>
<ul>
<li>Linux;</li>
<li>macOS;</li>
<li>Windows;</li>
<li>browser extension;</li>
<li>Android, on an ordinary Linux runner, decoding a signing keystore stored in GitHub secrets and producing the already-signed APK.</li>
</ul>
<p>At the end, one job waits for all five to finish and publishes a single GitHub Release with every binary together. It builds for four different systems, including Android, without ever leaving Linux.</p>
<p>Not everything came out perfect on first look, and I&rsquo;d rather admit that than pretend it&rsquo;s all clean: frank_sherlock and frank_karaoke publish the release as a draft by default, which blocks the automation that follows (like the AUR push) until someone clicks &ldquo;publish&rdquo; by hand. I found this hole while writing this piece.</p>
<p>A neighboring project, frank_scanlation, had already solved a similar but different problem: GitHub suppresses the event that triggers automation when the release itself was created by the bot&rsquo;s own token, so its workflow triggers the next step by hand, via <code>workflow_dispatch</code>, from inside the job itself. Neither fix is hard. I&rsquo;ll apply both to the projects that still need it this week, because that&rsquo;s exactly the kind of accumulated friction this whole article is telling you not to leave sitting.</p>
<p>And speaking of hidden friction: ai-memory has Windows-specific code (NTFS file index, a native PowerShell hook) that only really runs on GitHub&rsquo;s <code>windows-latest</code> runner, about 1000 seconds against 250 on Linux for the same suite. I went as far as setting up a local Windows VM via <a href="https://github.com/dockur/windows"target="_blank" rel="noopener">dockur/windows</a>, thinking I&rsquo;d gain speed.</p>
<p>I didn&rsquo;t: the workload is dominated by disk I/O, and the result came out slower than GitHub&rsquo;s runner for the full suite. It only turned out useful for fast iteration on a single isolated crate. A good tool is one you measure, not one you assume, even when the measurement contradicts your initial bet.</p>
<h2>The Automated Review That Holds All of This Up<span class="hx:absolute hx:-mt-20" id="the-automated-review-that-holds-all-of-this-up"></span>
    <a href="#the-automated-review-that-holds-all-of-this-up" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>I already <a href="/en/2026/09/17/talking-about-my-ai-skills/">detailed my review skills</a> in a recent article, so I&rsquo;ll summarize here:</p>
<ul>
<li><code>pr-audit</code> audits every PR before merge with the principle of <strong>evidence over narrative</strong>: nothing the contributor wrote is true until proven, with a hostile checklist for malicious code, exposed credentials, semver breaks, and prompt injection attempts;</li>
<li><code>iss-audit</code> applies the same distrust to issues, never executing a command pasted by whoever reported it;</li>
<li><code>github-resolution</code> turns what got approved into merged code, one ticket at a time, with a regression test written before the fix, and the rule that <em>&ldquo;slop is a defect&rdquo;</em>: no speculative abstraction, no forgotten TODO, no weakening a test just to pass faster.</li>
</ul>
<p>That catches the most expensive category of garbage inside the same diff. It doesn&rsquo;t catch duplication spread across different PRs, because the audit looks at one isolated diff, not the whole project&rsquo;s architecture. That part still depends on me noticing the pattern repeating and telling it to consolidate.</p>
<p>By my estimate, this handles about 99% of review and resolution work on its own, with me supervising and deciding on the cases that are left. But that&rsquo;s not the main point. Automated review is necessary, but it alone proves nothing about whether the software is good.</p>
<h2>What Actually Keeps This Grounded: Real People Using It<span class="hx:absolute hx:-mt-20" id="what-actually-keeps-this-grounded-real-people-using-it"></span>
    <a href="#what-actually-keeps-this-grounded-real-people-using-it" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Here&rsquo;s the part I think is the most important in the whole article, and the one that usually gets skipped over: your own review isn&rsquo;t worth as much as you think. What&rsquo;s worth something is published software being used by real people, giving real feedback.</p>
<p>I went and checked ai-memory&rsquo;s real numbers on GitHub, without rounding up to sound impressive:</p>
<ul>
<li><strong>issues: 287 total, 249 came from people who aren&rsquo;t me</strong>, 86.8% of the total;</li>
<li><strong>pull requests: 562 total, 371 from outside</strong>, 10 of them Dependabot&rsquo;s automatic bumps and the other 361 from real people, roughly 121 distinct contributors.</li>
</ul>
<p>The two people who contributed the most alongside me sent 64 and 43 PRs each, on their own.</p>
<p>That&rsquo;s what keeps the project grounded. It isn&rsquo;t the LLM being a good reviewer, nor me being a good reviewer. It&rsquo;s hundreds of real people actually using it, finding cases I never imagined, and sending real issues and PRs. Without that, my automated review skills would just be confirming my own assumptions back to me, no matter how rigorous the rules look on paper.</p>
<p>It doesn&rsquo;t matter how good the AI you use is: if nobody uses what you published, you have no real feedback, and without real feedback there&rsquo;s no real improvement. There&rsquo;s only your own opinion about your own work, validated by nobody but yourself.</p>
<h2>The Conclusion That Matters<span class="hx:absolute hx:-mt-20" id="the-conclusion-that-matters"></span>
    <a href="#the-conclusion-that-matters" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Real software engineering <strong>needs</strong> fast, near-zero-effort deploy. It isn&rsquo;t optional, it isn&rsquo;t a luxury for a big team, it&rsquo;s a prerequisite. And if reviewing and shipping to production is taking longer than it took to develop the feature, you&rsquo;re not just wrong.</p>
<p><strong>You are the bottleneck.</strong></p>
<p>Excuses and justifications aren&rsquo;t worth anything at that point. Use that time to fix yourself and your process, because only real-world results matter, not the sense of security from having reviewed a diff line by line that nobody else is ever going to look at again.</p>
<p>If the premise changed, I change today. Not tomorrow, not next week. And if yours hasn&rsquo;t changed yet, the problem isn&rsquo;t AI. It&rsquo;s you holding back your own work.</p>
]]></content:encoded><category>devops</category><category>software-engineering</category><category>coding-agents</category><category>vibe-coding</category></item><item><title>Open Source Best Practices with LLMs - The Bare Minimum</title><link>https://akitaonrails.github.io/en/2026/05/30/open-source-best-practices-llm-the-minimum/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2026/05/30/open-source-best-practices-llm-the-minimum/</guid><pubDate>Sat, 30 May 2026 13:00:00 -0300</pubDate><description>&lt;p&gt;Over the last few months I&amp;rsquo;ve pushed a pile of personal projects to GitHub. &lt;a href="https://github.com/akitaonrails/frank_fbi"target="_blank" rel="noopener"&gt;Frank FBI&lt;/a&gt;, &lt;a href="https://github.com/akitaonrails/FrankMega"target="_blank" rel="noopener"&gt;Frank Mega&lt;/a&gt;, &lt;a href="https://github.com/akitaonrails/FrankYomik"target="_blank" rel="noopener"&gt;Frank Yomik&lt;/a&gt;, &lt;a href="https://github.com/akitaonrails/FrankSherlock"target="_blank" rel="noopener"&gt;Frank Sherlock&lt;/a&gt;, &lt;a href="https://github.com/akitaonrails/frank_investigator"target="_blank" rel="noopener"&gt;Frank Investigator&lt;/a&gt;, &lt;a href="https://github.com/akitaonrails/ai-jail"target="_blank" rel="noopener"&gt;ai-jail&lt;/a&gt;, &lt;a href="https://github.com/akitaonrails/ai-memory"target="_blank" rel="noopener"&gt;ai-memory&lt;/a&gt;, &lt;a href="https://github.com/akitaonrails/ai-usagebar"target="_blank" rel="noopener"&gt;ai-usagebar&lt;/a&gt;, &lt;a href="https://github.com/akitaonrails/ghpending"target="_blank" rel="noopener"&gt;ghpending&lt;/a&gt;, and so on. Some in Ruby on Rails, most in Rust, one or two in Flutter. Coded mostly with Claude Code and Codex, riding the wave of my &lt;a href="https://akitaonrails.github.io/en/2026/05/14/wrapping-up-my-ai-marathon-success-or-failure/"&gt;vibe coding marathon&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;And here&amp;rsquo;s the part nobody mentions when they say &amp;ldquo;I built a project with AI&amp;rdquo;: generating the code is the easy part. The hard part, the thing that separates a repo that&amp;rsquo;s just a pile of code from an actual open source project, is everything that comes &lt;strong&gt;after&lt;/strong&gt; &lt;code&gt;git init&lt;/code&gt;.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Over the last few months I&rsquo;ve pushed a pile of personal projects to GitHub. <a href="https://github.com/akitaonrails/frank_fbi"target="_blank" rel="noopener">Frank FBI</a>, <a href="https://github.com/akitaonrails/FrankMega"target="_blank" rel="noopener">Frank Mega</a>, <a href="https://github.com/akitaonrails/FrankYomik"target="_blank" rel="noopener">Frank Yomik</a>, <a href="https://github.com/akitaonrails/FrankSherlock"target="_blank" rel="noopener">Frank Sherlock</a>, <a href="https://github.com/akitaonrails/frank_investigator"target="_blank" rel="noopener">Frank Investigator</a>, <a href="https://github.com/akitaonrails/ai-jail"target="_blank" rel="noopener">ai-jail</a>, <a href="https://github.com/akitaonrails/ai-memory"target="_blank" rel="noopener">ai-memory</a>, <a href="https://github.com/akitaonrails/ai-usagebar"target="_blank" rel="noopener">ai-usagebar</a>, <a href="https://github.com/akitaonrails/ghpending"target="_blank" rel="noopener">ghpending</a>, and so on. Some in Ruby on Rails, most in Rust, one or two in Flutter. Coded mostly with Claude Code and Codex, riding the wave of my <a href="/en/2026/05/14/wrapping-up-my-ai-marathon-success-or-failure/">vibe coding marathon</a>.</p>
<p>And here&rsquo;s the part nobody mentions when they say &ldquo;I built a project with AI&rdquo;: generating the code is the easy part. The hard part, the thing that separates a repo that&rsquo;s just a pile of code from an actual open source project, is everything that comes <strong>after</strong> <code>git init</code>.</p>
<p>I have a strong opinion about this, so let me just plant the flag: no open source project is ready to be published without three things. In order of importance:</p>
<ol>
<li><strong>Installation surface.</strong> The new user has to be able to install and try the tool with the least friction possible. One command, ideally.</li>
<li><strong>Tests and automated CI.</strong> So contributions are easier and there&rsquo;s common ground on what&rsquo;s minimally acceptable to land in the project.</li>
<li><strong>Documentation.</strong> The short, easy-to-read version (the README) and a more detailed set in <code>docs/</code>, so new contributors understand the architectural decisions.</li>
</ol>
<p>Without those three, the way I see it, the project isn&rsquo;t ready. It can have the most brilliant code on earth inside, doesn&rsquo;t matter. If a stranger can&rsquo;t install it, can&rsquo;t trust that whatever they touch won&rsquo;t break everything, and can&rsquo;t understand why things are the way they are, then you don&rsquo;t have an open source project. You have a public dead archive.</p>
<p>And there&rsquo;s a bonus that became an essential part of my workflow: <strong>when all my projects follow the same pattern, I can just tell Claude Code or Codex &ldquo;run the deploy&rdquo; or &ldquo;cut a release,&rdquo; and they can do it.</strong> Because the structure is predictable. <code>bin/deploy</code> does the same thing in every project. The release comes out of a tag. The agent doesn&rsquo;t have to guess anything. Standardization is what makes the automation trustworthy. It looks like fussiness from an organized person, but it&rsquo;s the prerequisite for me to trust, eyes closed, whatever the agent is about to run.</p>
<p>Let me break down the three pillars and show the minimum setup I actually use.</p>
<h2>Pillar 1: The installation surface<span class="hx:absolute hx:-mt-20" id="pillar-1-the-installation-surface"></span>
    <a href="#pillar-1-the-installation-surface" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>This is the most important one and the most neglected. People spend weeks tuning the internal algorithm and then ship a README that says &ldquo;clone the repo and run <code>cargo build --release</code>.&rdquo; So the guy who was curious enough to try it needs to have Rust installed, wait ten minutes for it to compile, and pray no system lib is missing. You just lost 90% of your potential users.</p>
<p>The ideal is to offer <strong>several paths</strong> and let the user pick the one that fits their environment. Look at the install header for ai-jail, my champion of options:</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="c1"># Homebrew (macOS or Linux)</span>
</span></span><span class="line"><span class="cl">brew tap akitaonrails/tap <span class="o">&amp;&amp;</span> brew install ai-jail
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># AUR (Arch Linux) - prebuilt binary</span>
</span></span><span class="line"><span class="cl">yay -S ai-jail-bin
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># cargo (any platform with Rust)</span>
</span></span><span class="line"><span class="cl">cargo install ai-jail
</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 use -g github:akitaonrails/ai-jail
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Direct binary download</span>
</span></span><span class="line"><span class="cl">curl -fsSL https://github.com/akitaonrails/ai-jail/releases/latest/download/ai-jail-linux-x86_64.tar.gz <span class="p">|</span> tar xz</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>Five paths. The Arch user runs <code>yay</code>. The macOS one runs <code>brew</code>. The Rust dev runs <code>cargo install</code>. Whoever uses <a href="https://github.com/jdx/mise"target="_blank" rel="noopener">mise</a> to manage versions pulls it through mise. And whoever just wants the binary downloads the tarball straight from the release. Everyone grabs what they&rsquo;re already used to, nobody&rsquo;s forced to install a whole toolchain just to kick the tires.</p>
<h3>The secret: compile once, repackage many<span class="hx:absolute hx:-mt-20" id="the-secret-compile-once-repackage-many"></span>
    <a href="#the-secret-compile-once-repackage-many" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The gotcha that scares you at first is thinking each package format needs its own build. It doesn&rsquo;t. The trick is to <strong>compile the binary once per architecture, then repackage that same binary into every format.</strong></p>
<p>In my Rust pipeline, the build job produces a tarball with the finished binary and its <code>sha256</code>. The packaging steps that follow (Docker, AUR, Homebrew) <strong>download that already-compiled tarball</strong> instead of recompiling. It cuts CI time, guarantees the byte that goes to AUR is exactly the one that goes to Docker, and kills off that whole class of &ldquo;works in one format, breaks in the other&rdquo; bug.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="c"># Docker job reusing the already-compiled binary</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">docker-amd64</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">needs</span><span class="p">:</span><span class="w"> </span><span class="l">build-linux</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">steps</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">actions/download-artifact@v4</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">with</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">ai-memory-linux-x86_64</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">artifacts</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">Desempacota o binário pré-compilado</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="l">tar -xzf artifacts/ai-memory-linux-x86_64.tar.gz -C dist/docker/</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">docker/build-push-action@v6</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">target</span><span class="p">:</span><span class="w"> </span><span class="l">runtime-prebuilt-amd64 </span><span class="w"> </span><span class="c"># inject the binary, don&#39;t recompile</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>The AUR job does the same thing: download the tarball, compute the <code>sha256</code>, inject it into the <code>PKGBUILD</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-sh" data-lang="sh"><span class="line"><span class="cl"><span class="nv">X86_SHA</span><span class="o">=</span><span class="k">$(</span>awk <span class="s1">&#39;{print $1}&#39;</span> artifacts/ai-usagebar-linux-x86_64.tar.gz.sha256<span class="k">)</span>
</span></span><span class="line"><span class="cl">sed -i <span class="s2">&#34;s/^sha256sums_x86_64=.*/sha256sums_x86_64=(&#39;</span><span class="nv">$X86_SHA</span><span class="s2">&#39;)/&#34;</span> packaging/aur/PKGBUILD-bin</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>That&rsquo;s the core idea. The binary is the real artifact. RPM, DEB, AppImage, AUR, Homebrew tarball, Docker image, all of it is just <strong>wrapping around the same binary</strong>.</p>
<p>The two snippets above are excerpts. If you want to see the whole thing running, the full build-once-reuse flow (binary compiled once, reused across the Docker and AUR jobs) lives in <a href="https://github.com/akitaonrails/ai-memory/blob/main/.github/workflows/release.yml"target="_blank" rel="noopener">ai-memory&rsquo;s <code>release.yml</code></a>, and the <code>sha256</code> injection into the <code>PKGBUILD</code> is in <a href="https://github.com/akitaonrails/ai-usagebar/blob/main/.github/workflows/release.yml"target="_blank" rel="noopener">ai-usagebar&rsquo;s <code>release.yml</code></a>.</p>
<h3>The menu of formats<span class="hx:absolute hx:-mt-20" id="the-menu-of-formats"></span>
    <a href="#the-menu-of-formats" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Every platform is its own world. Here&rsquo;s the minimum worth knowing:</p>
<p><strong>Linux</strong> is the most fragmented. The options, from simplest to most work:</p>
<ul>
<li><strong>Tarball</strong> (<code>.tar.gz</code>): the lowest common denominator. Works on any distro, just unpack it and drop it in <code>PATH</code>. Always offer this one.</li>
<li><strong>AUR</strong> (Arch): two packages, the <code>-bin</code> (prebuilt binary, installs in seconds) and the source one (compiles on the user&rsquo;s machine). I publish both with the <code>KSXGitHub/github-actions-deploy-aur</code> action. The <code>-bin</code> is what everybody uses.</li>
<li><strong>AppImage</strong>: a single executable file that carries all its dependencies inside. Good for desktop apps. Frank Sherlock, which is Tauri, builds the AppImage right in the build and I still GPG-sign it afterward (the signing step is in <a href="https://github.com/akitaonrails/FrankSherlock/blob/master/.github/workflows/release.yml"target="_blank" rel="noopener">its <code>release.yml</code></a>). For a simple binary you can use <code>appimagetool</code>.</li>
<li><strong>RPM and DEB</strong>: for Fedora/openSUSE and Debian/Ubuntu. The tool that saves your life here is <a href="https://github.com/goreleaser/nfpm"target="_blank" rel="noopener">nfpm</a>, which generates both from a single declarative <code>nfpm.yaml</code>, no need to learn the guts of <code>rpmbuild</code> or <code>dpkg-deb</code>. It takes the already-compiled binary and spits out both packages.</li>
</ul>
<p><strong>macOS</strong> has two targets: Apple Silicon (<code>aarch64-apple-darwin</code>) and Intel (<code>x86_64-apple-darwin</code>). You either compile them separately or build a universal binary with <code>lipo</code>. And here lives the platform&rsquo;s biggest headache: <strong>signing and notarization</strong>. Without it, macOS Gatekeeper refuses to open your app. The flow is to decode the P12 certificate, import it into a temporary keychain, sign with <code>codesign --options runtime --timestamp</code>, and notarize:</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">codesign --sign <span class="s2">&#34;</span><span class="nv">$APPLE_SIGNING_IDENTITY</span><span class="s2">&#34;</span> --options runtime --timestamp ./meu-binario
</span></span><span class="line"><span class="cl">ditto -c -k meu-binario notarize.zip
</span></span><span class="line"><span class="cl">xcrun notarytool submit notarize.zip <span class="se">\
</span></span></span><span class="line"><span class="cl">  --apple-id <span class="s2">&#34;</span><span class="nv">$APPLE_ID</span><span class="s2">&#34;</span> --password <span class="s2">&#34;</span><span class="nv">$APPLE_PASSWORD</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  --team-id <span class="s2">&#34;</span><span class="nv">$APPLE_TEAM_ID</span><span class="s2">&#34;</span> --wait</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>This requires a paid Apple Developer account. It&rsquo;s annoying, but it&rsquo;s the price of not throwing an &ldquo;app is damaged&rdquo; error in your user&rsquo;s face. The whole signing-and-notarization job, with the temporary-keychain dance, is in <a href="https://github.com/akitaonrails/ai-jail/blob/master/.github/workflows/release.yml"target="_blank" rel="noopener">ai-jail&rsquo;s <code>release.yml</code></a>.</p>
<p><strong>Windows</strong> is the easiest to build (<code>x86_64-pc-windows-msvc</code>) and the thorniest to sign. A Windows code signing certificate is expensive and bureaucratic, so I, honestly, don&rsquo;t sign. I accept that SmartScreen will throw a warning on first run and I make that clear in the README. For a personal open source project, that&rsquo;s good enough.</p>
<p><strong>Rust crates</strong> (<code>cargo publish</code>): if it&rsquo;s a Rust library or CLI, publishing to <a href="https://crates.io"target="_blank" rel="noopener">crates.io</a> gives you <code>cargo install</code> for free. One line in the pipeline. Tip: make it idempotent, because re-running on an already-published tag errors out. I <code>grep</code> the output for &ldquo;already exists&rdquo; so it doesn&rsquo;t blow up the whole release over that.</p>
<p><strong>Homebrew</strong>: the clean way is to keep your own tap (<code>akitaonrails/homebrew-tap</code>). The pipeline updates the formula with the new URL and the new <code>sha256</code> on every release. The user runs <code>brew tap</code> once and after that it&rsquo;s just <code>brew upgrade</code>.</p>
<p><strong>mise</strong>: <a href="https://github.com/jdx/mise"target="_blank" rel="noopener">mise</a> can install straight from GitHub releases without you configuring anything on your end, as long as your tarballs follow a platform-name convention. You get this path basically for free just by naming your releases well.</p>
<p>Notice a detail: AUR, Homebrew, mise, and the direct download <strong>all consume the same release tarball</strong>. You publish the binaries once, and four different package managers know how to fend for themselves. That&rsquo;s why &ldquo;compile once, repackage many&rdquo; is the golden rule.</p>
<h2>Pillar 2: GitHub Actions, tag-based releases, and the changelog<span class="hx:absolute hx:-mt-20" id="pillar-2-github-actions-tag-based-releases-and-the-changelog"></span>
    <a href="#pillar-2-github-actions-tag-based-releases-and-the-changelog" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The second thing that makes an open source project serious is CI. Forget the &ldquo;build passing&rdquo; badge on the README, the value is in the common ground it creates: when someone sends a pull request, CI runs the tests, the linter, and the security scanner, and you know right away whether that contribution breaks anything. Without it, every PR turns into a tiresome manual negotiation.</p>
<p>On my Rust projects CI runs <code>cargo fmt --check</code>, <code>cargo clippy -D warnings</code>, <code>cargo test</code>, and <code>cargo audit</code>. On the Rails projects it&rsquo;s <code>rubocop</code>, <code>brakeman</code> (security scanner), <code>bundler-audit</code>, and the Minitest suite. <a href="https://github.com/akitaonrails/FrankMD"target="_blank" rel="noopener">FrankMD</a> alone has over 1,800 tests between Ruby and JavaScript. Frank Sherlock clears 300 tests in Rust plus 300 on the frontend. That safety net is what lets me accept a stranger&rsquo;s PR without sweating.</p>
<h3>PR review with an LLM<span class="hx:absolute hx:-mt-20" id="pr-review-with-an-llm"></span>
    <a href="#pr-review-with-an-llm" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The tests and CI are the bottom net. Above it there&rsquo;s a layer that became central to my workflow: using the LLM itself to review the pull requests. Opus and GPT are absurdly good at code review and quality refactoring. Better than me on consistency, because they don&rsquo;t get tired and they don&rsquo;t skim over 200 lines just because it&rsquo;s the fifth review of the day.</p>
<p>I have two prompts I use all the time. The first, for a specific PR that just landed:</p>
<blockquote>
  <p>look at the open PR on github. don&rsquo;t trust the author&rsquo;s description. audit the code thoroughly to see if it actually makes sense. make sure there are no regressions and no drop in code quality. make sure test coverage is adequate. then tell me what you think we should do.</p>

</blockquote>
<p>Notice the &ldquo;don&rsquo;t trust the author&rsquo;s description.&rdquo; That&rsquo;s deliberate. A PR description tells you what the person thought they did, and it doesn&rsquo;t always match what they actually did. The LLM has to read the real code, not the summary.</p>
<p>The second prompt is for after a long coding session or a bunch of PRs merged at once:</p>
<blockquote>
  <p>we touched a lot of code. audit the code those PRs changed, look for dead code, unnecessary duplication, magic hardcoded values that should be constants and/or better documented, missing test coverage, aim for <a href="/en/2026/04/20/clean-code-for-ai-agents/">clean code</a> principles, then check whether the documentation was properly updated.</p>

</blockquote>
<p>With the verdict in hand, I decide. For an incomplete PR, what I do depends on the size and my priority at that moment. If it&rsquo;s something small, I fix it myself right in the PR with an <code>amend</code>, reply explaining what I changed, and then merge and close. When I can merge but can&rsquo;t fix beforehand, I merge and immediately drop a follow-up fix commit. And when it&rsquo;s a big change, I ask the LLM to reply to the PR explaining why we&rsquo;re not merging, with the right direction on what needs to be done.</p>
<p>That&rsquo;s how I closed almost 40 PRs and 20 issues on ai-memory alone, in a couple of days. That volume doesn&rsquo;t happen by hand, and it also can&rsquo;t happen on autopilot.</p>
<p>And this is the point I keep hammering: none of this is blind. I&rsquo;m piloting the LLM toward those goals the whole time. It does the audit, puts the suggestions on the table, and even writes the text of the reply, but the one who decides to merge, fix, or reject is me. The LLM is the tireless reviewer; I&rsquo;m the one who signs off.</p>
<h3>The tag-based trigger<span class="hx:absolute hx:-mt-20" id="the-tag-based-trigger"></span>
    <a href="#the-tag-based-trigger" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The release part is separate from CI. While CI runs on every push and PR, the release fires <strong>only when you push a version tag</strong>:</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">on</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">push</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">tags</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;v*.*.*&#39;</span><span class="w">   </span><span class="c"># dispara em v1.2.3, v0.5.0, etc.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">workflow_dispatch</span><span class="p">:</span><span class="w"> </span><span class="c"># permite disparar manualmente também</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>The flow for cutting a version becomes this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">git tag v0.5.0
</span></span><span class="line"><span class="cl">git push origin v0.5.0</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>And that&rsquo;s it. GitHub Actions notices the tag, fires the release workflow, compiles for the targets, packages everything, signs whatever needs signing, creates the GitHub Release, and publishes to the package managers. I don&rsquo;t touch anything else. In fact that&rsquo;s literally what I tell Claude Code: &ldquo;cut v0.5.0.&rdquo; It runs <code>cargo set-version</code>, updates the CHANGELOG, commits, creates the tag, pushes, and the rest is the pipeline.</p>
<p>The Rust build matrix compiles each target on its native platform:</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">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">matrix</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">include</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">os</span><span class="p">:</span><span class="w"> </span><span class="l">ubuntu-22.04</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">target</span><span class="p">:</span><span class="w"> </span><span class="l">x86_64-unknown-linux-gnu</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">os</span><span class="p">:</span><span class="w"> </span><span class="l">ubuntu-22.04-arm</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">target</span><span class="p">:</span><span class="w"> </span><span class="l">aarch64-unknown-linux-gnu</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">os</span><span class="p">:</span><span class="w"> </span><span class="l">macos-latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">target</span><span class="p">:</span><span class="w"> </span><span class="l">aarch64-apple-darwin</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">os</span><span class="p">:</span><span class="w"> </span><span class="l">windows-latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">target</span><span class="p">:</span><span class="w"> </span><span class="l">x86_64-pc-windows-msvc</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 cross-compilation gotcha: if you compile ARM inside an x86 machine (via <a href="https://github.com/cross-rs/cross"target="_blank" rel="noopener">cross</a>), you <strong>can&rsquo;t run the tests</strong> for that binary right there, because the architecture doesn&rsquo;t match. The fix is to run tests only on the native target and cross-compile the rest without testing. ai-usagebar does exactly that, x86 native and ARM via <code>cross</code>, in the matrix of <a href="https://github.com/akitaonrails/ai-usagebar/blob/main/.github/workflows/release.yml"target="_blank" rel="noopener">its <code>release.yml</code></a>. And always use a compile cache: <code>Swatinem/rust-cache</code> easily cuts build time in half.</p>
<h3>Release notes and the changelog matter<span class="hx:absolute hx:-mt-20" id="release-notes-and-the-changelog-matter"></span>
    <a href="#release-notes-and-the-changelog-matter" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>This is where a lot of people slack off, and they shouldn&rsquo;t. When you cut a release, the person thinking about upgrading wants to know <strong>what changed</strong>. &ldquo;v0.5.0&rdquo; says nothing. A changelog says something.</p>
<p>I keep a <code>CHANGELOG.md</code> in the <a href="https://keepachangelog.com/"target="_blank" rel="noopener">Keep a Changelog</a> format, one section per version. And the pipeline automatically extracts the current version&rsquo;s section to paste into the GitHub Release body. A simple <code>awk</code> handles it:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">awk -v <span class="nv">ver</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$VERSION</span><span class="s2">&#34;</span> <span class="s1">&#39;
</span></span></span><span class="line"><span class="cl"><span class="s1">  BEGIN { hdr = &#34;## [&#34; ver &#34;]&#34; }
</span></span></span><span class="line"><span class="cl"><span class="s1">  index($0, hdr) == 1 { flag=1; next }
</span></span></span><span class="line"><span class="cl"><span class="s1">  flag &amp;&amp; index($0, &#34;## [&#34;) == 1 { exit }
</span></span></span><span class="line"><span class="cl"><span class="s1">  flag { print }
</span></span></span><span class="line"><span class="cl"><span class="s1">&#39;</span> CHANGELOG.md &gt; changelog_section.md</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>That <code>awk</code> is the same one that runs on the ai-usagebar release (again in <a href="https://github.com/akitaonrails/ai-usagebar/blob/main/.github/workflows/release.yml"target="_blank" rel="noopener">its <code>release.yml</code></a>, which is my most polished one on this front). Then, after the changelog, I append the install instructions and the <code>sha256</code> checksums for each artifact. The result is a GitHub Release that explains itself: what changed, how to install it, and how to verify the integrity of the download. The user reads it and decides whether to upgrade. That&rsquo;s the kind of care that makes a stranger trust your project.</p>
<h2>Pillar 3: Real deployment, no fuss<span class="hx:absolute hx:-mt-20" id="pillar-3-real-deployment-no-fuss"></span>
    <a href="#pillar-3-real-deployment-no-fuss" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The projects that run as a service (the Rails ones, mainly) have to go somewhere. And here I go against the hype. No Kubernetes. No Kamal. No three-stage deploy pipeline with manual approval.</p>
<p>I have a home server. A <a href="/en/2026/03/31/migrating-my-home-server-with-claude-code/">Minisforum MS-S1 Max running openSUSE MicroOS</a> that I migrated recently, with a <a href="https://github.com/go-gitea/gitea"target="_blank" rel="noopener">Gitea</a> acting as a private Docker registry on port 3007. For a solo developer with a simple home server, that&rsquo;s more than enough. All that cluster-orchestration ceremony is solving a problem I don&rsquo;t have.</p>
<p>The whole deploy fits in a single <code>bin/deploy</code>. It does three things: builds the image, pushes it to the registry, and SSHes into the server to bring up the new version:</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="cp">#!/usr/bin/env bash
</span></span></span><span class="line"><span class="cl"><span class="nb">set</span> -euo pipefail
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Config comes from a .gitignored file, outside the repo</span>
</span></span><span class="line"><span class="cl"><span class="nb">source</span> <span class="s2">&#34;</span><span class="k">$(</span>dirname <span class="s2">&#34;</span><span class="nv">$0</span><span class="s2">&#34;</span><span class="k">)</span><span class="s2">/../config/deploy.env&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">IMAGE</span><span class="o">=</span><span class="s2">&#34;</span><span class="si">${</span><span class="nv">REGISTRY_HOST</span><span class="si">}</span><span class="s2">:</span><span class="si">${</span><span class="nv">REGISTRY_PORT</span><span class="si">}</span><span class="s2">/akitaonrails/frank_fbi:latest&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;==&gt; Buildando a imagem...&#34;</span>
</span></span><span class="line"><span class="cl">docker build -t <span class="s2">&#34;</span><span class="nv">$IMAGE</span><span class="s2">&#34;</span> .
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;==&gt; Empurrando pro registry...&#34;</span>
</span></span><span class="line"><span class="cl">docker push <span class="s2">&#34;</span><span class="nv">$IMAGE</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;==&gt; Subindo no servidor...&#34;</span>
</span></span><span class="line"><span class="cl">ssh <span class="s2">&#34;</span><span class="si">${</span><span class="nv">DEPLOY_USER</span><span class="si">}</span><span class="s2">@</span><span class="si">${</span><span class="nv">DEPLOY_HOST</span><span class="si">}</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  <span class="s2">&#34;cd </span><span class="si">${</span><span class="nv">REMOTE_DIR</span><span class="si">}</span><span class="s2"> &amp;&amp; \
</span></span></span><span class="line"><span class="cl"><span class="s2">   docker compose pull &amp;&amp; \
</span></span></span><span class="line"><span class="cl"><span class="s2">   docker compose down --remove-orphans &amp;&amp; \
</span></span></span><span class="line"><span class="cl"><span class="s2">   docker compose up -d&#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>That&rsquo;s it. <code>build</code>, <code>push</code>, <code>ssh</code> with <code>pull</code>, <code>down</code>, <code>up</code>. Four commands on the remote server, chained in a single SSH. No agent running over there, no webhook, no control panel. The server has a <code>docker-compose.production.yml</code> pointing at the <code>:latest</code> image from the registry, and the <code>pull</code> brings the new version down.</p>
<p>Notice the sensitive data lives in a <code>config/deploy.env</code> that <strong>doesn&rsquo;t go into Git</strong>. The repo only carries the <code>config/deploy.env.example</code>, with the structure and fake values. This &ldquo;versioned example, real one ignored&rdquo; pattern repeats in every project of mine, and it&rsquo;s what lets the project be public without leaking the host, user, or path of my server.</p>
<p>The image itself comes from a multi-stage Dockerfile. The build stage installs the compilation tools, builds everything, and the final stage copies only the result into a lean image, running as a non-root user. The production compose swaps <code>build:</code> for <code>image:</code>, points the volumes at the server&rsquo;s persistent paths (<code>/var/opt/docker/...</code>), and uses <code>restart: always</code> to bring everything back up if the server reboots.</p>
<p>And again, the beauty of standardization: since <strong>every</strong> project of mine has this same <code>bin/deploy</code>, I can tell Claude Code &ldquo;deploy Frank FBI&rdquo; and it knows exactly what to run. I don&rsquo;t have to remember a single command. The tool is the executable documentation of the process.</p>
<h2>The lesson: nobody cares about your stack<span class="hx:absolute hx:-mt-20" id="the-lesson-nobody-cares-about-your-stack"></span>
    <a href="#the-lesson-nobody-cares-about-your-stack" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Now the part I most wanted to write, and the one worth more than all the YAML snippets above.</p>
<p>Most people, when they go to present a project, focus everything on the technology. &ldquo;Check out my brand-new vector database.&rdquo; &ldquo;Check out my last-gen reactive framework.&rdquo; &ldquo;I used such-and-such microservices architecture.&rdquo; And they fill the top of the README with that: the dependency list, the component diagram, the stack.</p>
<p>Nobody cares.</p>
<p>Seriously. The user who lands on your README doesn&rsquo;t want to know which database you used. They want to know <strong>what problem this solves for them</strong>. Implementation detail doesn&rsquo;t belong at the top of the README. It belongs in <code>docs/</code>, for whoever&rsquo;s going to contribute. In the README, what matters is the use case.</p>
<p>Look at the difference. &ldquo;MCP server in Rust with FTS5 and optional LLM consolidation&rdquo; says nothing to anybody. Now &ldquo;long-term memory shared across Claude Code, Codex, and Cursor, with handoff between sessions&rdquo; — that one the guy gets instantly, because it&rsquo;s the pain he has. Both describe ai-memory. One talks about technology, the other talks about the problem it solves.</p>
<p>The use case is the single most important thing about the whole project. Before writing a line of code, the right question is &ldquo;what problem does this solve, and for whom?&rdquo; If you can&rsquo;t answer that in one sentence, you&rsquo;re building a <strong>solution looking for a problem</strong>. And a solution looking for a problem is, in practice, a solution that solves nothing. Doesn&rsquo;t matter how elegant the code is on the inside.</p>
<p>The UX layer, the surface the user touches, is worth far more than the internal engineering. A project with mediocre technology that&rsquo;s trivial to install, that clearly states what problem it solves, and that has honest documentation, goes further than a brilliant project nobody can run. Every time.</p>
<p>So the minimum, the real minimum for an open source project, is this: easy install, CI that gives you confidence, documentation that starts from the problem. The pretty algorithm comes way down the priority list. The three pillars of this post exist to serve one thing, which is the experience of whoever shows up from outside. Get that right first. The rest is implementation detail.</p>
]]></content:encoded><category>open-source</category><category>software-engineering</category><category>devops</category></item><item><title>Migrating my Home Server with Claude Code | openSUSE MicroOS</title><link>https://akitaonrails.github.io/en/2026/03/31/migrating-my-home-server-with-claude-code/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2026/03/31/migrating-my-home-server-with-claude-code/</guid><pubDate>Tue, 31 Mar 2026 16:00:00 -0300</pubDate><description>&lt;p&gt;My old home server was a mess. An Intel NUC with Ubuntu Server that I&amp;rsquo;d been patching together for two years. Containers with hardcoded paths, volumes mounted in random places (&lt;code&gt;/home/akitaonrails/docker/&lt;/code&gt;, &lt;code&gt;/home/akitaonrails/sonarr/&lt;/code&gt;, &lt;code&gt;/mnt/terachad/&lt;/code&gt;), docker-compose files scattered with no consistent layout. It worked, but if I lost the disk it would take days to rebuild everything from memory.&lt;/p&gt;
&lt;p&gt;With the &lt;a href="https://akitaonrails.github.io/en/2026/03/31/minisforum-ms-s1-max-amd-ai-max-395-review/"&gt;new Minisforum MS-S1 Max&lt;/a&gt; that I bought, I decided to do the migration properly. And I decided to use Claude Code from the start to speed up the process. It&amp;rsquo;s a home server, only I use it, so the risk of making a mistake is low. But if this were a real production server, I would never do this without rigorous human review at every step.&lt;/p&gt;</description><content:encoded><![CDATA[<p>My old home server was a mess. An Intel NUC with Ubuntu Server that I&rsquo;d been patching together for two years. Containers with hardcoded paths, volumes mounted in random places (<code>/home/akitaonrails/docker/</code>, <code>/home/akitaonrails/sonarr/</code>, <code>/mnt/terachad/</code>), docker-compose files scattered with no consistent layout. It worked, but if I lost the disk it would take days to rebuild everything from memory.</p>
<p>With the <a href="/en/2026/03/31/minisforum-ms-s1-max-amd-ai-max-395-review/">new Minisforum MS-S1 Max</a> that I bought, I decided to do the migration properly. And I decided to use Claude Code from the start to speed up the process. It&rsquo;s a home server, only I use it, so the risk of making a mistake is low. But if this were a real production server, I would never do this without rigorous human review at every step.</p>
<p>What follows is the migration writeup and a guide for anyone who wants to replicate it. If I ever have to rebuild from scratch, this post is the documentation.</p>
<h2>Choosing the operating system<span class="hx:absolute hx:-mt-20" id="choosing-the-operating-system"></span>
    <a href="#choosing-the-operating-system" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><h3>Why not Ubuntu Server again<span class="hx:absolute hx:-mt-20" id="why-not-ubuntu-server-again"></span>
    <a href="#why-not-ubuntu-server-again" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>I used Ubuntu Server on the NUC for convenience. But <code>do-release-upgrade</code> is Russian roulette. Every time Ubuntu releases a new version, the upgrade is a real risk of breaking things. Packages change, configs get overwritten, dependencies clash. For a server that has to be running constantly, that&rsquo;s unacceptable.</p>
<h3>Why not Arch Linux<span class="hx:absolute hx:-mt-20" id="why-not-arch-linux"></span>
    <a href="#why-not-arch-linux" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>I use Arch on the desktop and I like it. But Arch is a rolling release with no stability guarantees at all. For a desktop where I can stop and fix problems, fine. For a headless server running 49 Docker containers that has to work after every reboot, no.</p>
<h3>Fedora CoreOS vs openSUSE MicroOS<span class="hx:absolute hx:-mt-20" id="fedora-coreos-vs-opensuse-microos"></span>
    <a href="#fedora-coreos-vs-opensuse-microos" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The two modern options for a container server are Fedora CoreOS and openSUSE MicroOS. Both are immutable systems: the root filesystem is read-only, updates are atomic (they either apply in full or don&rsquo;t apply at all), and rollback is instant.</p>
<p>The difference: Fedora CoreOS uses Ignition (declarative configuration before the first boot) and is designed to be provisioned automatically. MicroOS uses <code>transactional-update</code> and allows normal interactive use. For a home server where I want SSH and the ability to poke around manually, MicroOS fits better.</p>
<h3>What makes MicroOS different<span class="hx:absolute hx:-mt-20" id="what-makes-microos-different"></span>
    <a href="#what-makes-microos-different" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The immutable system concept changes how you operate the server:</p>
<p>Every package install or <code>/etc</code> edit goes through <code>transactional-update</code>, which creates a new btrfs snapshot, applies the change in that snapshot, and on the next reboot the system boots into the updated snapshot. If the change breaks something, you do <code>transactional-update rollback</code> and you&rsquo;re back on the previous snapshot in seconds.</p>
<p>Updates are automatic and daily. The <code>transactional-update.timer</code> downloads patches, creates a snapshot, and <code>rebootmgr</code> reboots in a configured window (in my case, between 4am and 5:30am). If the update breaks the boot, GRUB automatically falls back to the previous snapshot.</p>
<p>SELinux is enforcing by default. That caused 90% of the problems during the migration, but it&rsquo;s the right setting for security.</p>
<h2>Initial setup<span class="hx:absolute hx:-mt-20" id="initial-setup"></span>
    <a href="#initial-setup" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><h3>Hardware<span class="hx:absolute hx:-mt-20" id="hardware"></span>
    <a href="#hardware" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><ul>
<li>AMD Ryzen AI Max+ 395, 128GB LPDDR5X</li>
<li>96GB allocated as VRAM via BIOS (UMA Frame Buffer Size)</li>
<li>2TB NVMe (system + Docker)</li>
<li>Wired 2.5Gbps network</li>
<li>Synology DS1821+ NAS at 192.168.0.21 (NFS)</li>
</ul>
<h3>First steps<span class="hx:absolute hx:-mt-20" id="first-steps"></span>
    <a href="#first-steps" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>MicroOS install is standard. Then:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

/var/mnt/terachad/    (Synology NFS)
├── Bitcoin/data/     (blockchain, 785GB)
├── Downloads/        (torrents &#43; nzbget)
├── Videos/           (Radarr movies &#43; Sonarr series)
└── Ollama/models/    (model overflow if local disk fills up)</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h2>A warning about using AI to administer servers<span class="hx:absolute hx:-mt-20" id="a-warning-about-using-ai-to-administer-servers"></span>
    <a href="#a-warning-about-using-ai-to-administer-servers" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>I used Claude Code to speed up the migration. It created compose files, wrote backup scripts, configured the firewall, diagnosed SELinux problems. It worked well for my case: home server, only I use it, and I was reviewing every step.</p>
<p>But there are traps. Claude doesn&rsquo;t know that <code>:Z</code> breaks SQLite unless you tell it. It doesn&rsquo;t know that Fulcrum doesn&rsquo;t accept env vars unless it&rsquo;s already seen the Dockerfile. It will invent &ldquo;better&rdquo; volume layouts that break Plex because Plex stores absolute paths in its database.</p>
<p>If it were real production: don&rsquo;t do this without review. Every compose file Claude generates, read it in full before applying. Every destructive command (rollback, delete, recreate), confirm manually. And have tested backups before you start. Claude is great for generating the first version and diagnosing errors, but the architecture decisions and the safety validations are yours.</p>
<p>The previous home server posts that may give additional context:</p>
<ul>
<li><a href="/en/2024/04/03/my-personal-netflix-with-docker-compose/">My &ldquo;Personal Netflix&rdquo; with Docker Compose</a></li>
<li><a href="/en/2025/09/09/accessing-my-home-server-with-a-real-domain/">Accessing my Home Server with a real domain</a></li>
<li><a href="/en/2025/09/10/protecting-your-home-server-with-cloudflare-zero-trust/">Protecting your Home Server with Cloudflare Zero Trust</a></li>
<li><a href="/en/2025/09/10/installing-grafana-on-my-home-server/">Installing Grafana on my Home Server</a></li>
<li><a href="/en/2025/09/10/omarchy-2-0-bitwarden-self-hosted-vaultwarden/">Self-hosted Vaultwarden</a></li>
</ul>
]]></content:encoded><category>homelab</category><category>containers</category><category>devops</category><category>security</category></item><item><title>SQLite + Kamal: Rails Deploy Without Drama | Behind The M.Akita Chronicles</title><link>https://akitaonrails.github.io/en/2026/02/20/sqlite-kamal-rails-deploy-without-drama-behind-the-m-akita-chronicles/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2026/02/20/sqlite-kamal-rails-deploy-without-drama-behind-the-m-akita-chronicles/</guid><pubDate>Fri, 20 Feb 2026 03:18:55 +0000</pubDate><description>&lt;p&gt;This post is part of a series; follow along via the tag &lt;a href="https://akitaonrails.github.io/en/tags/the-m-akita-chronicles/"&gt;/themakitachronicles&lt;/a&gt;. This is part 7.&lt;/p&gt;
&lt;p&gt;And make sure to subscribe to my new newsletter &lt;a href="https://themakitachronicles.com/"target="_blank" rel="noopener"&gt;The M.Akita Chronicles&lt;/a&gt;!&lt;/p&gt;
&lt;p&gt;&amp;ndash;&lt;/p&gt;
&lt;p&gt;If I told you that you can run a full Rails application in production — with database, job queue, cache — on a single $12/month VPS without installing PostgreSQL, Redis, or any other external service, would you believe me?&lt;/p&gt;
&lt;p&gt;Well, you can. That&amp;rsquo;s Rails 8 with SQLite and Kamal.&lt;/p&gt;</description><content:encoded><![CDATA[<p>This post is part of a series; follow along via the tag <a href="/en/tags/the-m-akita-chronicles/">/themakitachronicles</a>. This is part 7.</p>
<p>And make sure to subscribe to my new newsletter <a href="https://themakitachronicles.com/"target="_blank" rel="noopener">The M.Akita Chronicles</a>!</p>
<p>&ndash;</p>
<p>If I told you that you can run a full Rails application in production — with database, job queue, cache — on a single $12/month VPS without installing PostgreSQL, Redis, or any other external service, would you believe me?</p>
<p>Well, you can. That&rsquo;s Rails 8 with SQLite and Kamal.</p>
<h2>SQLite on Rails 8: now it&rsquo;s for real<span class="hx:absolute hx:-mt-20" id="sqlite-on-rails-8-now-its-for-real"></span>
    <a href="#sqlite-on-rails-8-now-its-for-real" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Rails 8 brought SQLite as a real production option, not as a development toy. And when I say &ldquo;real&rdquo;, I mean <code>rails new</code> generates everything ready to go: main database in SQLite, SolidQueue (jobs) in SQLite, SolidCache (cache) in SQLite, SolidCable (WebSocket) in SQLite.</p>
<p><strong>Four services that used to require PostgreSQL + Redis + Memcached are now <code>.sqlite3</code> files on disk.</strong></p>
<p>WAL mode (Write-Ahead Logging) enables concurrent reads while a write is happening. For applications with tens of thousands of requests per day — which is the vast majority of Rails applications in the world — SQLite is more than enough. And read latency is measured in <strong>microseconds</strong>, not milliseconds. It&rsquo;s local disk access, not a network round-trip.</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"># database.yml — production with SQLite</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">production</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">primary</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">&lt;&lt;</span><span class="p">:</span><span class="w"> </span><span class="cp">*default</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">database</span><span class="p">:</span><span class="w"> </span><span class="l">storage/production.sqlite3</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">queue</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">&lt;&lt;</span><span class="p">:</span><span class="w"> </span><span class="cp">*default</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">database</span><span class="p">:</span><span class="w"> </span><span class="l">storage/production_queue.sqlite3</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">migrations_paths</span><span class="p">:</span><span class="w"> </span><span class="l">db/queue_migrate</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">cache</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">&lt;&lt;</span><span class="p">:</span><span class="w"> </span><span class="cp">*default</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">database</span><span class="p">:</span><span class="w"> </span><span class="l">storage/production_cache.sqlite3</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">migrations_paths</span><span class="p">:</span><span class="w"> </span><span class="l">db/cache_migrate</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>Three files. No TCP connection. No pooling. No &ldquo;is PostgreSQL accepting connections?&rdquo;. Backup is a <code>VACUUM INTO</code> and that&rsquo;s it — a single SQL command that produces an atomic copy. Restore? Paste the file back. Try doing that with a 2GB PostgreSQL dump.</p>
<h2>Kamal: Docker Deploy Without Kubernetes<span class="hx:absolute hx:-mt-20" id="kamal-docker-deploy-without-kubernetes"></span>
    <a href="#kamal-docker-deploy-without-kubernetes" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Kamal is the Rails 8 deployer. Think of it as &ldquo;SSH + Docker, but smart&rdquo;. No Kubernetes. No Helm charts. No 300-line YAML describing an Ingress Controller.</p>
<p>The entire configuration fits in one file:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="c"># config/deploy.yml</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">service</span><span class="p">:</span><span class="w"> </span><span class="l">minha-app</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">meuregistry/minha-app</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">servers</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">web</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="m">107.170.70.49</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">options</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">network</span><span class="p">:</span><span class="w"> </span><span class="l">minha-network</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">minha-app-storage:/rails/storage</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">minha-app-content:/rails/content</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">proxy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">ssl</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">host</span><span class="p">:</span><span class="w"> </span><span class="l">app.meudominio.com</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">builder</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">arch</span><span class="p">:</span><span class="w"> </span><span class="l">amd64</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">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">clear</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">RAILS_LOG_LEVEL</span><span class="p">:</span><span class="w"> </span><span class="l">info</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="l">RAILS_MASTER_KEY</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">DATABASE_URL</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>What Kamal does when you run <code>kamal deploy</code>:</p>
<ol>
<li>Builds the Docker image locally (or in CI)</li>
<li>Pushes to the registry</li>
<li>SSHs into the server</li>
<li>Pulls the new image</li>
<li>Runs migrations</li>
<li>Restarts the container with <strong>zero downtime</strong> via kamal-proxy</li>
</ol>
<p>The kamal-proxy is the secret: it acts as a reverse proxy in front of your containers. When the deploy happens, it brings up the new container, waits for it to be healthy, redirects traffic, and then tears down the old one. Your users never notice a thing.</p>
<h2>Docker Volumes: The Secret of SQLite in Production<span class="hx:absolute hx:-mt-20" id="docker-volumes-the-secret-of-sqlite-in-production"></span>
    <a href="#docker-volumes-the-secret-of-sqlite-in-production" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The critical point of SQLite in Docker is: <strong>the data can&rsquo;t die with the container</strong>. Docker volumes solve this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">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">minha-app-storage:/rails/storage</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>The <code>storage/</code> directory contains all the SQLite databases. With the Docker volume, they persist across deploys. The container is ephemeral; the data is permanent.</p>
<p>But there&rsquo;s a detail that trips up a lot of people: if you run <strong>two different services</strong> that share data through files (not database), you need a shared 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-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="c"># Service A writes content</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="w">  </span>- <span class="l">content-compartilhado:/rails/content</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="c"># Service B reads content</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="w">  </span>- <span class="l">content-compartilhado:/rails/content</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>The same Docker volume mounted on two containers. Simple, it works, and you don&rsquo;t need NFS, S3, or anything fancy. It&rsquo;s a directory on the server&rsquo;s disk.</p>
<h2>Multiple Services on the Same VPS<span class="hx:absolute hx:-mt-20" id="multiple-services-on-the-same-vps"></span>
    <a href="#multiple-services-on-the-same-vps" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>A rarely discussed advantage of Kamal: you can run <strong>multiple applications</strong> on the same server. The kamal-proxy routes by hostname:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>app.meudominio.com    → container-app
bot.meudominio.com    → container-bot</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Each service has its own <code>deploy.yml</code>, its own Docker image, its own deploy cycle. But they all run on the same VPS, sharing the same Docker network for internal communication.</p>
<p>SSL is automatic via Let&rsquo;s Encrypt — kamal-proxy handles that. Each hostname gets its certificate without you configuring anything.</p>
<h2>Hooks: Deploy Automation<span class="hx:absolute hx:-mt-20" id="hooks-deploy-automation"></span>
    <a href="#hooks-deploy-automation" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Kamal supports hooks at various points of the deploy cycle. The most useful is <code>pre-deploy</code>, which runs <strong>before</strong> the new container replaces the old one:</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="c1"># .kamal/hooks/pre-deploy</span>
</span></span><span class="line"><span class="cl"><span class="c1">#!/bin/sh</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Run migrations before the container goes live</span>
</span></span><span class="line"><span class="cl"><span class="n">ssh</span> <span class="n">root</span><span class="err">@</span><span class="vg">$KAMAL_HOSTS</span> <span class="p">\</span>
</span></span><span class="line"><span class="cl">  <span class="s2">&#34;docker exec minha-app-web bin/rails db:migrate&#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>Other useful hooks:</p>
<ul>
<li><code>post-deploy</code> — notify the team, clear cache</li>
<li><code>pre-connect</code> — check server health</li>
<li><code>docker-setup</code> — install dependencies on the host</li>
</ul>
<h2>The Dockerfile Rails Generates<span class="hx:absolute hx:-mt-20" id="the-dockerfile-rails-generates"></span>
    <a href="#the-dockerfile-rails-generates" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Rails 8 generates an optimized production Dockerfile. Some highlights:</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-dockerfile" data-lang="dockerfile"><span class="line"><span class="cl"><span class="c"># Multi-stage build: large build stage, minimal runtime stage</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">ruby:3.3-slim</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="s">base</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"># Install only runtime dependencies</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">RUN</span> apt-get install -y libsqlite3-0<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="k">FROM</span><span class="w"> </span><span class="s">base</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="s">build</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="c"># Here install everything to compile native gems</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">RUN</span> apt-get install -y build-essential libsqlite3-dev<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 and install gems</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">COPY</span> Gemfile* ./<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">RUN</span> bundle install --without development test<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"># Final stage: only what&#39;s needed to run</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">base</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">COPY</span> --from<span class="o">=</span>build /usr/local/bundle /usr/local/bundle<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="k">RUN</span> bundle <span class="nb">exec</span> bootsnap precompile --gemfile app/ lib/</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>Multi-stage build means the final image has no compilers, no development headers, nothing unnecessary. The production image stays lean.</p>
<h2>Secrets: No .env in Production<span class="hx:absolute hx:-mt-20" id="secrets-no-env-in-production"></span>
    <a href="#secrets-no-env-in-production" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Kamal has a secrets system that reads from a local file (<code>.kamal/secrets</code>) and injects them as environment variables into the container. That file <strong>never</strong> goes to git:</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"># .kamal/secrets (gitignored)</span>
</span></span><span class="line"><span class="cl"><span class="nv">RAILS_MASTER_KEY</span><span class="o">=</span>abc123...
</span></span><span class="line"><span class="cl"><span class="nv">AWS_ACCESS_KEY_ID</span><span class="o">=</span>AKIA...</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>In <code>deploy.yml</code>, you reference:</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">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">secret</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">RAILS_MASTER_KEY</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">AWS_ACCESS_KEY_ID</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>Kamal reads from the local file and configures it on the server. Simple, secure, no HashiCorp Vault or AWS Secrets Manager (unless you want — Kamal supports adapters).</p>
<h2>SQLite Backup: Absurdly Simple (If You Do It Right)<span class="hx:absolute hx:-mt-20" id="sqlite-backup-absurdly-simple-if-you-do-it-right"></span>
    <a href="#sqlite-backup-absurdly-simple-if-you-do-it-right" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Want a backup? One SQL command:</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">VACUUM</span><span class="w"> </span><span class="k">INTO</span><span class="w"> </span><span class="s1">&#39;/tmp/backup/newsletter.sqlite3&#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><code>VACUUM INTO</code> is <strong>atomic</strong> — it produces a consistent, defragmented copy of the database, even with writes happening. It runs while the application is serving requests normally. And the resulting copy is a complete, functional SQLite database — you can open it, query it, restore it.</p>
<p><strong>Warning</strong>: copying the <code>.sqlite3</code> file directly (<code>cp</code>, <code>tar</code>, <code>rsync</code>) from a database in use <strong>can corrupt the backup</strong>. If a write is in progress at the moment of the copy, you end up with a half-written file. SQLite has WAL (Write-Ahead Log) and journaling that protect the active database — but the raw copy doesn&rsquo;t inherit that protection.</p>
<p>In practice, I automated this with a Rails job that runs every hour:</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="k">class</span> <span class="nc">BackupDatabaseJob</span> <span class="o">&lt;</span> <span class="no">ApplicationJob</span>
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">perform</span>
</span></span><span class="line"><span class="cl">    <span class="n">backup_dir</span> <span class="o">=</span> <span class="no">File</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="no">Rails</span><span class="o">.</span><span class="n">configuration</span><span class="o">.</span><span class="n">content_dir</span><span class="p">,</span> <span class="s2">&#34;backups&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="no">FileUtils</span><span class="o">.</span><span class="n">mkdir_p</span><span class="p">(</span><span class="n">backup_dir</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">dest</span> <span class="o">=</span> <span class="no">File</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">backup_dir</span><span class="p">,</span> <span class="s2">&#34;newsletter.sqlite3&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span><span class="o">.</span><span class="n">connection</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="s2">&#34;VACUUM INTO &#39;</span><span class="si">#{</span><span class="n">dest</span><span class="si">}</span><span class="s2">&#39;&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>The backup goes to the shared content directory — the same one that&rsquo;s already synced with <code>rsync</code> to my local machine. No extra backup agent, no external service, no volume snapshot. One SQL, one file, one <code>rsync</code>.</p>
<p>Compare that with <code>pg_dump</code> of a production PostgreSQL database with dozens of tables and constraints. The SQLite backup is <strong>one command that produces a working file</strong>. Restore? Copy it back and restart.</p>
<h2>When NOT to Use SQLite<span class="hx:absolute hx:-mt-20" id="when-not-to-use-sqlite"></span>
    <a href="#when-not-to-use-sqlite" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>I&rsquo;ll be honest: SQLite isn&rsquo;t for everything.</p>
<ul>
<li><strong>Multiple web servers</strong> writing to the same database? No. SQLite is single-writer. If you need horizontal scaling with multiple nodes, go PostgreSQL.</li>
<li><strong>Massive datasets</strong> (hundreds of GB)? PostgreSQL has better query planning and parallelism.</li>
<li><strong>Replication and high availability</strong>? PostgreSQL with streaming replication.</li>
</ul>
<p>But the honest question is: <strong>how many applications really need that?</strong> The vast majority run on a single server and will never need more. And for those, SQLite + Kamal is the most productive combination out there today.</p>
<h2>The Real Cost<span class="hx:absolute hx:-mt-20" id="the-real-cost"></span>
    <a href="#the-real-cost" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Let&rsquo;s do the math:</p>
<table>
  <thead>
      <tr>
          <th>Component</th>
          <th>Before</th>
          <th>Now</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Server</td>
          <td>VPS $24/month</td>
          <td>VPS $12/month</td>
      </tr>
      <tr>
          <td>Database</td>
          <td>RDS PostgreSQL $30/month</td>
          <td>SQLite (included)</td>
      </tr>
      <tr>
          <td>Redis</td>
          <td>ElastiCache $15/month</td>
          <td>SolidQueue (included)</td>
      </tr>
      <tr>
          <td>Cache</td>
          <td>ElastiCache $15/month</td>
          <td>SolidCache (included)</td>
      </tr>
      <tr>
          <td>Deploy</td>
          <td>Complex CI/CD</td>
          <td><code>kamal deploy</code></td>
      </tr>
      <tr>
          <td>SSL</td>
          <td>Certbot + cron</td>
          <td>Automatic</td>
      </tr>
      <tr>
          <td><strong>Total</strong></td>
          <td><strong>~$84/month + headache</strong></td>
          <td><strong>$12/month</strong></td>
      </tr>
  </tbody>
</table>
<p>And the cognitive cost? Instead of debugging &ldquo;why did Redis lose my jobs?&rdquo;, &ldquo;why is PostgreSQL rejecting connections?&rdquo;, &ldquo;why did the cert expire?&rdquo;, you focus on what matters: <strong>the product</strong>.</p>
<h2>Conclusion<span class="hx:absolute hx:-mt-20" id="conclusion"></span>
    <a href="#conclusion" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>SQLite + Kamal is not a downgrade. The truth is that most applications never needed the complexity we thought they needed. Rails 8 embraced that and delivered a deploy experience that is, without exaggeration, the simplest that ever existed in the Ruby ecosystem.</p>
<p>One VPS. One command. Zero external services. And an application that runs as fast as any enterprise setup with 15 services in docker-compose.</p>
]]></content:encoded><category>the-m-akita-chronicles</category><category>rails</category><category>devops</category></item><item><title>Installing Grafana on My Home Server</title><link>https://akitaonrails.github.io/en/2025/09/10/installing-grafana-on-my-home-server/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2025/09/10/installing-grafana-on-my-home-server/</guid><pubDate>Wed, 10 Sep 2025 16:00:00 -0300</pubDate><description>&lt;p&gt;This post continues my &lt;a href="https://akitaonrails.github.io/en/tags/homeserver/"&gt;home server series&lt;/a&gt;, once again serving as notes for my future self. As I explained before, my home server is an Intel NUC mini-PC that I only access via SSH. So I wanted a way to monitor system resource usage without having to manually SSH in and open BTOP or NTOP.&lt;/p&gt;
&lt;p&gt;I had been too lazy to install Grafana back then, but I finally got around to it, and it was much easier than I expected, which is why I decided to share how I did it.&lt;/p&gt;</description><content:encoded><![CDATA[<p>This post continues my <a href="/en/tags/homeserver/">home server series</a>, once again serving as notes for my future self. As I explained before, my home server is an Intel NUC mini-PC that I only access via SSH. So I wanted a way to monitor system resource usage without having to manually SSH in and open BTOP or NTOP.</p>
<p>I had been too lazy to install Grafana back then, but I finally got around to it, and it was much easier than I expected, which is why I decided to share how I did it.</p>
<p>The idea is to spin up <a href="https://github.com/google/cadvisor"target="_blank" rel="noopener">cAdvisor</a>, which monitors container resources, and <a href="https://github.com/prometheus/prometheus"target="_blank" rel="noopener">Prometheus</a>, which scrapes system resource data, then consolidate the data from both into <a href="https://grafana.com/"target="_blank" rel="noopener">Grafana</a>, a web application that lets you build graphical dashboards to monitor your systems. If you do it right, in the end you&rsquo;ll have a screen like this:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20250910155108_screenshot-2025-09-10_15-50-49.png" alt="Grafana Dashboard"  loading="lazy" /></p>
<h3>Installation<span class="hx:absolute hx:-mt-20" id="installation"></span>
    <a href="#installation" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Spinning this up on my home server is very simple. I do everything with Docker Compose, so:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

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

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

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

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

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

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker compose -f monitor-docker-compose.yml up -d</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Done, super simple. I should have done this sooner.</p>
<p>As you can see in the Docker Compose file, when you open the site for the first time, the login is <code>admin</code> with password <code>change-me</code>. Obviously, the first thing to do is change the password:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20250910160042_screenshot-2025-09-10_16-00-31.png" alt="Admin Password"  loading="lazy" /></p>
<h3>Dashboards<span class="hx:absolute hx:-mt-20" id="dashboards"></span>
    <a href="#dashboards" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Now we&rsquo;re practically done. We can check whether Prometheus is working and integrated. Just go to the &ldquo;Connections&rdquo; menu and &ldquo;Data Sources&rdquo;:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20250910160151_screenshot-2025-09-10_16-01-40.png" alt="Data Sources"  loading="lazy" /></p>
<p>Grafana can consolidate data from dozens of different sources, and Prometheus is only one of them. In a larger, more complex infrastructure, this list would be huge.</p>
<p>To build dashboards, you can start from scratch and assemble them widget by widget, but it&rsquo;s easier to import dashboards that already exist. Just click the magnifying glass at the top, in the search box, and choose &ldquo;Import dashboards&rdquo;:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20250910160509_screenshot-2025-09-10_16-04-59.png" alt="import dashboards menu"  loading="lazy" /></p>
<p>On that page you can either import a JSON file, copy and paste the JSON, or search by an ID already registered on grafana.com:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20250910160603_screenshot-2025-09-10_16-05-46.png" alt="import by ID"  loading="lazy" /></p>
<p>Right off the bat, I learned that there are 3 IDs that are useful for me:</p>
<ul>
<li><strong>1860 - Node Exporter Full</strong> - the screenshot at the beginning of this article, with several system metrics like CPU, memory, disk, network, etc. The most useful one for me.</li>
<li><strong>13484 - Docker/cAdvisor Exporter</strong> - CPU and memory metrics, but broken down per container running on the server.</li>
<li><strong>3662 - Prometheus 2.0 overview</strong> - a general health summary.</li>
</ul>
<p>If you like these or other dashboards, just favorite them with a &ldquo;pin&rdquo; and you&rsquo;re set.</p>
<p>For example, here&rsquo;s the cAdvisor dashboard:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20250910160927_screenshot-2025-09-10_16-09-13.png" alt="cAdvisor dashboard"  loading="lazy" /></p>
<p>I haven&rsquo;t explored other dashboards yet. If you have ID recommendations to import, drop them in the comments below!</p>
<p>With this, I have a quick and good-looking view of my home server&rsquo;s resource usage, and I can spot anything weird happening right away. These services don&rsquo;t seem to consume many resources, so I can leave them running alongside the other containers.</p>
]]></content:encoded><category>homelab</category><category>containers</category><category>devops</category></item><item><title>[SmallTips] Running GitLab CI Runner Locally</title><link>https://akitaonrails.github.io/en/2018/04/28/smalltips-running-gitlab-ci-runner-locally/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2018/04/28/smalltips-running-gitlab-ci-runner-locally/</guid><pubDate>Sat, 28 Apr 2018 18:35:00 -0300</pubDate><description>&lt;p&gt;If there is one thing you should always do, it&amp;rsquo;s maintain your test suite. Every bug you fix, every new feature, you should add new tests.&lt;/p&gt;
&lt;p&gt;I have been working very hard the past few weeks, quite literally coding non-stop, 7 days a week. Remember that &amp;ldquo;stop complaining you don&amp;rsquo;t have time, what do you do from midnight to 6 AM?&amp;rdquo; thing? Yeah, won&amp;rsquo;t apply to me.&lt;/p&gt;
&lt;p&gt;And it&amp;rsquo;s exactly when you&amp;rsquo;re sleep deprived and exhausted that you start to make mistakes. Even though your code runs, locally, you will shoot yourself in the foot in one of those 3 AM coding spree sessions.&lt;/p&gt;</description><content:encoded><![CDATA[<p>If there is one thing you should always do, it&rsquo;s maintain your test suite. Every bug you fix, every new feature, you should add new tests.</p>
<p>I have been working very hard the past few weeks, quite literally coding non-stop, 7 days a week. Remember that &ldquo;stop complaining you don&rsquo;t have time, what do you do from midnight to 6 AM?&rdquo; thing? Yeah, won&rsquo;t apply to me.</p>
<p>And it&rsquo;s exactly when you&rsquo;re sleep deprived and exhausted that you start to make mistakes. Even though your code runs, locally, you will shoot yourself in the foot in one of those 3 AM coding spree sessions.</p>
<p>The test suite was really my sidekick, co-pilot, bringing me back to my senses whenever the night got too dark.</p>
<p>I maintain my own GitLab custom server for all of my company&rsquo;s client and internal projects. Just a bit of paranoia, to make sure my code is owned by me alone. And GitLab is a fantastic tool. Even more so because it has <a href="https://about.gitlab.com/features/gitlab-ci-cd/"target="_blank" rel="noopener">its own Continuous Integration companion</a>.</p>
<p>Just add your <code>.gitlab-ci.yml</code> file to your project, create a new branch, push it and create a Merge Request and the CI kicks in, with multiple <a href="https://docs.gitlab.com/ee/ci/yaml/#jobs"target="_blank" rel="noopener">parallel jobs</a>  if you want. Even when I was forgetting to run my tests, the CI would not let me fail.</p>
<p>Keeping a CI up and running does require you to stop and maintain your specs though. Sometimes you will wonder, why does a test run on your local machine but keeps failing in the CI server?</p>
<p>That&rsquo;s when it&rsquo;s super useful to run the CI docker image locally, to iron out remaining environment-dependent quirks.</p>
<p>And you can do just that by running the GitLab CI Runner itself. It will pick up your project&rsquo;s <code>.gitlab-ci.yml</code> and run it locally through docker. So whatever problem you&rsquo;re seeing on the server will quite definitely happen locally as well. With the added benefit that you don&rsquo;t need to wait in line in case there is a queue of jobs waiting to run (many developers working, not as many machines to chew the CI work immediately). And you won&rsquo;t be the one adding useless jobs to the same queue.</p>
<p>And, if you&rsquo;ve followed my recommendations and installed the awesome Arch Linux distro (or my personal favorite derivative, Manjaro Gnome) you can easily install the runner through AUR like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

<div><pre><code>kubectl create secret generic editor-credential \
--from-file=credential.json=/home/myself/download/my-project-1212121.json</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Also notice that, as a normal cron job, there is a schedule parameter that you might want to change. In the example, &ldquo;0 4 * * *&rdquo; means that it will run the snapshot every day at 4 AM.</p>
<p>Check out the <a href="https://github.com/grugnog/google-cloud-auto-snapshot"target="_blank" rel="noopener">original repository</a> of this solution for more details.</p>
<p>And this should be it for now!</p>
<p>As I said, in the beginning, this is not a complete procedure, just highlights of some of the important parts. If you&rsquo;re new to Kubernetes you just read about Deployment, Service, Ingress, but you have ReplicaSet, DaemonSet, and much more to play with.</p>
<p>I think this is also already too long to add a <a href="https://cloud.google.com/sql/docs/mysql/high-availability"target="_blank" rel="noopener">multi-region High Availability</a> setup explanation, so let&rsquo;s leave it at that.</p>
<p>Any corrections or suggestions are more than welcome as I am still in the learning process, and there are a ton of things that I still don&rsquo;t know myself.</p>
]]></content:encoded><category>rails</category><category>cloud</category><category>containers</category><category>devops</category></item><item><title>Rails 5.1 Deployment Checklist for Heroku</title><link>https://akitaonrails.github.io/en/2017/06/28/rails-5-1-heroku-deployment-checklist-for-heroku/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2017/06/28/rails-5-1-heroku-deployment-checklist-for-heroku/</guid><pubDate>Wed, 28 Jun 2017 15:12:00 -0300</pubDate><description>&lt;p&gt;I released &lt;a href="http://www.theconf.club"target="_blank" rel="noopener"&gt;THE CONF&lt;/a&gt; yesterday. I hope you enjoy the conference program and take advantage of the limited early-bird discount!&lt;/p&gt;
&lt;p&gt;Anyway, the website itself is super simple. A single page site. I chose to use Rails 5.1 as the site structure because it takes care of all the stuff I&amp;rsquo;d have to add manually in any other framework. Especially now that it brings native Yarn and Webpack support it&amp;rsquo;s a breeze to use by any competent front-end developer.&lt;/p&gt;</description><content:encoded><![CDATA[<p>I released <a href="http://www.theconf.club"target="_blank" rel="noopener">THE CONF</a> yesterday. I hope you enjoy the conference program and take advantage of the limited early-bird discount!</p>
<p>Anyway, the website itself is super simple. A single page site. I chose to use Rails 5.1 as the site structure because it takes care of all the stuff I&rsquo;d have to add manually in any other framework. Especially now that it brings native Yarn and Webpack support it&rsquo;s a breeze to use by any competent front-end developer.</p>
<p>But even with the many built-in niceties, a full production setup still requires extra steps that most beginners will not know. So I decided to compile a small checklist of things you must take care of before deploying to production. It&rsquo;s not an extensive and complete list for all use cases, but this should be enough for most small cases.</p>
<ul>
<li><a href="#setup">Setting up the project</a></li>
<li><a href="#cdn">Adding a CDN and configuring CORS support</a> (to be able to load fonts for example)</li>
<li><a href="#cache">Adding Memcached</a></li>
</ul>
<p><a name="setup"></a></p>
<h2>Setting up the project<span class="hx:absolute hx:-mt-20" id="setting-up-the-project"></span>
    <a href="#setting-up-the-project" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>One thing that most people forget, even experienced developers, is that the <code>rails new</code> command used to bootstrap the initial project structure accepts many option flags. Instead of having to manually tweak files later, you can start like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>rails new your_project \
--database=postgresql \
--webpack=react \
--skip-action-cable \
--skip-coffee
--skip-turbolinks</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>If you&rsquo;re building with React or another full-featured javascript framework, you will probably want to skip Turbolinks. Otherwise, if it&rsquo;s a simple site, do use <a href="https://sevos.io/2017/02/27/turbolinks-lifecycle-explained.html"target="_blank" rel="noopener">Turbolinks</a>.</p>
<p>Start using PostgreSQL from the get-go, don&rsquo;t use Sqlite3.</p>
<p>Skip Action Cable. Prefer a real solution such as <a href="https://pusher.com/"target="_blank" rel="noopener">Pusher.com</a>. If you really need something done in-house, consider something like my solution with Elixir, the <a href="http://expusherlite.cm42.io/"target="_blank" rel="noopener">Ex Pusher Lite</a>. Take this recommendation with a grain of salt, of course, for small things Action Cable is more than enough. I may write another post just elaborating on this point if people indicate they want to in the comments section.</p>
<p>Anyway, I digress. Make sure you have 2 boot files, first the canonical <code>Procfile</code> to be used by Heroku in production:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>web: bin/rails server -p $PORT -b 0.0.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>Second, a <code>Procfile.dev</code> to be used only in your development environment:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>web: ./bin/rails server
webpacker: ./bin/webpack-dev-server</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>This is how you fire up the webpack server that will compile your assets in real-time during development. You need to also remember to run these two dependency commands now:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>yarn install
bundle install</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>Install javascript dependencies with <code>yarn add [package]</code> and that&rsquo;s it! In production you don&rsquo;t use the webpack server (which is why we don&rsquo;t add it to the production <code>Procfile</code>), instead Heroku automatically detects the <a href="https://github.com/rails/webpacker"target="_blank" rel="noopener">webpacker</a> gem then it installs the nodejs buildpack, runs <code>yarn install</code> for you and when <code>rails assets:precompile</code> runs it will also execute <code>yarn run</code> which will pre-compile all the assets (javascript, stylesheets, images) with the proper fingerprinting for cache busting and everything else we are used to in the normal Rails Asset Pipeline.</p>
<p>So, for Heroku, you basically don&rsquo;t have to do anything. And in a custom deployment server you just need to remember to run the <code>assets:precompile</code> task and have it do everything for you.</p>
<p><a name="cdn"></a></p>
<h3>Adding a CDN and configuring CORS<span class="hx:absolute hx:-mt-20" id="adding-a-cdn-and-configuring-cors"></span>
    <a href="#adding-a-cdn-and-configuring-cors" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>This is really super easy. There is no reason why anyone wouldn&rsquo;t add a CDN to any website. Please just do it.</p>
<p>Open your AWS Management Console and <a href="https://console.aws.amazon.com/cloudfront/"target="_blank" rel="noopener">open CloudFront</a>. From there click on &ldquo;Create Distribution&rdquo; and just follow the defaults in the Wizard. The requirement is that you MUST know the domain and sub-domain you want to point to (for example &ldquo;<a href="https://www.theconf.club"target="_blank" rel="noopener">www.theconf.club</a>&rdquo;) in the &ldquo;Origin Domain Name&rdquo; field.</p>
<p>The only customization you MUST do is to change the <a href="http://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/header-caching.html#header-caching-web-cors"target="_blank" rel="noopener">&ldquo;Forward Headers&rdquo;</a> option to &ldquo;Whitelist&rdquo; in the &ldquo;Default Cache Behavior Settings&rdquo;. Then you need to add &ldquo;Origin&rdquo;, &ldquo;Access-Control-Request-Headers&rdquo;, and &ldquo;Access-Control-Request-Method&rdquo; to the Whitelist. And that&rsquo;s it, now your distribution is CORS enabled.</p>
<p>It will take some time to create (it has to configure many data centers around the world), but you will end up with a URL representing your distribution, something like <code>doz7rtw2u3wg4.cloudfront.net</code>. I recommend you add it as a Heroku environment variable like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>heroku config:set CDN_URL=doz7rtw2u3wg4.cloudfront.net</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Now, edit your <code>config/environments/production.rb</code> and add the following:</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">config</span><span class="o">.</span><span class="n">action_controller</span><span class="o">.</span><span class="n">asset_host</span> <span class="o">=</span> <span class="no">ENV</span><span class="o">[</span><span class="s1">&#39;CDN_URL&#39;</span><span class="o">]</span> <span class="k">if</span> <span class="no">ENV</span><span class="o">[</span><span class="s1">&#39;CDN_URL&#39;</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>To actually use the CDN you must declare every asset you use across your view templates using Rails View Helpers such as <code>image_tag</code>, <code>asset_path</code>, <code>javascript_pack_tag</code>, <code>stylesheet_pack_tag</code>, <code>stylesheet_link_tag</code>, etc. The Rails bootstrap already creates layout templates with such helpers, you just need to follow them.</p>
<p>When webpack runs, it will generate all static, optimized and pre-compiled assets in <code>public/packs</code> with a manifest file declaring the full URL pointing to the CDN. For example, if I fetch the <code>/app/public/packs/manifest.json</code> from the Heroku dyno directly, I will get something like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>{
  &#34;Roboto-Bold.woff&#34;: &#34;//doz7rtw2u3wg4.cloudfront.net/packs/Roboto-Bold-eed9aab5449cc9c8430d7d258108f602.woff&#34;,
  &#34;Roboto-Bold.woff2&#34;: &#34;//doz7rtw2u3wg4.cloudfront.net/packs/Roboto-Bold-c0f1e4a4fdfb8048c72e86aadb2a247d.woff2&#34;,
  &#34;Roboto-Light.woff&#34;: &#34;//doz7rtw2u3wg4.cloudfront.net/packs/Roboto-Light-ea36cd9a0e9eee97012a67b8a4570d7b.woff&#34;,
  &#34;Roboto-Light.woff2&#34;: &#34;//doz7rtw2u3wg4.cloudfront.net/packs/Roboto-Light-3c37aa69cd77e6a53a067170fa8fe2e9.woff2&#34;,
  &#34;Roboto-Medium.woff&#34;: &#34;//doz7rtw2u3wg4.cloudfront.net/packs/Roboto-Medium-cf4d60bc0b1d4b2314085919a00e1724.woff&#34;,
  &#34;Roboto-Medium.woff2&#34;: &#34;//doz7rtw2u3wg4.cloudfront.net/packs/Roboto-Medium-1561b424aaef2f704bbd89155b3ce514.woff2&#34;,
  &#34;Roboto-Regular.woff&#34;: &#34;//doz7rtw2u3wg4.cloudfront.net/packs/Roboto-Regular-3cf6adf61054c328b1b0ddcd8f9ce24d.woff&#34;,
  &#34;Roboto-Regular.woff2&#34;: &#34;//doz7rtw2u3wg4.cloudfront.net/packs/Roboto-Regular-5136cbe62a63604402f2fedb97f246f8.woff2&#34;,
  &#34;Roboto-Thin.woff&#34;: &#34;//doz7rtw2u3wg4.cloudfront.net/packs/Roboto-Thin-44b78f142603eb69f593ed4002ed7a4a.woff&#34;,
  &#34;Roboto-Thin.woff2&#34;: &#34;//doz7rtw2u3wg4.cloudfront.net/packs/Roboto-Thin-1f35e6a11d27d2e10d28946d42332dc5.woff2&#34;,
  &#34;application.css&#34;: &#34;//doz7rtw2u3wg4.cloudfront.net/packs/application-09b53ce9ca3dd595ee99.css&#34;,
  &#34;application.css.map&#34;: &#34;//doz7rtw2u3wg4.cloudfront.net/packs/application-09b53ce9ca3dd595ee99.css.map&#34;,
  &#34;application.js&#34;: &#34;//doz7rtw2u3wg4.cloudfront.net/packs/application-799300612ff6d6595198.js&#34;,
  &#34;application.js.map&#34;: &#34;//doz7rtw2u3wg4.cloudfront.net/packs/application-799300612ff6d6595198.js.map&#34;,
  &#34;home_page.js&#34;: &#34;//doz7rtw2u3wg4.cloudfront.net/packs/home_page-ff3b49407a1d01592ad5.js&#34;,
  &#34;home_page.js.map&#34;: &#34;//doz7rtw2u3wg4.cloudfront.net/packs/home_page-ff3b49407a1d01592ad5.js.map&#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>So, if for some reason I had to create a new CDN distribution, you have to remember to update the <code>CDN_URL</code> variable on Heroku and redeploy your app so it regenerates the assets and this manifest file. It will just use those URLs when rendering the final HTML.</p>
<p>When a user opens your site, it will receive the HTML with URLs pointing to the CDN. The very first time it won&rsquo;t have any cached assets, so it will ask your site for them. Your site must return the assets WITH the correct CORS headers so the CDN caches them with the headers and forwards those headers to the browser. The browser needs those headers because it will open from domain <code>www.theconf.club</code>, for example, but fonts are loading from <code>doz7rtw2u3wg4.cloudfront.net</code>, so it would raise a security warning and not load the fonts because they are in different domains. Hence the CORS headers the font provides, indicating that they are safe to load.</p>
<p>For your Rails app to return the proper headers, you need to add the <code>rack-cors</code> gem to your Gemfile. Then you must add a new configuration file at <code>config/initializers/rack-cors.rb</code> with:</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="k">if</span> <span class="n">defined?</span> <span class="no">Rack</span><span class="o">::</span><span class="no">Cors</span>
</span></span><span class="line"><span class="cl">  <span class="no">Rails</span><span class="o">.</span><span class="n">configuration</span><span class="o">.</span><span class="n">middleware</span><span class="o">.</span><span class="n">insert_before</span> <span class="mi">0</span><span class="p">,</span> <span class="no">Rack</span><span class="o">::</span><span class="no">Cors</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="n">allow</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">      <span class="n">origins</span> <span class="sx">%w[
</span></span></span><span class="line"><span class="cl"><span class="sx">        https://theconf.club
</span></span></span><span class="line"><span class="cl"><span class="sx">        http://theconf.club
</span></span></span><span class="line"><span class="cl"><span class="sx">        https://www.theconf.club
</span></span></span><span class="line"><span class="cl"><span class="sx">        http://www.theconf.club
</span></span></span><span class="line"><span class="cl"><span class="sx">        https://theconf.herokuapp.com
</span></span></span><span class="line"><span class="cl"><span class="sx">        http://theconf.herokuapp.com
</span></span></span><span class="line"><span class="cl"><span class="sx">      ]</span>
</span></span><span class="line"><span class="cl">      <span class="n">resource</span> <span class="s1">&#39;/assets/*&#39;</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</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>When you deploy, you know it&rsquo;s working correctly when you Curl an asset and it returns the Access-* headers like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>$ curl -I -s -X GET -H &#34;Origin: www.theconf.club&#34; http://www.theconf.club/packs/Roboto-Regular-5136cbe62a63604402f2fedb97f246f8.woff2
HTTP/1.1 200 OK
Server: Cowboy
Date: Wed, 28 Jun 2017 17:44:41 GMT
Connection: keep-alive
Access-Control-Allow-Origin: www.theconf.club
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Expose-Headers: 
Access-Control-Max-Age: 1728000
Access-Control-Allow-Credentials: true
Last-Modified: Wed, 28 Jun 2017 17:06:03 GMT
Content-Type: application/font-woff2
Cache-Control: public, max-age=2592000
Vary: Origin
Content-Length: 64832
Via: 1.1 vegur</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>And if everything above is already in place, you should be able to see the headers being forwarded through the CDN, like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>$ curl -I -s -X GET -H &#34;Origin: www.theconf.club&#34; http://doz7rtw2u3wg4.cloudfront.net/packs/Roboto-Regular-5136cbe62a63604402f2fedb97f246f8.woff2
HTTP/1.1 200 OK
Content-Type: application/font-woff2
Content-Length: 64832
Connection: keep-alive
Server: Cowboy
Date: Wed, 28 Jun 2017 17:45:18 GMT
Access-Control-Allow-Origin: www.theconf.club
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Expose-Headers: 
Access-Control-Max-Age: 1728000
Access-Control-Allow-Credentials: true
Last-Modified: Wed, 28 Jun 2017 17:06:03 GMT
Cache-Control: public, max-age=2592000
Via: 1.1 vegur, 1.1 86e9abdb4c15b9d3a542f9b93245e87e.cloudfront.net (CloudFront)
Vary: Origin
X-Cache: Miss from cloudfront
X-Amz-Cf-Id: tVkZ41RRr66iBT6atWTO_oeTY_jG0zCBFuXU8bKyClZDQ8kl-hDegA==</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 CDN is the secret sauce that allows any content-based website to scale way beyond what your server can provide. It&rsquo;s a huge cost-saving and it also makes for a much smoother user experience for your users.</p>
<p>One last caveat. The many Rails View Helpers such as <code>image_tag</code> allow you to add the image file name without an extension in development and they will correctly find the image. But on the server it will fail to render the template this way. As a rule of thumb <strong>ALWAYS</strong> fill in the entire filename and extension, for example <code>image_tag(&quot;logo.png&quot;)</code> instead of just <code>image_tag(&quot;logo&quot;)</code>.</p>
<p>You can see how this fails if you open a console in Heroku and check out how it fails to derive the full image URL:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>$ heroku run rails c                                                                                                                  
Running rails c on ⬢ theconf... up, run.8271 (Hobby)
Loading production environment (Rails 5.1.2)

irb(main):001:0&gt; ActionController::Base.helpers.asset_path(&#34;icon-goals&#34;)
Sprockets::Rails::Helper::AssetNotFound: The asset &#34;icon-goals&#34; is not present in the asset pipeline.
    from (irb):1

irb(main):002:0&gt; ActionController::Base.helpers.asset_path(&#34;icon-goals.png&#34;)
=&gt; &#34;//d134ipy19a646x.cloudfront.net/assets/icon-goals-b969b3b7325d33ad85a88dbb5b894832909ed738eea9964b9cf535646b93674b.png&#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><a name="cache"></a></p>
<h3>Adding Memcached<span class="hx:absolute hx:-mt-20" id="adding-memcached"></span>
    <a href="#adding-memcached" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Talking about caching is always a complicated thing, which is why many people avoid even trying. Even though you can go really crazy with super granular configurations such as using <a href="http://edgeguides.rubyonrails.org/caching_with_rails.html#russian-doll-caching"target="_blank" rel="noopener">Russian Doll Caching</a>, just adding cache in a few spots can greatly benefit you. And it&rsquo;s super easy to boot.</p>
<p>The first thing to do is add <a href="https://devcenter.heroku.com/articles/memcachier"target="_blank" rel="noopener">Memcachier</a> to your Heroku application. It has a free-tier and for most small apps it should be enough.</p>
<p>Configuration is also trivial. Start by adding the following gems to your <code>Gemfile</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-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">group</span> <span class="ss">:production</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">gem</span> <span class="s1">&#39;rack-cors&#39;</span><span class="p">,</span> <span class="nb">require</span><span class="p">:</span> <span class="s1">&#39;rack/cors&#39;</span>
</span></span><span class="line"><span class="cl">  <span class="n">gem</span> <span class="s1">&#39;rack-cache&#39;</span>
</span></span><span class="line"><span class="cl">  <span class="n">gem</span> <span class="s1">&#39;dalli&#39;</span>
</span></span><span class="line"><span class="cl">  <span class="n">gem</span> <span class="s1">&#39;kgio&#39;</span>
</span></span><span class="line"><span class="cl">  <span class="n">gem</span> <span class="s1">&#39;memcachier&#39;</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Now you must edit your <code>config/environments/production.rb</code> like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">config</span><span class="o">.</span><span class="n">cache_store</span> <span class="o">=</span> <span class="ss">:dalli_store</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">client</span> <span class="o">=</span> <span class="no">Dalli</span><span class="o">::</span><span class="no">Client</span><span class="o">.</span><span class="n">new</span><span class="p">((</span><span class="no">ENV</span><span class="o">[</span><span class="s2">&#34;MEMCACHIER_SERVERS&#34;</span><span class="o">]</span> <span class="o">||</span> <span class="s2">&#34;&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">split</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="ss">:username</span> <span class="o">=&gt;</span> <span class="no">ENV</span><span class="o">[</span><span class="s2">&#34;MEMCACHIER_USERNAME&#34;</span><span class="o">]</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                           <span class="ss">:password</span> <span class="o">=&gt;</span> <span class="no">ENV</span><span class="o">[</span><span class="s2">&#34;MEMCACHIER_PASSWORD&#34;</span><span class="o">]</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                           <span class="ss">:failover</span> <span class="o">=&gt;</span> <span class="kp">true</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                           <span class="ss">:socket_timeout</span> <span class="o">=&gt;</span> <span class="mi">1</span><span class="o">.</span><span class="mi">5</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                           <span class="ss">:socket_failure_delay</span> <span class="o">=&gt;</span> <span class="mi">0</span><span class="o">.</span><span class="mi">2</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                           <span class="ss">:value_max_bytes</span> <span class="o">=&gt;</span> <span class="mi">10485760</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">config</span><span class="o">.</span><span class="n">action_dispatch</span><span class="o">.</span><span class="n">rack_cache</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="ss">:metastore</span>    <span class="o">=&gt;</span> <span class="n">client</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="ss">:entitystore</span>  <span class="o">=&gt;</span> <span class="n">client</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>Now, let&rsquo;s say you have a block in your template that requires a bunch of records from your database. But you know that those records barely change. What can you do? One alternative is to cache the ActiveRecord query entirely like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">HomePageController</span> <span class="o">&lt;</span> <span class="no">ApplicationController</span>
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">index</span>
</span></span><span class="line"><span class="cl">    <span class="vi">@selected_proposals</span> <span class="o">=</span> <span class="no">Rails</span><span class="o">.</span><span class="n">cache</span><span class="o">.</span><span class="n">fetch</span><span class="p">(</span><span class="s1">&#39;selected_proposals&#39;</span><span class="p">,</span> <span class="ss">expires_in</span><span class="p">:</span> <span class="mi">1</span><span class="o">.</span><span class="n">day</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">      <span class="no">Proposal</span><span class="o">.</span><span class="n">includes</span><span class="p">(</span><span class="ss">:authors</span><span class="p">)</span><span class="o">.</span><span class="n">where</span><span class="p">(</span><span class="ss">selected</span><span class="p">:</span> <span class="kp">true</span><span class="p">)</span><span class="o">.</span><span class="n">to_a</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>The <code>#to_a</code> is necessary because ActiveRecord queries are lazy. The <code>#to_a</code> forces it to fetch and it will be cached. Next time, it will not touch the database for an entire day!</p>
<p>I could also have added a <code>cache do ... end</code> block in the template itself. There are many options. Point is that it&rsquo;s not as difficult as most people would think.</p>
<p>What makes caching difficult is adding expiration logic. And the rule of thumb is: never try to manually expire any cache. Just change the lookup key for something else and let the old data just die (memcached will take care of getting rid of unused old data).</p>
<p>You really want to read the <a href="http://edgeguides.rubyonrails.org/caching_with_rails.html"target="_blank" rel="noopener">Rails Guides on Caching</a>. It really is not as difficult as you think and you can cache only the few snippets you know are performance-sensitive and leave the other parts that are highly dynamic and you&rsquo;re unsure how to properly cache.</p>
<p>But as it&rsquo;s super cheap, just use it right now.</p>
<p><a name="ssl"></a></p>
<h3>Adding SSL support<span class="hx:absolute hx:-mt-20" id="adding-ssl-support"></span>
    <a href="#adding-ssl-support" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>If you have any security-sensitive transactions going on (ex. purchases) you want to use SSL. Again, many people avoid it because it&rsquo;s usually difficult to even understand how to properly get a certificate.</p>
<p><a href="https://letsencrypt.org/"target="_blank" rel="noopener">Let&rsquo;s Encrypt</a> made the process super trivial. Kudos to them! And even better: it&rsquo;s free! So, you don&rsquo;t have any excuses to not have SSL.</p>
<p>And over Heroku, it&rsquo;s <a href="https://devcenter.heroku.com/articles/automated-certificate-management"target="_blank" rel="noopener">even easier</a>!</p>
<blockquote>
  <p>ACM (Automated Certificate Management) is enabled by default for all applications created after March 21, 2017 that are running on Hobby or Professional dynos. The following steps are for applications currently running on Free dynos, and for applications created before that date.</p>

</blockquote>
<p>On the Free Tier Dynos, this is what you do:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>heroku certs:auto:enable</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>Check status with <code>heroku certs:auto</code>.</p>
<p>Done, there is no step 2.</p>
<p>We used to have to use the complicated gem <a href="https://github.com/pixielabs/letsencrypt-rails-heroku"target="_blank" rel="noopener">letsencrypt-rails-heroku</a>, but now it&rsquo;s just too easy.</p>
<h3>Conclusion<span class="hx:absolute hx:-mt-20" id="conclusion"></span>
    <a href="#conclusion" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>I believe this covers the very basics of stuff you should be doing before deploying your small app to Heroku. For larger apps there are many more concerns that are beyond the scope of this post.</p>
<p>If you remember about more tips and tricks, please share in the comments section below.</p>
]]></content:encoded><category>rails</category><category>devops</category><category>tutorials</category></item><item><title>Elixir Phoenix App deployed into a Load Balanced DigitalOcean setup</title><link>https://akitaonrails.github.io/en/2016/12/23/elixir-phoenix-app-deployed-into-a-load-balanced-digitalocean-setup/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2016/12/23/elixir-phoenix-app-deployed-into-a-load-balanced-digitalocean-setup/</guid><pubDate>Fri, 23 Dec 2016 15:07:00 -0200</pubDate><description>&lt;p&gt;One of the main advantages of building a Websockets enabled web application using Phoenix is how &amp;ldquo;easy&amp;rdquo; it is for Erlang to connect itself into a cluster.&lt;/p&gt;
&lt;p&gt;For starters, Erlang does not need multiple processes like Ruby (which is limited to one connection per process, or per thread if you&amp;rsquo;re using a threaded-server like Puma). One single Erlang process will take over the entire machine, if you need to. Internally it will keep one real thread per machine-core. And each thread will have its own Scheduler to manage as many micro-processes as you need. You can read all about it in my post titled &lt;a href="http://www.akitaonrails.com/en/en/2015/11/25/yocto-services-e-meu-primeiro-mes-com-elixir/"target="_blank" rel="noopener"&gt;&amp;ldquo;Yocto Services&amp;rdquo;&lt;/a&gt;.&lt;/p&gt;</description><content:encoded><![CDATA[<p>One of the main advantages of building a Websockets enabled web application using Phoenix is how &ldquo;easy&rdquo; it is for Erlang to connect itself into a cluster.</p>
<p>For starters, Erlang does not need multiple processes like Ruby (which is limited to one connection per process, or per thread if you&rsquo;re using a threaded-server like Puma). One single Erlang process will take over the entire machine, if you need to. Internally it will keep one real thread per machine-core. And each thread will have its own Scheduler to manage as many micro-processes as you need. You can read all about it in my post titled <a href="http://www.akitaonrails.com/en/en/2015/11/25/yocto-services-e-meu-primeiro-mes-com-elixir/"target="_blank" rel="noopener">&ldquo;Yocto Services&rdquo;</a>.</p>
<p>Moreover, Erlang has built-in capabilities to form a cluster, where each Erlang instance acts as a peer-to-peer Node, without the need for a centralized coordinator. You can read all about it in my post about <a href="http://www.akitaonrails.com/en/en/2015/11/25/exercicio-exmessenger-entendendo-nodes-em-elixir/"target="_blank" rel="noopener">Nodes</a>. The power of Erlang is in how &ldquo;easy&rdquo; it is to form reliable distributed systems.</p>
<p>You can fire up many Phoenix instances and from one of the instances, it can broadcast messages to Users subscribed in Channels even if their sockets are connected to different instances. It&rsquo;s seamless and you don&rsquo;t need to do anything special in your code. Phoenix, Elixir and Erlang are doing all the heavy lifting for you behind the scenes.</p>
<h3>No Heroku for You :-(<span class="hx:absolute hx:-mt-20" id="no-heroku-for-you--"></span>
    <a href="#no-heroku-for-you--" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Because you want to take advantage of this scalability and high availability feature for distributed systems (in the small example of a real-time chat system) you will need to have more control over your infrastructure. This requirement rules out the majority of Platform as a Service (PaaS) offerings out there, such as Heroku. Heroku&rsquo;s model revolves around single, volatile processes in isolated containers. Those jailed processes (dynos) are not aware of other processes or the internal networking, so you can&rsquo;t fire up Dynos and have them form a cluster because they won&rsquo;t be able to find each other.</p>
<p>If you already know how to configure Linux related stuff: Postgresql, HAproxy, etc, go ahead directly to the <a href="#phoenix-setup">Phoenix-specific section</a>.</p>
<h3>IaaS (DigitalOcean) to the rescue!<span class="hx:absolute hx:-mt-20" id="iaas-digitalocean-to-the-rescue"></span>
    <a href="#iaas-digitalocean-to-the-rescue" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>You want long lived processes in network reachable servers (either through private networking, VPN, or plain simple - insecure! - public networks).</p>
<p>In this example I want to walk you through a very simple deployment using DigitalOcean (you can choose any IaaS, such as AWS, Google Cloud, Azure or whatever you feel more comfortable with).</p>
<p>I have created 4 droplets (all using the smallest size of 512Mb of RAM):</p>
<ul>
<li>1 Postgresql database (single point of failure: it&rsquo;s not the focus of this article to build a highly available, replicated database setup);</li>
<li>1 HAProxy server (single point of failure: again, it&rsquo;s not the focus to create a highly available load balancing scheme);</li>
<li>2 Phoenix servers - one in the NYC datacenter and another in the London datacenter, to demonstrate how easy it is for Erlang to form clusters even with geographically separated boxes.</li>
</ul>
<h3>Basic Ubuntu 16.04 configuration<span class="hx:absolute hx:-mt-20" id="basic-ubuntu-1604-configuration"></span>
    <a href="#basic-ubuntu-1604-configuration" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><ul>
<li>Goals: configure locale, assure unattended updates are up, upgrade packages, install and configure Elixir and Node.</li>
<li>You should also do: change SSH to another port and install [fail2ban](<a href="https://www.digitalocean.com/community/tutorials/how-to-protect-ssh-with-fail2ban-on-ubuntu-14-04"target="_blank" rel="noopener">https://www.digitalocean.com/community/tutorials/how-to-protect-ssh-with-fail2ban-on-ubuntu-14-04</a>, disallow login through password).</li>
</ul>
<p>You will want to read my post about <a href="http://www.akitaonrails.com/en/2016/09/21/ubuntu-16-04-lts-xenial-no-vagrant-com-vmware-fusion"target="_blank" rel="noopener">configuring Ubuntu 16.04</a>. To summarize, start by configuring proper UTF-8:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>sudo locale-gen &#34;en_US.UTF-8&#34;
sudo dpkg-reconfigure locales
sudo update-locale LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8
echo &#39;LC_ALL=en_US.UTF-8&#39; | sudo tee -a /etc/environment
echo &#39;LANG=en_US.UTF-8&#39; | sudo tee -a /etc/environment</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>Make sure you add a proper user into the sudo group and from now on do not use the root user. I will create a user named <code>pusher</code> and I will explain in another post why. You should create a username that suits your application.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>adduser pusher
usermod -aG sudo pusher</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Now log out and log in again through this user. <code>ssh pusher@server-ip-address</code>. If you&rsquo;re on a Mac copy the public key of your SSH certificate like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>ssh-copy-id -i ~/.ssh/id_ed25519.pub pusher@server-ip-address</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>It creates the <code>.ssh/authorized_keys</code> if it doesn&rsquo;t exist, sets the correct permission bits and appends your public key. You can do it manually, of course.</p>
<p>DigitalOcean&rsquo;s droplets start without a swap file and I&rsquo;d recommend adding one, especially if you want to start with the smaller boxes with less than 1GB of RAM:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
sudo cp /etc/fstab /etc/fstab.bak
echo &#39;/swapfile none swap sw 0 0&#39; | sudo tee -a /etc/fstab
sudo sysctl vm.swappiness=10
sudo sysctl vm.vfs_cache_pressure=50
echo &#39;vm.swappiness=10&#39; | sudo tee -a /etc/sysctl.conf
echo &#39;vm.vfs_cache_pressure=50&#39; | sudo tee -a /etc/sysctl.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>Make sure you have unattended upgrades configured. You will want at least to have security updates automatically installed when available.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>sudo apt install unattended-upgrades</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Now, let&rsquo;s install Elixir and Node (Phoenix needs Node.js):</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>wget https://packages.erlang-solutions.com/erlang-solutions_1.0_all.deb &amp;&amp; sudo dpkg -i erlang-solutions_1.0_all.deb
curl -sL https://deb.nodesource.com/setup_6.x | sudo -E bash -
sudo apt-get update
sudo apt-get upgrade
sudo apt-get install build-essential nodejs esl-erlang elixir erlang-eunit erlang-base-hipe
sudo npm install -g brunch
mix local.hex
mix archive.install https://github.com/phoenixframework/archives/raw/master/phoenix_new.ez # this is optional, install if you want to manually test phoenix in your box</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Now you have an Elixir capable machine ready. Create a image snapshot over DigitalOcean, move it to the regions where you want to create your other droplets and use this image to create as many droplets as you need.</p>
<p>For this example, I created a second droplet in the London region, a third droplet for postgresql in the NYC1 region and a fourth droplet in the NYC3 region for HAProxy.</p>
<p>I will refer to their public IP addresses as <strong>&ldquo;nyc-ip-address&rdquo;</strong>, <strong>&ldquo;lon-ip-address&rdquo;</strong>, <strong>&ldquo;pg-ip-address&rdquo;</strong>, and <strong>&ldquo;ha-ip-address&rdquo;</strong>.</p>
<h3>Basic PostgreSQL configuration<span class="hx:absolute hx:-mt-20" id="basic-postgresql-configuration"></span>
    <a href="#basic-postgresql-configuration" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><ul>
<li>Goal: basic configuration of Postgresql to allow the Phoenix servers to connect.</li>
<li>To do: create a secondary role just to connect to the application database and another superuser role to create the database and migrate the schema. Also lock down the machine and configure SSH tunnels or another secure way, at least private networking, rather than allowing plain 5432 TCP port connections from the public Internet.</li>
</ul>
<p>Now you can connect to <code>ssh pusher@pg-ip-address</code> and follow this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>sudo apt-get install postgresql postgresql-contrib</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>You should create a new role with the same name of the user you added above (&ldquo;pusher&rdquo; in our example):</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>$ sudo -u postgres createuser --interactive

Enter name of role to add: pusher
Shall the new role be a superuser? (y/n) y

$ sudo -u postgres createdb pusher</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>Postgresql expects to find a database with the same name as the role and the role should have the same name as the Linux user. Now you can use <code>psql</code> to set a password for this new role:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>$ sudo -u postgres psql
\password pusher</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>Register a secure password, take note and let&rsquo;s move on.</p>
<p>Postgresql comes locked down to external connections. One way to connect from the outside is to configure your servers to create an <a href="https://www.postgresql.org/docs/9.1/static/ssh-tunnels.html"target="_blank" rel="noopener">SSH tunnel</a> to the database server and keep external TCP connections through port 5432 disavowed.</p>
<p>But for this example, we will just allow connections from the public Internet to the 5432 TCP port. <strong>Warning:</strong> this is VERY insecure!</p>
<p>Edit the <code>/etc/postgresql/9.5/main/postgresql.conf</code> and find the <code>listen_addresses</code> configuration line and allow it:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>listen_addresses = &#39;*&#39;    # what IP address(es) to listen on;</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>This should bind the server to the TCP port. Now edit <code>/etc/postgresql/9.5/main/pg_hba.conf</code> and edit the end to look like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code># IPv4 local connections:
host    all             all             127.0.0.1/32            trust
host    all             all             your-local-machine-ip-address/24        trust
host    all             all             nyc-ip-address/24       trust
host    all             all             lon-ip-address/24       trust
# IPv6 local connections:
host    all             all             ::1/128                 trust</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>Save the configuration file and restart the server:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>sudo service postgresql restart</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>See what I did there? I only allowed connections coming from the public IPs of the Phoenix servers. This does not make the server secure, just a little bit less vulnerable. If you&rsquo;re behind a DHCP/NAT based network, just Google for &ldquo;what&rsquo;s my IP&rdquo; to see your public facing IP address - which is probably shared by many other users, remember you&rsquo;re allowing connections from an insecure IP to your database server! Once you make initial tests, create your new schema, then you should remove that <code>your-local-machine-ip-address/24</code> line from the configuration.</p>
<p>From your Phoenix application, you can edit your local <code>config/prod.secret.exs</code> file to look like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># Configure your database</span>
</span></span><span class="line"><span class="cl"><span class="n">config</span> <span class="ss">:your_app_name</span><span class="p">,</span> <span class="no">ExPusherLite</span><span class="o">.</span><span class="n">Repo</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="ss">adapter</span><span class="p">:</span> <span class="no">Ecto</span><span class="o">.</span><span class="n">Adapters</span><span class="o">.</span><span class="n">Postgres</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="ss">username</span><span class="p">:</span> <span class="s2">&#34;pusher&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="ss">password</span><span class="p">:</span> <span class="s2">&#34;your-super-secure-pg-password&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="ss">database</span><span class="p">:</span> <span class="s2">&#34;your-app-database-name&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="ss">hostname</span><span class="p">:</span> <span class="s2">&#34;pg-ip-address&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="ss">pool_size</span><span class="p">:</span> <span class="mi">20</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>Replace the information for your server and database and now you can test it out like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>MIX_ENV=prod iex -S mix phoenix.server</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>If you see a <code>:econnrefused</code> message from postgrex, then you&rsquo;re in trouble. Re-check your configuration, restart the server and try again. If everything connects, you can run <code>MIX_ENV=prod mix do ecto.create, ecto.migrate</code> to prepare your database.</p>
<p>Finally, you will want to lock down the rest of your server with UFW, at the very least. UFW should come pre-installed in Ubuntu 16, so you can just do:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>ufw allow 5432
ufw allow ssh
ufw enable</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>That&rsquo;s it. And again, this does not make your server secure, it just makes it less insecure. There is a huge difference!</p>
<p>And by the way, if you&rsquo;re a Docker fan:</p>
<blockquote>
  <p>DO NOT INSTALL A DATABASE INSIDE A DOCKER CONTAINER!</p>

</blockquote>
<p><a href="http://patrobinson.github.io/2016/11/07/thou-shalt-not-run-a-database-inside-a-container/"target="_blank" rel="noopener">You have been warned</a>!</p>
<h3>Basic HAProxy configuration<span class="hx:absolute hx:-mt-20" id="basic-haproxy-configuration"></span>
    <a href="#basic-haproxy-configuration" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><ul>
<li>Goals: Provide a simple solution to load balance between our 2 Phoenix servers.</li>
<li>To do: There is something wrong with session checking or something like that as sometimes I have to refresh my browser so I am not sent back to the login form in my application. Phoenix uses cookie-based sessions so I don&rsquo;t think it is missing sessions.</li>
</ul>
<p>Now let&rsquo;s <code>ssh pusher@ha-ip-address</code>. This one is easy, let&rsquo;s just install HAProxy:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>sudo apt-get install haproxy</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>Edit <code>/etc/haproxy/haproxy.cfg</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>...
listen your-app-name
  bind 0.0.0.0:80
  mode http
  stats enable
  stats uri /haproxy?stats
  stats realm Strictly\ Private
  stats auth admin:some-secure-password-for-admin
  option forwardfor
  option http-server-close
  option httpclose
  balance roundrobin
  cookie SERVERID insert indirect nocache
  server us your_us_server_IP:8080 check cookie us1
  server uk your_uk_server_IP:8080 check cookie uk1</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>You can avoid the <code>stats</code> lines if you have other means of monitoring, otherwise set a secure password for the <code>admin</code> user. One very important part is the <code>option http-server-close</code> as explained in <a href="http://blog.silverbucket.net/post/31927044856/3-ways-to-configure-haproxy-for-websockets"target="_blank" rel="noopener">this other blog post</a>, otherwise you may have trouble with Websockets.</p>
<p>For some reason I am having some trouble with my application after I login and it sets the session, sometimes I have to refresh to be sent to the correct page, not sure why yet and I believe it&rsquo;s something in the HAProxy configuration. If anyone knows what it is, let me know in the comments section below. For now, I am just relying on Sticky sessions (server affinity) by making HAProxy write back a cookie with the server.</p>
<p>Now you can restart the server and enable UFW as well:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>sudo service haproxy restart
sudo ufw allow http
sudo ufw allow https
sudo ufw allow ssh
sudo ufw enable</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>You can easily add SSL support to your application by configuring HAProxy (and not the Phoenix nodes). The <a href="https://www.digitalocean.com/community/tutorials/how-to-secure-haproxy-with-let-s-encrypt-on-ubuntu-14-04"target="_blank" rel="noopener">DigitalOcean documentation</a> is comprehensive, so just follow it. At the end, my <code>haproxy.cfg</code> looks like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>global
   log /dev/log    local0
   log /dev/log    local1 notice
   chroot /var/lib/haproxy
   stats socket /run/haproxy/admin.sock mode 660 level admin
   stats timeout 30s
   user haproxy
   group haproxy
   daemon
   maxconn 2048
   tune.ssl.default-dh-param 2048

defaults
   log global
   mode http
   option httplog
   option dontlognull
   option redispatch
   option forwardfor
   option http-server-close
   timeout connect 5000
   timeout client  50000
   timeout server  50000

frontend www-http
   bind your_ha_proxy_IP:80
   reqadd X-Forwarded-Proto:\ http
   default_backend www-backend

frontend www-https
   bind your_ha_proxy_IP:443 ssl crt /etc/haproxy/certs/your_domain.pem
   reqadd X-Forwarded-Proto:\ https
   acl letsencrypt-acl path_beg /.well-known/acme-challenge/
   use_backend letsencrypt-backend if letsencrypt-acl
   default_backend www-backend

backend www-backend
   redirect scheme https if !{ ssl_fc }
   # setting session stickiness
   cookie SERVERID insert indirect nocache
   server us your_us_server_IP:8080 check cookie us1
   server uk your_uk_server_IP:8080 check cookie uk1

backend letsencrypt-backend
   server letsencrypt 127.0.0.1:54321</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Finally, I will assume you have a DNS server/service somewhere where you can register the IP of this HAproxy server as an A record so you can access it by a full name such as &ldquo;your-app-name.mydomain.com&rdquo;.</p>
<p><a name="phoenix-setup"></a></p>
<h3>Basic Phoenix Configuration<span class="hx:absolute hx:-mt-20" id="basic-phoenix-configuration"></span>
    <a href="#basic-phoenix-configuration" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><ul>
<li>Goal: configure the Phoenix app to be deployable. Configure the servers to have the necessary configuration files.</li>
<li>To do: find out a way to cut down the super slow deployment times.</li>
</ul>
<p>Finally, we have almost everything in place.</p>
<p>I will assume that you have a working Phoenix application already in place, otherwise create one from the many tutorials out there.</p>
<p>I have assembled this information from <a href="https://medium.com/@diamondgfx/deploying-phoenix-applications-with-exrm-97a3867ebd04#.7qyuplncx"target="_blank" rel="noopener">posts</a> such as <a href="http://engineering.pivotal.io/post/how-to-set-up-an-elixir-cluster-on-amazon-ec2/"target="_blank" rel="noopener">this very helpful one</a> from Pivotal about an AWS-based deployment. In summary you must do a number of changes to your configuration.</p>
<p>When you&rsquo;re developing your application, you will notice that whenever you run it, it delta-compiles what changed. The binary bits are in the <code>_build/dev</code> or <code>_build/test</code> in the form of <code>.beam</code> binaries (similar to what <code>.class</code> are for Java).</p>
<p>Different from Ruby or Python or PHP, you are not deploying source-code to production servers. It&rsquo;s more akin to Java, where you must have everything compiled into binary bits and packaged into what&rsquo;s called a <strong>release</strong>. It&rsquo;s like a &ldquo;.war&rdquo; or &ldquo;.ear&rdquo; if you&rsquo;re from Java.</p>
<p>To create this package people usually use &ldquo;exrm&rdquo;, but it&rsquo;s being replaced by <a href="https://github.com/bitwalker/distillery"target="_blank" rel="noopener">&ldquo;distillery&rdquo;</a>, so we will use it.</p>
<p>Then, if you&rsquo;re from Ruby you&rsquo;re familiar with Capistrano. Or if you&rsquo;re from Python, you know Capistrano&rsquo;s clone, Fabric. Elixir has a similar tool (much simpler at this point), called <a href="https://github.com/boldpoker/edeliver"target="_blank" rel="noopener">&ldquo;edeliver&rdquo;</a>. It&rsquo;s your basic SSH automation tool.</p>
<p>You add them to <code>mix.exs</code> just like any other dependency:</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="o">...</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">application</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="o">[</span><span class="ss">mod</span><span class="p">:</span> <span class="p">{</span><span class="no">ExPusherLite</span><span class="p">,</span> <span class="o">[]</span><span class="p">},</span>
</span></span><span class="line"><span class="cl">   <span class="ss">applications</span><span class="p">:</span> <span class="o">[...</span><span class="p">,</span> <span class="ss">:edeliver</span><span class="o">]]</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">defp</span> <span class="n">deps</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="o">[...</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">   <span class="p">{</span><span class="ss">:edeliver</span><span class="p">,</span> <span class="s2">&#34;~&gt; 1.4.0&#34;</span><span class="p">},</span>
</span></span><span class="line"><span class="cl">   <span class="p">{</span><span class="ss">:distillery</span><span class="p">,</span> <span class="s2">&#34;~&gt; 1.0&#34;</span><span class="p">}</span><span class="o">]</span>
</span></span><span class="line"><span class="cl"><span class="k">end</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>From the Pivotal blog post, the important thing to not forget is to edit this part in the <code>config/prod.exs</code> file:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="ss">http</span><span class="p">:</span> <span class="o">[</span><span class="ss">port</span><span class="p">:</span> <span class="mi">8080</span><span class="o">]</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="ss">url</span><span class="p">:</span> <span class="o">[</span><span class="ss">host</span><span class="p">:</span> <span class="s2">&#34;your-app-name.yourdomain.com&#34;</span><span class="p">,</span> <span class="ss">port</span><span class="p">:</span> <span class="mi">80</span><span class="o">]</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="o">...</span>
</span></span><span class="line"><span class="cl"><span class="n">config</span> <span class="ss">:phoenix</span><span class="p">,</span> <span class="ss">:serve_endpoints</span><span class="p">,</span> <span class="kp">true</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>You MUST hardcode the default PORT of the Phoenix web server and the allowed domain (remember the domain name you associated to your HAProxy server above? That one). And you MUST uncomment the <code>:serve_endpoints, true</code> line!</p>
<p>For edeliver to work you have to create a <code>.deliver/config</code> file like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>USING_DISTILLERY=true

# change this to your app name:
APP=&#34;your-app-name&#34;

# change this to your own servers IP and add as many as you want
US=&#34;nyc-ip-address&#34;
UK=&#34;lon-ip-address&#34;

# the user you created in your Ubuntu machines above
USER=&#34;pusher&#34;

# which server do you want to build the first release?
BUILD_HOST=$US
BUILD_USER=$USER
BUILD_AT=&#34;/tmp/edeliver/$APP/builds&#34;

# list the production servers declared above:
PRODUCTION_HOSTS=&#34;$US $UK&#34;
PRODUCTION_USER=$USER
DELIVER_TO=&#34;/home/$USER&#34;

# do not change here

LINK_VM_ARGS=&#34;/home/$USER/vm.args&#34;

# For *Phoenix* projects, symlink prod.secret.exs to our tmp source
pre_erlang_get_and_update_deps() {
  local _prod_secret_path=&#34;/home/$USER/prod.secret.exs&#34;
  if [ &#34;$TARGET_MIX_ENV&#34; = &#34;prod&#34; ]; then
    __sync_remote &#34;
      ln -sfn &#39;$_prod_secret_path&#39; &#39;$BUILD_AT/config/prod.secret.exs&#39;

      cd &#39;$BUILD_AT&#39;

      mkdir -p priv/static

      mix deps.get

      npm install

      brunch build --production

      APP=&#39;$APP&#39; MIX_ENV=&#39;$TARGET_MIX_ENV&#39; $MIX_CMD phoenix.digest $SILENCE
    &#34;
  fi
}</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Remember the information we&rsquo;ve been gathering since the beginning of this long recipe? These are the options you MUST change to your own. Just follow the comments in the file content above and add it to your git repository. By the way, your project is in a proper GIT repository, RIGHT??</p>
<p>If you like to use passphrase protected SSH private keys, then it&rsquo;s going to be a huge pain to deploy because for each command, edeliver will issue an SSH command that will keep asking for you passphrase, a dozen times through everything. You&rsquo;ve been warned! If you still don&rsquo;t mind that, and you&rsquo;re in a Mac you will have an extra trouble because the Terminal will not be able to create a prompt for you to input your passphrase. You must create an <code>/usr/local/bin/ssh-askpass</code> <a href="https://github.com/markcarver/mac-ssh-askpass"target="_blank" rel="noopener">script</a>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>#!/bin/bash
# Script: ssh-askpass
# Author: Mark Carver
# Created: 2011-09-14
# Licensed under GPL 3.0

# A ssh-askpass command for Mac OS X
# Based from author: Joseph Mocker, Sun Microsystems
# http://blogs.oracle.com/mock/entry/and_now_chicken_of_the

# To use this script:
#   Install this script running INSTALL as root
#
# If you plan on manually installing this script, please note that you will have
# to set the following variable for SSH to recognize where the script is located:
#   export SSH_ASKPASS=&#34;/path/to/ssh-askpass&#34;

TITLE=&#34;${SSH_ASKPASS_TITLE:-SSH}&#34;;
TEXT=&#34;$(whoami)&#39;s password:&#34;;
IFS=$(printf &#34;\n&#34;);
CODE=(&#34;on GetCurrentApp()&#34;);
CODE=(${CODE[*]} &#34;tell application \&#34;System Events\&#34; to get short name of first process whose frontmost is true&#34;);
CODE=(${CODE[*]} &#34;end GetCurrentApp&#34;);
CODE=(${CODE[*]} &#34;tell application GetCurrentApp()&#34;);
CODE=(${CODE[*]} &#34;activate&#34;);
CODE=(${CODE[*]} &#34;display dialog \&#34;${@:-$TEXT}\&#34; default answer \&#34;\&#34; with title \&#34;${TITLE}\&#34; with icon caution with hidden answer&#34;);
CODE=(${CODE[*]} &#34;text returned of result&#34;);
CODE=(${CODE[*]} &#34;end tell&#34;);
SCRIPT=&#34;/usr/bin/osascript&#34;
for LINE in ${CODE[*]}; do
      SCRIPT=&#34;${SCRIPT} -e $(printf &#34;%q&#34; &#34;${LINE}&#34;)&#34;;
done;
eval &#34;${SCRIPT}&#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>Now do this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>sudo chmod &#43;x /usr/local/bin/ssh-askpass
sudo ln -s /usr/local/bin/ssh-askpass /usr/X11R6/bin/ssh-askpass</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Remember, Macs only. And now every time you try to deploy you will receive a number of graphical prompt windows asking for the SSH private key passphrase. It&rsquo;s freaking annoying! And you must have <a href="https://www.xquartz.org/"target="_blank" rel="noopener">XQuartz</a> installed, by the way.</p>
<p>Now you must manually create 3 files in all Phoenix servers. Start with the <code>your-app-name/vm.args</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>-name us@nyc-ip-address
-setcookie @bCd&amp;fG
-kernel inet_dist_listen_min 9100 inet_dist_listen_max 9155
-config /home/pusher/your-app-name.config
-smp auto</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>The <code>/home/pusher/your-app-name</code> is the directory where the release will be uncompressed after edeliver deploys the release tarball.</p>
<p>You must create this file in all Phoenix machines, by the way, changing the <code>-name</code> bit for the same name you declared in the <code>.deliver/config</code> file. The <code>-setcookie</code> should be any name, as long as it&rsquo;s the same in all servers.</p>
<p>See the <code>-config /home/pusher/your-app-name.config</code>? Create that file with the following:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>[{kernel,
  [
    {sync_nodes_optional, [&#39;uk@lon-ip-address&#39;]},
    {sync_nodes_timeout, 30000}
  ]}
].</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>This is an Erlang source-code. On the NYC machine you must declare the London name, and vice-versa. If you have several machines, all of them but the one you&rsquo;re in right now. Get it?</p>
<p>Finally, for the Phoenix app itself, you always have a <code>config/prod.secret.exs</code> that should never be <code>git add</code>ed to the repository, remember? This is where you put the Postgresql server information and random secret key to sign the session cookies:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>use Mix.Config

config :your_app_name, YourAppName.Endpoint,
  secret_key_base: &#34;...&#34;

# Configure your database
config :your_app_name, YourAppName.Repo,
  adapter: Ecto.Adapters.Postgres,
  username: &#34;pusher&#34;,
  password: &#34;your-super-secure-pg-password&#34;,
  database: &#34;your-app-database-name&#34;,
  hostname: &#34;pg-ip-address&#34;,
  pool_size: 20

# if you have Guardian, for example:
config :guardian, Guardian,
  secret_key: &#34;...&#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>How do you create a new random secret key? From your development machine just run: <code>mix phoenix.gen.secret</code> and copy the generated string into the file above.</p>
<p>So now you must have those 3 files in each Phoenix server, in the <code>/home/pusher</code> home folder:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>~/your-app-name/vm.args
~/prod.secret.exs
~/your-app-name.config</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>As per <a href="https://hexdocs.pm/distillery/runtime-configuration.html#vm-args"target="_blank" rel="noopener">distillery</a> documentation, you have to set an environment variable to let it know about the <code>vm.args</code> file, and this is <strong>SUPER</strong> important otherwise it will generate a default one that doesn&rsquo;t set the proper name and cookie, so the nodes will not find each other after booting up.</p>
<p>Using <code>sudo</code>, edit the <code>/etc/environment</code> and add the line:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>RELEASE_CONFIG_DIR=/home/pusher/your-app-name
VMARGS_PATH=/home/pusher/your-app-name/vm.args</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>You have to do it in all the phoenix servers.</p>
<p>In your local development directory you still have to run this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>mix release.init</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>It will generate a standard <code>rel/config.exs</code> file that you must add to your git repository with the following changes to the bottom:</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="o">...</span>
</span></span><span class="line"><span class="cl"><span class="n">environment</span> <span class="ss">:prod</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">plugin</span> <span class="no">Releases</span><span class="o">.</span><span class="n">Plugin</span><span class="o">.</span><span class="n">LinkConfig</span>
</span></span><span class="line"><span class="cl">  <span class="o">...</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">release</span> <span class="ss">:your_app_name</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">set</span> <span class="ss">version</span><span class="p">:</span> <span class="n">current_version</span><span class="p">(</span><span class="ss">:your_app_name</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="n">set</span> <span class="ss">applications</span><span class="p">:</span> <span class="o">[</span>
</span></span><span class="line"><span class="cl">    <span class="ss">your_app_name</span><span class="p">:</span> <span class="ss">:permanent</span>
</span></span><span class="line"><span class="cl">  <span class="o">]</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>The plugin should enable the release to find the local <code>vm.args</code> file in the server (this is super important, otherwise the remote commands such as start, stop, ping, etc will not work and the nodes will not load the correct information to boot up and form a cluster).</p>
<p>Finally, all set, you can issue this command:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>$ mix edeliver build release production --skip-mix-clean --verbose</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>If it finishes without errors you will have a message like the following in the end:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>...
==&gt; Release successfully built!
    You can run it in one of the following ways:
      Interactive: _build/prod/rel/your-app-name/bin/your-app-name console
      Foreground: _build/prod/rel/your-app-name/bin/your-app-name foreground
      Daemon: _build/prod/rel/your-app-name/bin/your-app-name start
--&gt; Copying release 0.0.1 to local release store
--&gt; Copying your-app-name.tar.gz to release store

RELEASE BUILD OF YOUR-APP-NAME WAS SUCCESSFUL!</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Now, this will take an absurdly long time to deploy. That&rsquo;s because it will git clone the source code of your app, fetch all Elixir dependencies (every time!), it will have to compile everything, then it will run the super slow <code>npm install</code> (every time!), brunch your assets, create the so-called &ldquo;release&rdquo;, tar and gzip it, download it and SCP it to the other machines you configured.</p>
<p>In the <code>.deliver/config</code> file you set a <code>BUILD_HOST</code> option. This is the machine where all this process takes place, so you will want to have at least this machine be beefier than the others. As I am using small 512Mb droplets, the process takes forever.</p>
<p>The last command will download the generated tarball. It has to do it to make sure you have the <a href="http://erlang.org/doc/tutorial/nif.html"target="_blank" rel="noopener">NIFs</a> compiled in the native environment where they run, because if you use a Mac, binaries from Macs won&rsquo;t run on Linux.</p>
<p>Now we must upload and decompress this tarball in each server with the following command:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>mix edeliver deploy release to 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>The slower your network, the longer this will take, as it&rsquo;s uploading a big tarfile across the public internet, so make sure you&rsquo;re in a fast connection. And when it finishes, we can restart the servers (if you already had instances running):</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>mix edeliver stop production
mix edeliver start 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>If you do everything right, the edeliver process finishes without any error and it leaves a daemon running in your server, like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>/home/pusher/your-app-name/erts-8.2/bin/beam -- -root /home/pusher/your-app-name -progname home/pusher/your-app-name/releases/0.0.1/your-app-name.sh -- -home /home/pusher -- -boot /home/pusher/your-app-name/releases/0.0.1/your-app-name -config /home/pusher/your-app-name/running-config/sys.config -boot_var ERTS_LIB_DIR /home/pusher/your-app-name/erts-8.2/../lib -pa /home/pusher/your-app-name/lib/your-app-name-0.0.1/consolidated -name us@nyc-ip-address -setcookie ex-push&amp;r-l!te -kernel inet_dist_listen_min 9100 inet_dist_listen_max 9155 -config /home/pusher/your-app-name.config -mode embedded -user Elixir.IEx.CLI -extra --no-halt &#43;iex -- console</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>It will still take a long time, but it should be easier. So this is a pro-tip for you, Linux users. Follow <a href="https://gist.github.com/mattweldon/2e8ecb953216438ad168"target="_blank" rel="noopener">this Gist</a> for more details, you must emulate what&rsquo;s run from the <code>.deliver/config</code> file&rsquo;s bottom half.</p>
<p>Also notice that I ran the migrations manually, but you can do it using <code>mix edeliver migrate</code>.</p>
<p>Read their documentation for more commands and configurations.</p>
<p>Also, do not forget to enable UFW:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>sudo ufw allow ssh
sudo ufw allow 8080
sudo ufw allow 4369
sudo ufw allow proto tcp from any to any port 9100:9155
sudo ufw default allow outgoing
sudo ufw enable</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>
<h3>Debugging Production bugs<span class="hx:absolute hx:-mt-20" id="debugging-production-bugs"></span>
    <a href="#debugging-production-bugs" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Right after I deployed, obviously it failed. And the problem is that the <code>/home/pusher/your-app-name/log/erlang.log</code> files (they are automatically rotated so you may find several files ending in a number), you won&rsquo;t see much.</p>
<p>What I recommend you to do is to change the <code>config/prod.exs</code> file ONLY in your development machine and change the log setting to <code>config :logger, level: :debug</code>, use the same <code>prod.secret.exs</code> you edited in the servers above and run it locally with <code>MIX_ENV=prod iex -S mix phoenix.server</code>.</p>
<p>For example, in development mode I had a code in the controller that was checking the existence of an optional query string parameter like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">if</span> <span class="n">params</span><span class="o">[</span><span class="s2">&#34;some_parameter&#34;</span><span class="o">]</span> <span class="k">do</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>That was working fine in development but crashing in production, so I had to change it to:</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="k">if</span> <span class="no">Map</span><span class="o">.</span><span class="n">has_key?</span><span class="p">(</span><span class="n">params</span><span class="p">,</span> <span class="s2">&#34;some_parameter&#34;</span><span class="p">)</span> <span class="k">do</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>Another thing was that Guardian was working normally in development, but in production I had to declare its application in the <code>mix.exs</code> like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">application</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="o">[</span><span class="ss">mod</span><span class="p">:</span> <span class="p">{</span><span class="no">ExPusherLite</span><span class="p">,</span> <span class="o">[]</span><span class="p">},</span>
</span></span><span class="line"><span class="cl">   <span class="ss">applications</span><span class="p">:</span> <span class="o">[...</span><span class="p">,</span> <span class="ss">:guardian</span><span class="p">,</span> <span class="ss">:edeliver</span><span class="o">]]</span>
</span></span><span class="line"><span class="cl"><span class="k">end</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>I was getting <code>:econnrefused</code> errors because I forgot to run <code>MIX_ENV=prod mix do ecto.create, ecto.migrate</code> as I instructed above. Once I figured those out, my application was up and running through the <code>http://your-app-name.yourdomain.com</code>, HAProxy was correctly forwarding to the 8080 port in the servers and everything runs fine, including the WebSocket connections.</p>
<h3>Conclusion<span class="hx:absolute hx:-mt-20" id="conclusion"></span>
    <a href="#conclusion" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>As I mentioned above, this kind of procedure makes me really miss an easy to deploy solution such as Heroku.</p>
<p>The only problem I am facing right now is that when I log in through Coherence&rsquo;s sign in page, I am not redirected to the correct URI I am trying (&quot;/admin&quot; in my case), sometimes reloading after sign in works, sometimes it doesn&rsquo;t. Sometimes I am inside a &ldquo;/admin&rdquo; page but when I click one of the links it sends me back to the sign in page even though I am already signed in. I am not sure if it&rsquo;s a bug in Coherence, ExAdmin, Phoenix itself or an HAProxy misconfiguration. I will update this post if I find out.</p>
<p>Edeliver also takes an obscene amount of time to deploy. Even waiting for sprockets to process in a <code>git push heroku master</code> deploy feels way faster in comparison. And this is for a very bare-bone Phoenix app. Having to fetch everything (because Hex doesn&rsquo;t keep a local global cache, all dependencies are statically vendored in the project directory). Having to run the super slow npm doesn&rsquo;t help either.</p>
<p>I still need to research if there are faster options, but for now what I have &ldquo;works&rdquo;.</p>
<p>And more importantly, now I have a scalable cluster for real-time bi-directional WebSockets, which is the main reason one might want to use Phoenix in the first place.</p>
<p>If you want to build a &ldquo;normal&rdquo; website, keep it simple and do it in Rails, Django, Express or whatever is your web framework of choice. If you want real-time communications the easy way, I might have a better solution. Keep an eye on my blog for news to come soon! ;-)</p>
]]></content:encoded><category>elixir-and-erlang</category><category>devops</category><category>cloud</category><category>distributed-systems</category></item><item><title>Moving away from Slack into Rocket.chat. Good Enough.</title><link>https://akitaonrails.github.io/en/2016/08/09/moving-away-from-slack-into-rocket-chat-good-enough/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2016/08/09/moving-away-from-slack-into-rocket-chat-good-enough/</guid><pubDate>Tue, 09 Aug 2016 16:21:00 -0300</pubDate><description>&lt;p&gt;In my quest to create my own private infrastructure I&amp;rsquo;ve already moved away from Pivotal Tracker (saving this for a future post), Bitbucket, Github, Travis and into GitLab and GitLab Runner. There are more services I will move away from in the future, so expect more posts like this.&lt;/p&gt;
&lt;p&gt;In &lt;a href="http://www.akitaonrails.com/en/2016/08/03/migrando-para-o-gitlab-sim-vale-a-pena"target="_blank" rel="noopener"&gt;my previous post&lt;/a&gt; I forgot to mention minimal firewall and SSL configuration, but you can check it here.&lt;/p&gt;
&lt;p&gt;One of the most difficult pieces to move away from is Slack. Don&amp;rsquo;t get me wrong, it&amp;rsquo;s a great tool and it gets the job done. The free-tier is really competent but I really want some of the paid features such as history of all conversations with search capabilities and uploads without limit.&lt;/p&gt;</description><content:encoded><![CDATA[<p>In my quest to create my own private infrastructure I&rsquo;ve already moved away from Pivotal Tracker (saving this for a future post), Bitbucket, Github, Travis and into GitLab and GitLab Runner. There are more services I will move away from in the future, so expect more posts like this.</p>
<p>In <a href="http://www.akitaonrails.com/en/2016/08/03/migrando-para-o-gitlab-sim-vale-a-pena"target="_blank" rel="noopener">my previous post</a> I forgot to mention minimal firewall and SSL configuration, but you can check it here.</p>
<p>One of the most difficult pieces to move away from is Slack. Don&rsquo;t get me wrong, it&rsquo;s a great tool and it gets the job done. The free-tier is really competent but I really want some of the paid features such as history of all conversations with search capabilities and uploads without limit.</p>
<p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/555/big_Franz.png" alt="Codeminer Rocket"  loading="lazy" /></p>
<h2>The Cost Conundrum<span class="hx:absolute hx:-mt-20" id="the-cost-conundrum"></span>
    <a href="#the-cost-conundrum" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>But then you have to commit to pay USD 8/month per user (USD 6.67 if you pay ahead of time). If you&rsquo;re a small team (around a dozen people) it&rsquo;s well worth it and you should definitely pay and be done with it.</p>
<p>If you have more than 20 people then it starts to become a conundrum. On my GitLab cost calculations, some people argued that saving USD 15k yearly is not such a big deal, and they&rsquo;re not wrong.</p>
<p>If you&rsquo;re from outside of the US coastal cities, especially from countries in South America or Asia where currency can be more than 3 times weaker, then it quickly starts to become a big deal.</p>
<p>Apart from that, we are currently offloading a whole lot of our IPs and knowledge into the care of companies we don&rsquo;t really know and where we don&rsquo;t hold any real stakes.</p>
<p>If you&rsquo;re a medium to big company it really starts to make sense to want to have more control over your own property. Of course, it costs some work and maintenance, so it&rsquo;s not for everybody.</p>
<p>What I will argue is that if you have <strong>150 people</strong> or more, and you&rsquo;re still growing, it&rsquo;s not such a big burden to jump into your own infrastructure.</p>
<blockquote>
  <p>You avoid paying around <a href="https://codeminer42.slack.com/pricing"target="_blank" rel="noopener"><strong>USD 12,000 a year</strong></a> or more, and instead you will pay <strong>USD 960 a year</strong> in Digital Ocean boxes and MMS plus a few hours every month in basic maintenance. That&rsquo;s 12 times cheaper.</p>

</blockquote>
<p>Again, you shouldn&rsquo;t do it just for the cost savings as the maintenance burden can easily outweigh the savings if you or your team are not devops savvy and you don&rsquo;t have anyone to keep monitoring it. For most companies, the conversation history is not so important and even the free-tier will be more than enough.</p>
<p>Cost is less relevant depending on your context, and there are many different uses for group chats. For example, if you&rsquo;re an <a href="https://news.ycombinator.com/item?id=9754626"target="_blank" rel="noopener">open source community</a>, you would try <a href="https://gitter.im"target="_blank" rel="noopener">Gitter</a> instead.</p>
<p>There are also <a href="https://www.google.com/search?q=slack&#43;alternatives"target="_blank" rel="noopener">many other alternatives</a> that you may want to try.</p>
<p>For the sake of this article, let&rsquo;s assume you&rsquo;re a small to medium company, in need of private rooms, many external users (for example, your clients), with at least 50 people. Then this can start to make sense. Any other scenario and we have to analyze it differently.</p>
<h2>Why I chose Rocket.chat<span class="hx:absolute hx:-mt-20" id="why-i-chose-rocketchat"></span>
    <a href="#why-i-chose-rocketchat" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>With the cost reasoning out of the way, then it&rsquo;s a matter of choosing which alternative to use.</p>
<p>I tried <a href="https://www.mattermost.org/"target="_blank" rel="noopener">MatterMost</a> first and I really wanted to like it. It has a very similar feature set to Slack and most importantly, it&rsquo;s written in Go. So it&rsquo;s both lightweight, very fast, and not so difficult to contribute. It uses MySQL or PostgreSQL which I like.</p>
<p>Unfortunately, it has an important <strong>show-stopper</strong> for my needs. You can create both public channels or private groups, but anyone can delete a private group. If you want permission control for that you must use their <a href="https://about.mattermost.com/pricing/"target="_blank" rel="noopener">&ldquo;Enterprise&rdquo; offering</a> and pay <strong>USD 20/year per user</strong>.</p>
<blockquote>
  <p>It&rsquo;s still at least <strong>3 times cheaper</strong> than Slack, so it may be a good option for you.</p>

</blockquote>
<p>I really want to have full-control, not just make it cheaper. I could pay Slack already if I didn&rsquo;t care, so I would not just pay an alternative and not have full control (including the ability to tweak and improve the entire code base).</p>
<p>Then, it took me back to <a href="https://rocket.chat/"target="_blank" rel="noopener">Rocket.chat</a>. Its feature set also rivals Slack, it&rsquo;s good looking enough. But, it&rsquo;s made in Meteor. Now, I don&rsquo;t have anything against Meteor and I really think a Slack-clone is exactly the kind of use case where you could use Meteor to its full potential.</p>
<p>Technically I really think it&rsquo;s a downside to be forced to use MongoDB (Meteor requires Mongo). For small installations I really prefer to have PostgreSQL. MongoDB is competent, but for medium to big installations there are caveats, and I will show you why below.</p>
<p>I <strong>strongly dislike</strong> the <a href="https://medium.com/friendship-dot-js/i-peeked-into-my-node-modules-directory-and-you-wont-believe-what-happened-next-b89f63d21558#.8jd3z3n6u"target="_blank" rel="noopener">culture of writing software without <em>minimal care</em></a> such as having a reasonably complete test suite. You browse through the many packages that comprise Rocket.chat and several of them have no tests whatsoever. Some packages do have jasmine tests, but the majority is lacking.</p>
<p>This is <a href="https://github.com/RocketChat/Rocket.Chat/blob/a5cb22bb0017f4c39654bf1e2895ae64acb0339b/packages/rocketchat-katex/tests/jasmine/client/unit/katex.spec.coffee"target="_blank" rel="noopener">one example</a> I picked randomly, out of the few test files I found:</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="nx">describe</span> <span class="s1">&#39;rocketchat:katex Client&#39;</span><span class="p">,</span> <span class="o">-&gt;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nx">it</span> <span class="s1">&#39;should exist&#39;</span><span class="p">,</span> <span class="o">-&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="nx">expect</span><span class="p">(</span><span class="nx">RocketChat</span><span class="p">.</span><span class="nx">katex</span><span class="p">).</span><span class="nx">toBeDefined</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>This is sad, sorry to say that, but it is. Not surprisingly I&rsquo;ve seen katex issues while using the interface. And if you think this was just a poor choice, I picked <a href="https://github.com/RocketChat/Rocket.Chat/blob/a5cb22bb0017f4c39654bf1e2895ae64acb0339b/packages/rocketchat-markdown/tests/jasmine/client/unit/markdown.spec.coffee"target="_blank" rel="noopener">another test file</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-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="nx">describe</span> <span class="s1">&#39;rocketchat:markdown Client&#39;</span><span class="p">,</span> <span class="o">-&gt;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nx">it</span> <span class="s1">&#39;should exist&#39;</span><span class="p">,</span> <span class="o">-&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="nx">expect</span><span class="p">(</span><span class="nx">RocketChat</span><span class="p">.</span><span class="nx">Markdown</span><span class="p">).</span><span class="nx">toBeDefined</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>Really?</p>
<p>So, the code itself is not so pretty, you have to keep this in mind. I&rsquo;d much rather use Mattermost (which is much better structured and with enough tests), but it&rsquo;s not entirely free, so it&rsquo;s not an option for me. For my scenario, I&rsquo;d rather have sloppy code (that minimally &ldquo;works&rdquo;) that I can tweak than code I can&rsquo;t see, for this particular venture. Many inexperienced developers may argue about the necessity of having automated tests, but not having them also impacts this:</p>
<p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/552/big_Contributors_to_RocketChat_Rocket_Chat.png" alt="Rocket.chat contribution graph"  loading="lazy" /></p>
<p>If we see how the project is evolving over time, we can see that interest is diminishing. The lack of good automated tests also makes it more difficult for new people to contribute without adding regressions, and makes contribution reviews more difficult and time consuming.</p>
<p>On the other hand the feature set is good enough as it is. There is only so much you can add to a Slack-clone. The upside is that if the code doesn&rsquo;t change too much, I can tweak it more without having many things breaking all the time. It&rsquo;s the complete opposite from the impressive contribution boost over at GitLab, for example.</p>
<p>Doing a few days of testing through the administration settings, user interface, having dozens of users doing real conversation, convinced me that even though the code quality is bad, the feature set is indeed there. I think I can live with that. Your mileage may vary though.</p>
<p>I am using <a href="http://meetfranz.com"target="_blank" rel="noopener">Franz</a> to open Slack, Rocket.chat, Hangouts, Messenger and any number of other communicators in one single app, which is great. And Rocket.chat does provide <a href="https://itunes.apple.com/us/app/rocket.chat/id1028869439?mt=8"target="_blank" rel="noopener">mobile</a> <a href="https://play.google.com/store/apps/details?id=com.konecty.rocket.chat&amp;hl=en"target="_blank" rel="noopener">apps</a> based off PhoneGap. Again, it&rsquo;s good, not great.</p>
<p>Many people have heavy bots or integrations into their Slack configuration. The alternatives such as Rocket.chat do support incoming and outgoing webhooks and most of the integrations should be possible (with some tweaking and hacking such as this <a href="https://github.com/FinndropStudios/GifRocket"target="_blank" rel="noopener">Giphy example</a>), so make a list of all important integrations and do some Google research on that before continuing.</p>
<h2>Planning the Infrastructure<span class="hx:absolute hx:-mt-20" id="planning-the-infrastructure"></span>
    <a href="#planning-the-infrastructure" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>All that having been said, I&rsquo;d recommend you try Rocket.chat&rsquo;s <a href="https://demo.rocket.chat"target="_blank" rel="noopener">demo server</a> first to see if it has the minimal features you need. And also that you install a dry-run in a small Digital Ocean box with the intention of blowing it away later.</p>
<p>For a single-instance, stand-alone installation, temporary dry-run, I recommend you start by installing the <a href="https://www.digitalocean.com/community/tutorials/how-to-use-the-mongodb-one-click-application"target="_blank" rel="noopener">MongoDB One-Click Application</a> and follow <a href="https://www.digitalocean.com/community/tutorials/how-to-install-configure-and-deploy-rocket-chat-on-ubuntu-14-04"target="_blank" rel="noopener">this instruction</a> to install everything in one machine.</p>
<p>This is the worst possible production environment, which is why I am being so repetitive in saying that you should destroy this box after you do your testing.</p>
<p>As I&rsquo;ve said before, I am assuming a scenario where you have 50 or more people, with real clients, important communication going on. You don&rsquo;t want to keep a simple installation like this for production usage.</p>
<p>For a <strong>minimal production installation</strong> you will need at the very least 4 boxes with 1 GB of RAM each. Now, why 4?</p>
<p>This is an important section for most of the beginner Javascript programmers out there playing with the so called &ldquo;MEAN&rdquo; stack (MongoDB, Express, Angularjs, Node.js). I&rsquo;ve seen people deploy single-instance, stand-alone &ldquo;MEAN&rdquo; apps for production usage. And this is super bad.</p>
<blockquote>
  <p><strong>MongoDB is NOT MEANT to function with just one single instance!</strong></p>

</blockquote>
<p>More than that:</p>
<blockquote>
  <p><strong>DO NOT start an even number of MongoDB nodes, or at least install an Arbiter</strong></p>

</blockquote>
<p>So, the minimal setup for MongoDB is a <a href="https://docs.mongodb.com/manual/core/replica-set-architecture-three-members/"target="_blank" rel="noopener">Three-member Replica Set</a>, no less!</p>
<p>The article <a href="https://www.rainforestqa.com/blog/2012-11-05-mongodb-gotchas-and-how-to-avoid-them/"target="_blank" rel="noopener">&ldquo;MongoDB Gotchas &amp; How To Avoid Them&rdquo;</a> is a little bit old but you should be aware of several of the documented gotchas, including adding replica sets and avoiding waiting too long to start sharding your data.</p>
<p>The most important part of making Rocket.chat reliable is to make your MongoDB minimally reliable, and this is a hidden cost you must consider.</p>
<h4>Installing a Three-Member Replica Set<span class="hx:absolute hx:-mt-20" id="installing-a-three-member-replica-set"></span>
    <a href="#installing-a-three-member-replica-set" class="subheading-anchor" aria-label="Permalink for this section"></a></h4><p>For the sake of this article I will assume that you created 3 (three) MongoDB One-Click Applications on Digital Ocean&rsquo;s, at the very least with 1 GB RAM boxes (2 GB RAM machines are a good choice if you&rsquo;re in doubt, but never the 512 Mb) and another bare Ubuntu 14.04 box to be the web server. Create all machines in the same sub-region, all of them with private networking and backup enabled.</p>
<p>Take note of the public IPs and private IPs, for the sake of this article let&rsquo;s say you have this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>Public IP     Private IP    Hostname

192.160.10.1  10.0.0.1      mongo1.yourdomain.com
192.160.10.2  10.0.0.2      mongo2.yourdomain.com
192.160.10.3  10.0.0.3      mongo3.yourdomain.com
192.160.10.4  10.0.0.4      yourdomain.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>In the commands and code snippets below, make sure to replace the fake IPs with your own private IPs.</p>
<p>As I said in <a href="http://www.akitaonrails.com/en/2016/08/03/migrando-para-o-gitlab-sim-vale-a-pena?"target="_blank" rel="noopener">my GitLab post</a>, create a swap file, configure the locale, set it up for automatic security updates, and upgrade the Ubuntu packages.</p>
<p>The next first thing: configure the firewall in all machines (assuming <code>ufw</code> package is already installed):</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>sudo ufw reset
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow https
sudo ufw allow ssh
sudo ufw allow in on eth1 to any port 27017
sudo ufw enable</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>We are allowing MongoDB&rsquo;s 27017 port only for machines in the same private networking (through eth1, the public IP goes through eth0). We&rsquo;re also allowing port 22 for SSH and although I am not listing it here in this article you should also consider installing and configuring <a href="https://www.digitalocean.com/community/tutorials/how-to-protect-ssh-with-fail2ban-on-ubuntu-14-04"target="_blank" rel="noopener">fail2ban</a>. Digital Ocean also has a <a href="https://www.digitalocean.com/community/tutorials/how-to-set-up-a-firewall-with-ufw-on-ubuntu-14-04"target="_blank" rel="noopener">good post on UFW</a> for more details.</p>
<p>Next thing to do on the MongoDB machines is to <a href="https://docs.mongodb.com/manual/tutorial/transparent-huge-pages/"target="_blank" rel="noopener">Disable Transparent Huge Pages (THP)</a>. For that create a file <code>/etc/init.d/disable-transparent-hugepages</code>, with this content:</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="cp">#!/bin/sh
</span></span></span><span class="line"><span class="cl"><span class="c1">### BEGIN INIT INFO</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Provides:          disable-transparent-hugepages</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Required-Start:    $local_fs</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Required-Stop:</span>
</span></span><span class="line"><span class="cl"><span class="c1"># X-Start-Before:    mongod mongodb-mms-automation-agent</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Default-Start:     2 3 4 5</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Default-Stop:      0 1 6</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Short-Description: Disable Linux transparent huge pages</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Description:       Disable Linux transparent huge pages, to improve</span>
</span></span><span class="line"><span class="cl"><span class="c1">#                    database performance.</span>
</span></span><span class="line"><span class="cl"><span class="c1">### END INIT INFO</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">case</span> <span class="nv">$1</span> in
</span></span><span class="line"><span class="cl">  start<span class="o">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="o">[</span> -d /sys/kernel/mm/transparent_hugepage <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">      <span class="nv">thp_path</span><span class="o">=</span>/sys/kernel/mm/transparent_hugepage
</span></span><span class="line"><span class="cl">    <span class="k">elif</span> <span class="o">[</span> -d /sys/kernel/mm/redhat_transparent_hugepage <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">      <span class="nv">thp_path</span><span class="o">=</span>/sys/kernel/mm/redhat_transparent_hugepage
</span></span><span class="line"><span class="cl">    <span class="k">else</span>
</span></span><span class="line"><span class="cl">      <span class="k">return</span> <span class="m">0</span>
</span></span><span class="line"><span class="cl">    <span class="k">fi</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span> <span class="s1">&#39;never&#39;</span> &gt; <span class="si">${</span><span class="nv">thp_path</span><span class="si">}</span>/enabled
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span> <span class="s1">&#39;never&#39;</span> &gt; <span class="si">${</span><span class="nv">thp_path</span><span class="si">}</span>/defrag
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nb">unset</span> thp_path
</span></span><span class="line"><span class="cl">    <span class="p">;;</span>
</span></span><span class="line"><span class="cl"><span class="k">esac</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Now you can register it as a service:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>sudo chmod 755 /etc/init.d/disable-transparent-hugepages
sudo update-rc.d disable-transparent-hugepages defaults
sudo /etc/init.d/disable-transparent-hugepages 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>Stop MongoDB in all 3 machines:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>sudo service mongod stop</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Now comes a complicated procedure that you should not do wrong. It&rsquo;s only complicated because you have to do it in all 4 machines minding their correct private IP addresses. If you already have your own DNS server, you can skip this, otherwise follow through carefully.</p>
<p>You will have to edit both <code>/etc/hostname</code> and <code>/etc/hosts</code>. Each machine will have a particular name and they should be able to find the other machines by name. Let&rsquo;s start by editing the <code>/etc/hostname</code> on each machine. Use the table in the beginning of this section, for example, in the machine with public IP <code>192.160.10.1</code> you must edit its hostname to be <code>mongo1.yourdomain.com</code>, and so on for all the machines.</p>
<p>Once you have it done, you have to replace the line <code>127.0.0.1 localhost</code> in the <code>/etc/hosts</code> in each of them for the following:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>127.0.0.1 localhost mongo1.yourdomain.com
10.0.0.1 mongo1.yourdomain.com
10.0.0.2 mongo2.yourdomain.com
10.0.0.3 mongo3.yourdomain.com
10.0.0.4 yourdomain.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>This snippet assumes you&rsquo;re in the machine with private IP <code>10.0.0.1</code>, of course. Make sure you change <code>mongo1.yourdomain.com</code> in the first line for the hostname of the machine you&rsquo;re in. If this is too difficult for you, you definitely should not try to maintain your own infrastructure, so beware.</p>
<p>Finally, let&rsquo;s edit the <code>/etc/mongod.conf</code> with these changes:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>storage:
  dbPath: /mongodb-data
...
net:
  port: 27017
  bindIp: 0.0.0.0
...
replication:
  replSetName: rs0</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>This should be exactly the same in all machines. And you must create this <code>/mongodb-data</code> directory like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>sudo mkdir /mongodb-data
sudo chown -R mongodb:mongodb /mongodb-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>Now go ahead on machine <code>mongo1.yourdomain.com</code>, start up with <code>sudo service mongod start</code> and fire up the <code>mongo</code> command, and you should see something like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code># mongo
MongoDB shell version: 3.2.8
connecting to: test
&gt;</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Let&rsquo;s now configure the replica set by issuing this command in the MongoDB command-line interface:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>rs.initiate()
rs.add(&#39;mongo2.yourdomain.com&#39;)
rs.add(&#39;mongo3.yourdomain.com&#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>And you can now log in to the secondary machines and <code>sudo service mongod start</code> in all of them. They should all start joining the replica set and syncing.</p>
<p>You can check the status of the replication like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>&gt; rs.conf()

{
  &#34;set&#34; : &#34;rs0&#34;,
  &#34;date&#34; : ISODate(&#34;2016-08-09T17:33:10.466Z&#34;),
  &#34;myState&#34; : 1,
  &#34;term&#34; : NumberLong(-1),
  &#34;heartbeatIntervalMillis&#34; : NumberLong(2000),
  &#34;members&#34; : [
    {
      &#34;_id&#34; : 1,
      &#34;name&#34; : &#34;mongo1.yourdomain.com:27017&#34;,
      &#34;health&#34; : 1,
      &#34;state&#34; : 1,
      &#34;stateStr&#34; : &#34;PRIMARY&#34;,
      &#34;uptime&#34; : 11727,
      &#34;optime&#34; : Timestamp(1470763990, 3),
      &#34;optimeDate&#34; : ISODate(&#34;2016-08-09T17:33:10Z&#34;),
      &#34;electionTime&#34; : Timestamp(1470752553, 1),
      &#34;electionDate&#34; : ISODate(&#34;2016-08-09T14:22:33Z&#34;),
      &#34;configVersion&#34; : 6,
      &#34;self&#34; : true
    },
    {
      &#34;_id&#34; : 2,
      &#34;name&#34; : &#34;mongo2.yourdomain.com:27017&#34;,
      &#34;health&#34; : 1,
      &#34;state&#34; : 2,
      &#34;stateStr&#34; : &#34;SECONDARY&#34;,
      &#34;uptime&#34; : 11693,
      &#34;optime&#34; : Timestamp(1470763988, 4),
      &#34;optimeDate&#34; : ISODate(&#34;2016-08-09T17:33:08Z&#34;),
      &#34;lastHeartbeat&#34; : ISODate(&#34;2016-08-09T17:33:10.026Z&#34;),
      &#34;lastHeartbeatRecv&#34; : ISODate(&#34;2016-08-09T17:33:08.887Z&#34;),
      &#34;pingMs&#34; : NumberLong(0),
      &#34;syncingTo&#34; : &#34;mongo3.yourdomain.com:27017&#34;,
      &#34;configVersion&#34; : 6
    },
    {
      &#34;_id&#34; : 3,
      &#34;name&#34; : &#34;mongo3.yourdomain.com:27017&#34;,
      &#34;health&#34; : 1,
      &#34;state&#34; : 2,
      &#34;stateStr&#34; : &#34;SECONDARY&#34;,
      &#34;uptime&#34; : 11693,
      &#34;optime&#34; : Timestamp(1470763988, 4),
      &#34;optimeDate&#34; : ISODate(&#34;2016-08-09T17:33:08Z&#34;),
      &#34;lastHeartbeat&#34; : ISODate(&#34;2016-08-09T17:33:09.994Z&#34;),
      &#34;lastHeartbeatRecv&#34; : ISODate(&#34;2016-08-09T17:33:08.655Z&#34;),
      &#34;pingMs&#34; : NumberLong(0),
      &#34;syncingTo&#34; : &#34;mongo1.yourdomain.com:27017&#34;,
      &#34;configVersion&#34; : 6
    }
  ],
  &#34;ok&#34; : 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>Each box should be syncing data to another box, one of them will be marked &ldquo;PRIMARY&rdquo; and the others will be &ldquo;SECONDARY&rdquo;. If one of them &ldquo;dies&rdquo; for some reason, one of the others will be elected as PRIMARY until you add a replacement box. Read more details on this procedure in <a href="https://www.digitalocean.com/community/tutorials/how-to-implement-replication-sets-in-mongodb-on-an-ubuntu-vps"target="_blank" rel="noopener">Digital Ocean&rsquo;s post about implementing replication sets</a>.</p>
<h4>Adding your MongoDB environment to MMS<span class="hx:absolute hx:-mt-20" id="adding-your-mongodb-environment-to-mms"></span>
    <a href="#adding-your-mongodb-environment-to-mms" class="subheading-anchor" aria-label="Permalink for this section"></a></h4><p>Another thing beginners will not realize is that it&rsquo;s a good idea to monitor your services. At the very least you should install MongoDB Cloud Manager agents into your servers. Just set up an account at <a href="https://www.mongodb.com/cloud"target="_blank" rel="noopener">MongoDB Atlas</a>.</p>
<p>It&rsquo;s beyond the point of this article to explain Atlas, but you should have no trouble installing the agents with the Group ID and API Key provided. Then install the monitoring agent and have the Manager figure out your existing deployment and replica set.</p>
<p>The basic subscription for monitoring will cost you an extra USD 39 a month, but believe me when I say it&rsquo;s worth it to add all your MongoDB deployments under MMS.</p>
<p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/553/big_Deployment___Cloud_Manager__MongoDB_Cloud_Manager.png" alt="MMS"  loading="lazy" /></p>
<p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/554/big_Host___Cloud_Manager__MongoDB_Cloud_Manager.png" alt="Replica Set Graphs"  loading="lazy" /></p>
<h4>Installing a multi-instance Node.js service<span class="hx:absolute hx:-mt-20" id="installing-a-multi-instance-nodejs-service"></span>
    <a href="#installing-a-multi-instance-nodejs-service" class="subheading-anchor" aria-label="Permalink for this section"></a></h4><p>Another mistake beginners in the MEAN stack make is to start up a single Node.js instance. Yes, Node.js&rsquo;s asynchronous I/O nature makes it &ldquo;concurrent enough&rdquo; in 1 single thread. But you do want to maximize the rented machine so you should spin up at least one Node.js instance for each CPU core. A 1 GB RAM machine has just 1 core, but you can spin up at least 2 instances.</p>
<p>First of all, we must set up the <code>yourdomain.com</code> (<code>192.160.10.4</code> or <code>10.0.0.4</code> in the example) to support Node.js, let&rsquo;s do it:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>sudo apt-get install -y npm curl graphicsmagick nginx git bc
sudo npm install -g n
sudo npm install -g forever
sudo npm install -g forever-service
sudo n 0.10.40</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>If you haven&rsquo;t already, <a href="https://www.digitalocean.com/community/tutorials/how-to-create-a-sudo-user-on-ubuntu-quickstart"target="_blank" rel="noopener">create a more restricted sudo user</a>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>adduser rocket # add a password when asked
usermod -aG sudo rocket
su - rocket # and always ssh in with `ssh rocket@yourdomain.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>Within rocket&rsquo;s home directory let&rsquo;s install the Rocket.chat codebase:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>cd /home/rocket
curl -L https://rocket.chat/releases/latest/download -o rocket.chat.tgz
tar zxvf rocket.chat.tgz
mv bundle Rocket.Chat
cd Rocket.Chat/programs/server
npm install</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>And now let&rsquo;s add a way for the machine to start the Rocket.chat whenever it reboots:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>cd ~/Rocket.Chat
sudo forever-service install -s main.js -e &#34;ROOT_URL=https://rocketchat42.com/ MONGO_URL=mongodb://mongo1.yourdomain.com:27017/rocketchat MONGO_OPLOG_URL=mongodb://mongo1.yourdomain.com:27017/local PORT=3001&#34; rocketchat1
sudo forever-service install -s main.js -e &#34;ROOT_URL=https://rocketchat42.com/ MONGO_URL=mongodb://mongo1.yourdomain.com:27017/rocketchat MONGO_OPLOG_URL=mongodb://mongo1.yourdomain.com:27017/local PORT=3002&#34; rocketchat2

sudo start rocketchat1
sudo start rocketchat2</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Let&rsquo;s also create SSL certificates (and for that you must have a properly registered domain, of course). Again, <a href="https://www.digitalocean.com/community/tutorials/how-to-secure-nginx-with-let-s-encrypt-on-ubuntu-14-04"target="_blank" rel="noopener">Digital Ocean&rsquo;s documentation on Let&rsquo;s Encrypt</a> is very good. In summary, all you have to do is:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>sudo git clone https://github.com/letsencrypt/letsencrypt /opt/letsencrypt</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Now edit the <code>/etc/nginx/sites-available/default</code> and add this inside the server block:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

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

<div><pre><code>sudo service nginx reload

sudo openssl dhparam -out /etc/ssl/certs/dhparam.pem 2048

cd /opt/letsencrypt
./letsencrypt-auto certonly -a webroot --webroot-path=/usr/share/nginx/html -d yourdomain.com -d www.yourdomain.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>Again, replace <code>yourdomain.com</code> with your own registered domain, of course. The <code>letsencrypt</code> command will prompt you to enter your e-mail address and accept terms of service.</p>
<p><a href="https://letsencrypt.org/"target="_blank" rel="noopener">Let&rsquo;s Encrypt</a> is a fantastic free SSL provider. Their certificates are short lived and are meant to expire in 90 days, so you must set up auto-renewal. Start <code>sudo crontab -e</code> and add the following lines:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>30 2 * * 1 /opt/letsencrypt/letsencrypt-auto renew &gt;&gt; /var/log/le-renew.log
35 2 * * 1 /etc/init.d/nginx reload</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>This should take care of SSL. Now you can edit your <code>/etc/nginx/sites-available/default</code> and replace it with this template:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code># Upstreams
upstream backend {
    server 127.0.0.1:3001;
    server 127.0.0.1:3002;
}

server {
  listen 443 ssl;

  server_name yourdomain.com;

  ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;

  ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
  ssl_prefer_server_ciphers on;
  ssl_dhparam /etc/ssl/certs/dhparam.pem;
  ssl_ciphers &#39;ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH&#43;AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA&#39;;
  ssl_session_timeout 1d;
  ssl_session_cache shared:SSL:50m;
  ssl_stapling on;
  ssl_stapling_verify on;
  add_header Strict-Transport-Security max-age=15768000;

  location / {
    proxy_pass http://backend;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection &#34;upgrade&#34;;
    proxy_set_header Host $http_host;

    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forward-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forward-Proto http;
    proxy_set_header X-Nginx-Proxy true;

    proxy_redirect off;
  }

}

server {
  listen 80;
  server_name yourdomain.com www.yourdomain.com;
  return 301 https://$host$request_uri;
}</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>Just for the sake of completeness, again: replace <code>yourdomain.com</code> with your domain. Usually the <code>/etc/nginx/sites-enabled/default</code> is a symlink to <code>/etc/nginx/sites-available/default</code>, just check that as well. This configuration will load balance between the 2 node.js instances we configured and started before. And you can add as many as you want following the same <code>forever-install</code> procedure and adding the new instances to the <code>upstream</code> section in the nginx configuration above.</p>
<p>If everything is correct, you can restart nginx again: <code>sudo service nginx restart</code>.</p>
<p>And now you should have <code>https://yourdomain.com</code> already up and running. The first user that signs up will be the site administrator and from there you can customize Rocket.chat internally.</p>
<h2>Conclusion<span class="hx:absolute hx:-mt-20" id="conclusion"></span>
    <a href="#conclusion" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>As you can see, this is not a simple procedure to follow and I am assuming you have experience managing your own infrastructure. If you don&rsquo;t, you definitely should NOT do this by yourself.</p>
<p>If you do it correctly, you should have a functional Slack-clone with minimal reliability (thanks in large part to Digital Ocean) and minimal cost (USD 80/month in the configuration I described that should be enough for more than 100 users). Rocket.chat also offers more <a href="https://rocket.chat/docs/installation/"target="_blank" rel="noopener">documentation</a> for other environments including Docker configurations you might want to try. But the procedure above is sufficient for my needs.</p>
<p>I said it already but it&rsquo;s better to repeat it: don&rsquo;t fall for the trap of installing everything in a single box, with single instance MongoDB, and without proper monitoring. It&rsquo;s asking for trouble.</p>
<p>For now I am in the middle of the rollout. All developers in my company are already in the new deployment and soon half of the clients should also migrate (some will not be able to leave Slack just yet), but with Franz and the ability of full history and searchability this shouldn&rsquo;t be a concern even if a developer stays offline for some period of time.</p>
<p>I also don&rsquo;t advocate that everybody should be online and responding in real-time. It&rsquo;s infeasible, unproductive, creates unnecessary tension. People should participate when they have free time, and they should be able to concentrate without worrying that they are missing something. That&rsquo;s why they should opt-out of being notified in the more busy channels and just enable notification on the private groups that matter.</p>
<p>Coincidentally Jason Fried just posted about concerns over group chats at <a href="https://m.signalvnoise.com/is-group-chat-making-you-sweat-744659addf7d#.3qxe09una"target="_blank" rel="noopener">Signal v. Noise</a>, but the gist is that group chat should be purposeful and not yet another tool to create tension. People should definitely get offline when they need to fully concentrate on their work and have the opportunity to catch up with interesting conversations later. And really important communication should go through e-mail or other traditional ways, a simple <code>@all</code> doesn&rsquo;t cut it for company-wide announcements for example.</p>
<p>I hope this exercise gives you more perspective on what you can have and also raises awareness on the need for companies to regain more control over their own data, particularly knowledge. Erase your communication channels and you&rsquo;re losing years&rsquo; worth of knowledge that can be invaluable.</p>
]]></content:encoded><category>reviews</category><category>devops</category><category>databases</category></item><item><title>Moving to GitLab! Yes, it's worth it!</title><link>https://akitaonrails.github.io/en/2016/08/03/moving-to-gitlab-yes-it-s-worth-it/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2016/08/03/moving-to-gitlab-yes-it-s-worth-it/</guid><pubDate>Wed, 03 Aug 2016 14:49:00 -0300</pubDate><description>&lt;p&gt;I &lt;a href="https://akitaonrails.github.io/2007/09/22/jogar-pedra-em-gato-morto-por-que-subversion-no-presta/"&gt;started evangelizing Git in 2007&lt;/a&gt;. It was a very tough sell to make at the time.&lt;/p&gt;
&lt;p&gt;Outside of the kernel development almost no one wanted to learn it and we had very worthy competitors, from Subversion, to Mercurial, to Bazaar, to Darcs, to Perforce, and so on. But those of us who dug deeper knew that Git had the edge and it was a matter of time.&lt;/p&gt;
&lt;p&gt;Then GitHub showed up in 2008 and the rest is history. For many years it was just &amp;ldquo;cool&amp;rdquo; to be in GitHub. The Ruby community drove GitHub up into the sky. Finally it became the status quo and the one real monopoly in information repositories - not just software source code, but everything.&lt;/p&gt;</description><content:encoded><![CDATA[<p>I <a href="/2007/09/22/jogar-pedra-em-gato-morto-por-que-subversion-no-presta/">started evangelizing Git in 2007</a>. It was a very tough sell to make at the time.</p>
<p>Outside of the kernel development almost no one wanted to learn it and we had very worthy competitors, from Subversion, to Mercurial, to Bazaar, to Darcs, to Perforce, and so on. But those of us who dug deeper knew that Git had the edge and it was a matter of time.</p>
<p>Then GitHub showed up in 2008 and the rest is history. For many years it was just &ldquo;cool&rdquo; to be in GitHub. The Ruby community drove GitHub up into the sky. Finally it became the status quo and the one real monopoly in information repositories - not just software source code, but everything.</p>
<p>I always knew that we should have a &ldquo;local&rdquo; option, which is why <a href="https://gitorious.org/gitorious/oboxodo-gitorious?p=gitorious:oboxodo-gitorious.git;a=search;h=9f6bdf5887c65a440bc3fdc43a14652f42ddf103;s=Fabio&#43;Akita;st=committer"target="_blank" rel="noopener">I tried to contribute to Gitorious</a> way back in 2009. Other options arose, but eventually GitLab appeared around 2011 and picked up steam in the last couple of years.</p>
<p>GitHub itself raised <a href="https://www.crunchbase.com/organization/github#/entity"target="_blank" rel="noopener">USD 350 million in funding</a> and one of its required goals is to nail the Enterprise Edition for big corporations that don&rsquo;t want their data outside their closed gardens. Although GitHub hosts every single open source project out there, they are themselves closed-source.</p>
<p><a href="http://gitlab.com"target="_blank" rel="noopener">GitLab Inc.</a> started differently with an open source-first approach with their Community Edition (CE) and having both a GitHub-like hosted option as well as a supported Enterprise Edition for risk-averse corporations. They already raised <a href="https://www.crunchbase.com/organization/gitlab-com#/entity"target="_blank" rel="noopener">USD 5.62 million in funding</a>, and they are the most promising alternative to GitHub so far.</p>
<p>Of course, there are other platforms such as Atlassian&rsquo;s Bitbucket. But I believe Atlassian&rsquo;s strategy is slower and they have a larger suite of enterprise products to sell first, such as Confluence and Jira. I don&rsquo;t think they ever posed much of a competition against GitHub.</p>
<p>GitLab really started accelerating in 2015 as this <a href="https://github.com/gitlabhq/gitlabhq/graphs/contributors?from=2015-03-14&amp;to=2016-08-02&amp;type=c"target="_blank" rel="noopener">commit graph</a> shows:</p>
<p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/545/big_Contributors_to_gitlabhq_gitlabhq.png" alt="contributions"  loading="lazy" /></p>
<p>It&rsquo;s been steadily growing since 2011, but they seem to have crossed the first tipping point around late 2014, from early adopters to the early majority. This became more important as GitHub <a href="https://github.com/blog/2164-introducing-unlimited-private-repositories"target="_blank" rel="noopener">announced their pricing changes</a> in May.</p>
<p>They said they haven&rsquo;t committed to a deadline to enforce the change, so organizations can opt out of the new format for the time being. They are changing from &ldquo;limited repositories and unlimited users&rdquo; to &ldquo;unlimited repositories and limited users&rdquo;.</p>
<h2>The Cost-Benefit Conundrum<span class="hx:absolute hx:-mt-20" id="the-cost-benefit-conundrum"></span>
    <a href="#the-cost-benefit-conundrum" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>For example, if you have up to 8 developers on the USD 50/month plan (20 private repositories), the change won&rsquo;t affect you, as you will pay USD 25/month for 5 users and USD 9 for additional users (total of USD 52/month).</p>
<p>Now, if you have a big team of 100 developers currently in the Diamond Plan of USD 450/month (300 private repositories), you would have to pay USD 25/month + 95 times USD 9, which totals a staggering USD 880/month! <strong>Double the amount!</strong></p>
<p>This is an extra <strong>USD 10,560 per year</strong>!</p>
<p>And what does GitLab afford you instead?</p>
<p>You can have way more users and more repositories in a <strong>USD 40/month</strong> virtual box (4GB of RAM, 60GB SSD, 4TB transfer).</p>
<p>And it doesn&rsquo;t stop there. GitLab also has very functional <a href="https://gitlab.com/gitlab-org/gitlab-ci-multi-runner"target="_blank" rel="noopener">GitLab Multi Runner</a> which you can install in a separate box (actually, at least 3 boxes - more on that below).</p>
<p>You can easily connect this runner to the build system over GitLab so every new git push triggers the runner to run the automated test suite in a Docker image of your choosing. So it&rsquo;s a fully functional, full featured Continuous Integration system nicely integrated in your GitLab project interface:</p>
<p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/546/big_Pipelines___cm42-archived___PremiosOnline___GitLab.png" alt="Pipeline"  loading="lazy" /></p>
<p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/547/big_test___144____Builds___cm42-archived___PremiosOnline___GitLab.png" alt="CI Runner"  loading="lazy" /></p>
<p>Remind you of anything? Yep, it&rsquo;s a fully functional alternative to Travis-CI, Semaphore, CircleCI or any other CI you&rsquo;re using with a very easy to install procedure. Let&rsquo;s say you&rsquo;re paying Travis-CI USD 489/month to have 10 concurrent jobs.</p>
<p>You can install GitLab Runner in 3 boxes of USD 10/month (1GB RAM, 1 Cores, 30GB SSD) and have way more concurrent jobs (20? 50? Auto-Scale!?) that run <strong>faster</strong> (in a simple test, one build that took 15 minutes over Travis took less than 8 minutes at Digital Ocean).</p>
<p>So let&rsquo;s do the math for a year&rsquo;s worth of service. First considering no GitHub plan change:</p>
<blockquote>
  <p>USD 5,400 (GitHub) + USD 5,868 (Travis) = USD 11,268 a year.</p>

</blockquote>
<p>Now, the GitLab + GitLab Runner + Digital Ocean for the same features and unlimited users, unlimited repositories, unlimited concurrent builds:</p>
<blockquote>
  <p>USD 480 (GitLab) + USD 840 (Runner box) = USD 1,320 a year.</p>

</blockquote>
<p>This is already almost 8.5x cheaper with almost no change in quality.</p>
<p>For the worst case scenario, compare it when GitHub decides to enforce the new plans:</p>
<blockquote>
  <p>USD 10,560 (GitHub new plans) + USD 5,868 (Travis) = USD 16,428</p>

</blockquote>
<p>Now the GitLab option is 11x cheaper! You&rsquo;re saving almost USD 15,000 a year! This is not something you can ignore in your cost sheet.</p>
<p>As I said, the calculations above are only significant in a scenario with 100 developers. You must do your own math taking into account your team size and number of active projects (you can always archive unused projects).</p>
<p>Even if you don&rsquo;t have 100 developers. Let&rsquo;s consider the scenario for <strong>30 developers</strong> in the new GitHub per user plans and a smaller Travis configuration for 5 concurrent jobs:</p>
<blockquote>
  <p>USD 3,000 (GitHub new plan) + USD 3,000 (Travis) = USD 6,000</p>

</blockquote>
<p>It&rsquo;s 4.5x cheaper in the Digital Ocean + GitLab suite option.</p>
<p>Heck, let&rsquo;s consider the <strong>Current</strong> GitHub plan (the Platinum one, for up to 125 repositories):</p>
<blockquote>
  <p>USD 2,400 (GitHub current plan) + USD 3,000 (Travis) = USD 5,400</p>

</blockquote>
<p>Still at least <strong>4x more expensive</strong> than a GitLab-based solution!</p>
<p>And how long will it take for a single developer to figure out the set up and migrate everything from GitHub over to the new GitLab installation? I will say that you can reserve 1 week of work for the average programmer to do it following the official documentation and my tips and tricks below.</p>
<h2>Installing GitLab CE<span class="hx:absolute hx:-mt-20" id="installing-gitlab-ce"></span>
    <a href="#installing-gitlab-ce" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>I will not bore you with what you can readily find over the Web. I highly recommend you start with the easiest solution first: <a href="https://www.digitalocean.com/features/one-click-apps/gitlab/"target="_blank" rel="noopener">Digital Ocean&rsquo;s One-Click Automatic Install</a>. Install it in at least a 4GB RAM machine (you will want to keep it if you like it).</p>
<p>Of course, there is a number of different installation options, from AWS AMI images to Ubuntu packages you can install manually. Study the <a href="https://about.gitlab.com/installation/"target="_blank" rel="noopener">documentation</a>.</p>
<p>It will cost you USD 40 for a month of trial. If you want to save as much as tens of thousands of dollars, this is a bargain.</p>
<p>GitLab has many customization options. You can lock down your private GitLab to allow only users with an official e-mail from your domain, for example. You can configure <a href="http://docs.gitlab.com/ee/integration/omniauth.html"target="_blank" rel="noopener">OAuth2 providers</a> so your users can quickly sign in using their GitHub, Facebook, Google or other accounts.</p>
<h4>A Few Gotchas<span class="hx:absolute hx:-mt-20" id="a-few-gotchas"></span>
    <a href="#a-few-gotchas" class="subheading-anchor" aria-label="Permalink for this section"></a></h4><p>I&rsquo;ve stumbled upon a few caveats in the configuration. Which is why I recommend that you plan ahead - study this entire article ahead of time! - do a quick install that you can blow away, so you can &ldquo;feel&rdquo; the environment before trying to migrate all your repos over to your brand new GitLab. As a reference, this is a part of my <code>/etc/gitlab/gitlab.rb</code>:</p>
<p>&mdash; ruby</p>
<h1>register a domain for your server and place it here:</h1><p>external_url &ldquo;<a href="http://my-gitlab-server.com/%22"target="_blank" rel="noopener">http://my-gitlab-server.com/"</a></p>
<h1>you will want to enable <a href="https://git-lfs.github.com"target="_blank" rel="noopener">LFS</a></h1><p>gitlab_rails[&rsquo;lfs_enabled&rsquo;] = true</p>
<h1>register your emails</h1><p>gitlab_rails[&lsquo;gitlab_email_from&rsquo;] = &ldquo;<a href="mailto:no-reply@my-gitlab-server.com">no-reply@my-gitlab-server.com</a>&rdquo;
gitlab_rails[&lsquo;gitlab_support_email&rsquo;] = &ldquo;<a href="mailto:contact@my-gitlab-server.com">contact@my-gitlab-server.com</a>&rdquo;</p>
<h1>add your email configuration (template for gmail)</h1><p>gitlab_rails[&lsquo;smtp_enable&rsquo;] = true
gitlab_rails[&lsquo;smtp_address&rsquo;] = &ldquo;smtp.gmail.com&rdquo;
gitlab_rails[&lsquo;smtp_port&rsquo;] = 587
gitlab_rails[&lsquo;smtp_user_name&rsquo;] = &ldquo;&ndash; some no-reply email &mdash;&rdquo;
gitlab_rails[&lsquo;smtp_password&rsquo;] = &ldquo;&ndash; the password &mdash;&rdquo;
gitlab_rails[&lsquo;smtp_domain&rsquo;] = &ldquo;my-gitlab-server.com&rdquo;
gitlab_rails[&lsquo;smtp_authentication&rsquo;] = &ldquo;login&rdquo;
gitlab_rails[&lsquo;smtp_enable_starttls_auto&rsquo;] = true
gitlab_rails[&lsquo;smtp_openssl_verify_mode&rsquo;] = &lsquo;peer&rsquo;</p>
<h1>this is where you enable oauth2 integration</h1><p>gitlab_rails[&lsquo;omniauth_enabled&rsquo;] = true</p>
<h1>CAUTION!</h1><h1>This allows users to login without having a user account first. Define the allowed providers</h1><h1>using an array, e.g. [&ldquo;saml&rdquo;, &ldquo;twitter&rdquo;], or as true/false to allow all providers or none.</h1><h1>User accounts will be created automatically when authentication was successful.</h1><p>gitlab_rails[&lsquo;omniauth_allow_single_sign_on&rsquo;] = [&lsquo;github&rsquo;, &lsquo;google_oauth2&rsquo;, &lsquo;bitbucket&rsquo;]
gitlab_rails[&lsquo;omniauth_block_auto_created_users&rsquo;] = true</p>
<p>gitlab_rails[&lsquo;omniauth_providers&rsquo;] = [
{
&ldquo;name&rdquo; =&gt; &ldquo;github&rdquo;,
&ldquo;app_id&rdquo; =&gt; &ldquo;&ndash; github app id &ndash;&rdquo;,
&ldquo;app_secret&rdquo; =&gt; &ldquo;&ndash; github secret &ndash;&rdquo;,
&ldquo;url&rdquo; =&gt; &ldquo;https://github.com/&rdquo;,
&ldquo;args&rdquo; =&gt; { &ldquo;scope&rdquo; =&gt; &ldquo;user:email&rdquo; }
},
{
&ldquo;name&rdquo; =&gt; &ldquo;google_oauth2&rdquo;,
&ldquo;app_id&rdquo; =&gt; &ldquo;&ndash; google app id &ndash;&rdquo;,
&ldquo;app_secret&rdquo; =&gt; &ldquo;&ndash; google secret &ndash;&rdquo;,
&ldquo;args&rdquo; =&gt; { &ldquo;access_type&rdquo; =&gt; &ldquo;offline&rdquo;, &ldquo;approval_prompt&rdquo; =&gt; &lsquo;&rsquo;, hd =&gt; &lsquo;codeminer42.com&rsquo; }
},
{
&ldquo;name&rdquo; =&gt; &ldquo;bitbucket&rdquo;,
&ldquo;app_id&rdquo; =&gt; &ldquo;&ndash; bitbucket app id &ndash;&rdquo;,
&ldquo;app_secret&rdquo; =&gt; &ldquo;&ndash; bitbucket secret id &ndash;&rdquo;,
&ldquo;url&rdquo; =&gt; &ldquo;https://bitbucket.org/&rdquo;
}
]</p>
<h1>if you&rsquo;re importing repos from GitHub, Sidekiq workers can grow as high as 2.5GB of RAM and the default <a href="http://docs.gitlab.com/ee/operations/sidekiq_memory_killer.html"target="_blank" rel="noopener">Sidekiq Killer</a> config will cap it down to 1GB, so you want to either disable it by adding &lsquo;0&rsquo; or adding a higher limit</h1><h2>gitlab_rails[&rsquo;env&rsquo;] = { &lsquo;SIDEKIQ_MEMORY_KILLER_MAX_RSS&rsquo; =&gt; &lsquo;3000000&rsquo; }<span class="hx:absolute hx:-mt-20" id="gitlab_railsenv---sidekiq_memory_killer_max_rss--3000000-"></span>
    <a href="#gitlab_railsenv---sidekiq_memory_killer_max_rss--3000000-" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>There are <a href="https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/files/gitlab-cookbooks/gitlab/attributes/default.rb#L57"target="_blank" rel="noopener">dozens of default variables</a> you can <a href="https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/settings/environment-variables.md"target="_blank" rel="noopener">override</a>, just be careful on your testings.</p>
<p>Every time you change a configuration, you can just run the following commands:</p>
<hr>
<h2>sudo gitlab-ctl reconfigure
sudo gitlab-ctl restart<span class="hx:absolute hx:-mt-20" id="sudo-gitlab-ctl-restart"></span>
    <a href="#sudo-gitlab-ctl-restart" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>You can open a Rails console to inspect production objects like this:</p>
<hr>
<h2>gitlab-rails console<span class="hx:absolute hx:-mt-20" id="gitlab-rails-console"></span>
    <a href="#gitlab-rails-console" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>I had a lot of trouble importing big repos from GitHub, but after a few days debugging the problem with GitLab Core Team developers <a href="https://gitlab.com/u/dbalexandre"target="_blank" rel="noopener">Douglas Alexandre</a>, <a href="https://gitlab.com/u/brodock"target="_blank" rel="noopener">Gabriel Mazetto</a>, a few Merge Requests and some local patching, I was finally able to import relatively big projects (more than 5,000 commits, more than 1,000 issues, more than 1,200 pull requests with several comments worth of discussion threads). A project of this size can take a couple of hours to complete, mainly because <strong>it&rsquo;s damn slow to use GitHub&rsquo;s public APIs</strong> (they are slow and they have rate limits and abuse detection, so you can&rsquo;t fetch everything as fast as your bandwidth would allow).</p>
<p>(By the way, don&rsquo;t miss that GitLab will be over at <a href="http://www.rubyconf.com.br/pt-BR/speakers#Gabriel%20Gon%c3%a7alves%20Nunes%20Mazetto"target="_blank" rel="noopener">Rubyconf Brazil 2016</a>, on Sep 23-24)</p>
<p>Migrating all my GitHub projects took a couple of days, but they all went through smoothly and my team didn&rsquo;t have any trouble, just adjusting their git remote URLs and they&rsquo;re done.</p>
<p>The import procedure from GitHub is quite complete, it brings not only the git repo per se, but also all the metadata, from labels to comments and pull request history - which is the one that usually takes more time.</p>
<p>But I&rsquo;d recommend waiting for at least version 8.11 (it&rsquo;s currently 8.10.3) before trying to import large GitHub projects.</p>
<p>If you&rsquo;re on Bitbucket, unfortunately there are fewer features in the importer. It will mostly just bring the source code. So be aware of that if you extensively depend on their pull request system and you want to preserve this history. More features will come and you can even help them out, they are very resourceful and willing to make GitLab better.</p>
<h3>Side-track: Customizations for every Digital Ocean box<span class="hx:absolute hx:-mt-20" id="side-track-customizations-for-every-digital-ocean-box"></span>
    <a href="#side-track-customizations-for-every-digital-ocean-box" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Assume that you should run what&rsquo;s in this section for all new machines you create over Digital Ocean.</p>
<p>First of all, they come without a swap file. No matter how much RAM you have, the Linux OS is meant to work better by combining a swap file. You can <a href="https://www.digitalocean.com/community/tutorials/how-to-add-swap-on-ubuntu-14-04"target="_blank" rel="noopener">read more about it</a> later, for now just run the following as root:</p>
<hr>
<p>fallocate -l 4G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile</p>
<h2>sysctl vm.swappiness=10
sysctl vm.vfs_cache_pressure=50<span class="hx:absolute hx:-mt-20" id="sysctl-vmvfs_cache_pressure50"></span>
    <a href="#sysctl-vmvfs_cache_pressure50" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Edit the <code>/etc/fstab</code> file and add this line:</p>
<hr>
<h2>/swapfile   none    swap    sw    0   0<span class="hx:absolute hx:-mt-20" id="swapfile---none----swap----sw----0---0"></span>
    <a href="#swapfile---none----swap----sw----0---0" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Finally, edit the <code>/etc/sysctl.conf</code> file and add these lines:</p>
<hr>
<h2>vm.swappiness=10
vm.vfs_cache_pressure = 50<span class="hx:absolute hx:-mt-20" id="vmvfs_cache_pressure--50"></span>
    <a href="#vmvfs_cache_pressure--50" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Don&rsquo;t forget to set the <a href="http://askubuntu.com/questions/162391/how-do-i-fix-my-locale-issue"target="_blank" rel="noopener">default locale</a> of your machine. Start by editing the <code>/etc/environment</code> file and adding:</p>
<hr>
<h2>LC_ALL=en_US.UTF-8
LANG=en_US.UTF-8<span class="hx:absolute hx:-mt-20" id="langen_usutf-8"></span>
    <a href="#langen_usutf-8" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Then run:</p>
<hr>
<h2>sudo locale-gen en_US en_US.UTF-8
sudo dpkg-reconfigure locales<span class="hx:absolute hx:-mt-20" id="sudo-dpkg-reconfigure-locales"></span>
    <a href="#sudo-dpkg-reconfigure-locales" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Finally, you should have Ubuntu automatically install stable security patches for you. You don&rsquo;t want to forget machines online without the most current security fixes, so just run this:</p>
<hr>
<h2>sudo dpkg-reconfigure &ndash;priority=low unattended-upgrades<span class="hx:absolute hx:-mt-20" id="sudo-dpkg-reconfigure-prioritylow-unattended-upgrades"></span>
    <a href="#sudo-dpkg-reconfigure-prioritylow-unattended-upgrades" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Choose &ldquo;yes&rdquo; and you&rsquo;re done. And of course, for every fresh install, it&rsquo;s always good to run the good old:</p>
<hr>
<h2>sudo apt-get update &amp;&amp; sudo apt-get upgrade<span class="hx:absolute hx:-mt-20" id="sudo-apt-get-update--sudo-apt-get-upgrade"></span>
    <a href="#sudo-apt-get-update--sudo-apt-get-upgrade" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>This is the very basics, I believe it&rsquo;s easier to have an image with all this ready, but if you use the standard Digital Ocean images, these settings should do the trick for now.</p>
<h2>Installing the CI Runner<span class="hx:absolute hx:-mt-20" id="installing-the-ci-runner"></span>
    <a href="#installing-the-ci-runner" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Once you finish your GitLab installation, it&rsquo;s <a href="https://about.gitlab.com/2016/04/19/how-to-set-up-gitlab-runner-on-digitalocean/"target="_blank" rel="noopener">super easy</a> to deploy the GitLab Runner. You can use the same machine but I recommend you install it in a separate machine.</p>
<p>If you don&rsquo;t know what a runner is, just imagine it like this: It&rsquo;s basically a server connected to the GitLab install. When it&rsquo;s available and online, whenever someone pushes a new commit, merge request, to a repository that has a <code>gitlab-ci-yml</code> file present, GitLab will push a command to the runner.</p>
<p>Depending on how you configured the runner, it will receive this command and spawn a new Docker container. Inside the container it will execute whatever you have defined in the <code>gitlab-ci.yml</code> file in the project. Usually it&rsquo;s fetching cached files (dependencies, for example), and run your test suite.</p>
<p>In the most basic setup, you will only have one Runner and any subsequent builds from other users will wait in line until they finish. If you&rsquo;ve used external CI services such as Travis-CI or CircleCI, you know that they charge for some number of concurrent builds. And it&rsquo;s <strong>very expensive</strong>.</p>
<p>The less concurrent builds available, the more your users will have to wait for feedback on their changes, and less productive you will become. People may even start to avoid adding new tests, or completely ignore the tests, which will really hurt the quality of your project over time. If there is one thing you <strong>must not</strong> do is not having good automated test suites.</p>
<p>Gabriel Mazetto pointed me to a very important GitLab CI Runner feature: <a href="https://about.gitlab.com/2016/03/29/gitlab-runner-1-1-released/"target="_blank" rel="noopener"><strong>auto-scaling</strong></a>. This is what they use in their hosted offering over at GitLab.com.</p>
<p>You can <strong>easily</strong> set up a runner that can use &ldquo;docker-machine&rdquo; and your IaaS provider APIs to spin up machines on the fly to run as many concurrent builds as you want, and it will be super cheap!</p>
<p>For example, on Digital Ocean you can be charged USD 0.06 (6 cents) per hour of usage of a 4GB machine. Over at AWS EC2 you can be charged USD 0.041 per hour for an m3.medium machine.</p>
<p>There is extensive documentation but I will try to summarize what you have to do. For more details I highly recommend you to study their <a href="https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/blob/master/docs/install/autoscaling.md#prepare-the-docker-registry-and-cache-server"target="_blank" rel="noopener">official documentation</a>.</p>
<p>Start by creating 3 new machines at Digital Ocean, all in the same Region with private networking enabled! I will list a fake private IP address just for the sake of advancing in the configuration examples:</p>
<ul>
<li>a 1GB machine called &ldquo;docker-registry-mirror&rdquo;, (ex 10.0.0.1)</li>
<li>a 1GB machine called &ldquo;ci-cache&rdquo;, (ex 10.0.0.2)</li>
<li>a 1GB machine called &ldquo;ci-runner&rdquo;, (ex 10.0.0.3)</li>
</ul>
<p>Yeah, they can be small as very little will run on them. You can be conservative and choose the 2GB RAM options just to be on the safe side (and pricing will still be super cheap).</p>
<p>Don&rsquo;t forget to execute the basic configuration I mentioned above to enable a swapfile, auto security update and locale regeneration.</p>
<p>SSH in to &ldquo;docker-registry-mirror&rdquo; and just run:</p>
<hr>
<h2>docker run -d -p 6000:5000 <br>
-e REGISTRY_PROXY_REMOTEURL=https://registry-1.docker.io <br>
&ndash;restart always <br>
&ndash;name registry registry:2<span class="hx:absolute hx:-mt-20" id="name-registry-registry2"></span>
    <a href="#name-registry-registry2" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Now you will have a local Docker images registry proxy and cache at <code>10.0.0.1:6000</code> (take note of the real private IP).</p>
<p>SSH in to &ldquo;ci-cache&rdquo; and run:</p>
<hr>
<p>mkdir -p /export/runner</p>
<h2>docker run -it &ndash;restart always -p 9005:9000 <br>
-v /.minio:/root/.minio -v /export:/export <br>
&ndash;name minio <br>
minio/minio:latest /export<span class="hx:absolute hx:-mt-20" id="miniominiolatest-export"></span>
    <a href="#miniominiolatest-export" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Now you will have an AWS S3 clone called <a href="https://github.com/minio/minio"target="_blank" rel="noopener">Minio</a> running. I didn&rsquo;t know this project even existed, but it is a nifty little service written in Go to clone the AWS S3 behavior and APIs. So now you can have your very own S3 inside your infrastructure!</p>
<p>After Docker spins up, it will print out the Access Key and Secret keys, make notes. And this service will be running at <code>10.0.0.2:9005</code>.</p>
<p>You can even open a browser and see their web interface at <code>http://10.0.0.2:9005</code> and use the access and secret keys to login. Make sure you have a bucket named &ldquo;runner&rdquo;. The files will be stored at the <code>/export/runner</code> directory.</p>
<p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/549/big_Minio_Browser.png" alt="Minio Dashboard"  loading="lazy" /></p>
<p>Make sure the <a href="http://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html"target="_blank" rel="noopener">bucket name is valid</a> (it must be a valid DNS naming, for example, DO NOT use underlines).</p>
<p>Open this URL from your freshly installed GitLab-CE: <code>http://yourgitlab.com/admin/runners</code> and take note of the Registration Token. Let&rsquo;s say it&rsquo;s <code>1aaaa_Z1AbB2CdefGhij</code></p>
<p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/550/big_Admin_Area___GitLab.png" alt="Admin Area for Runner Registration Token"  loading="lazy" /></p>
<p>Finally, SSH in to &ldquo;ci-runner&rdquo; and run:</p>
<hr>
<p>curl -L <a href="https://github.com/docker/machine/releases/download/v0.7.0/docker-machine-%60uname"target="_blank" rel="noopener">https://github.com/docker/machine/releases/download/v0.7.0/docker-machine-`uname</a> -s<code>-</code>uname -m` &gt; /usr/local/bin/docker-machine</p>
<p>chmod +x /usr/local/bin/docker-machine</p>
<p>curl -L <a href="https://packages.gitlab.com/install/repositories/runner/gitlab-ci-multi-runner/script.deb.sh"target="_blank" rel="noopener">https://packages.gitlab.com/install/repositories/runner/gitlab-ci-multi-runner/script.deb.sh</a> | sudo bash</p>
<p>sudo apt-get install gitlab-ci-multi-runner</p>
<h2>rm -Rf ~/.docker # just to make sure<span class="hx:absolute hx:-mt-20" id="rm--rf-docker--just-to-make-sure"></span>
    <a href="#rm--rf-docker--just-to-make-sure" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Now you can register this new runner with your GitLab install, you will need the Registration Token mentioned above.</p>
<hr>
<h2>sudo gitlab-ci-multi-runner register<span class="hx:absolute hx:-mt-20" id="sudo-gitlab-ci-multi-runner-register"></span>
    <a href="#sudo-gitlab-ci-multi-runner-register" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>You will be asked a few questions, and this is what you can answer:</p>
<hr>
<h2>Please enter the gitlab-ci coordinator URL (e.g. <a href="https://gitlab.com/ci"target="_blank" rel="noopener">https://gitlab.com/ci</a> )
<a href="https://yourgitlab.com/ci"target="_blank" rel="noopener">https://yourgitlab.com/ci</a>
Please enter the gitlab-ci token for this runner
1aaaa_Z1AbB2CdefGhij # as in the example above
Please enter the gitlab-ci description for this runner
my-autoscale-runner
INFO[0034] fcf5c619 Registering runner&hellip; succeeded
Please enter the executor: shell, docker, docker-ssh, docker+machine, docker-ssh+machine, ssh?
docker+machine
Please enter the Docker image (eg. ruby:2.1):
codeminer42/ci-ruby:2.3
INFO[0037] Runner registered successfully. Feel free to start it, but if it&rsquo;s
running already the config should be automatically reloaded!<span class="hx:absolute hx:-mt-20" id="running-already-the-config-should-be-automatically-reloaded"></span>
    <a href="#running-already-the-config-should-be-automatically-reloaded" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Let&rsquo;s make a copy of the original configuration, just to be safe:</p>
<hr>
<h2>cp /etc/gitlab-runner/config.toml /etc/gitlab-runner/config.bak<span class="hx:absolute hx:-mt-20" id="cp-etcgitlab-runnerconfigtoml-etcgitlab-runnerconfigbak"></span>
    <a href="#cp-etcgitlab-runnerconfigtoml-etcgitlab-runnerconfigbak" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Copy the first few lines of this file (you want the token), it will look like this:</p>
<hr>
<p>concurrent = 1
check_interval = 0</p>
<h2>[[runners]]
name = &ldquo;my-autoscale-runner&rdquo;
url = &ldquo;<a href="http://yourgitlab.com/ci%22"target="_blank" rel="noopener">http://yourgitlab.com/ci"</a>
token = &ldquo;&mdash; generated runner token &mdash;&rdquo;
executor = &ldquo;docker+machine&rdquo;<span class="hx:absolute hx:-mt-20" id="executor--dockermachine"></span>
    <a href="#executor--dockermachine" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The important part here is the &ldquo;token&rdquo;. You will want to take note of it. And now you also will want to create a <a href="https://cloud.digitalocean.com/settings/api/tokens"target="_blank" rel="noopener">new API Token over at Digital Ocean</a>. Just Generate a New Token and take note.</p>
<p>You can now replace the entire <code>config.toml</code> file for this:</p>
<hr>
<p>concurrent = 20
check_interval = 0</p>
<h2>[[runners]]
name = &ldquo;my-autoscale-runner&rdquo;
url = &ldquo;<a href="http://yourgitlab.com/ci%22"target="_blank" rel="noopener">http://yourgitlab.com/ci"</a>
token = &ldquo;&mdash; generated runner token &mdash;&rdquo;
executor = &ldquo;docker+machine&rdquo;
limit = 15
[runners.docker]
tls_verify = false
image = &ldquo;codeminer42/ci-ruby:2.3&rdquo;
privileged = false
[runners.machine]
IdleCount = 2                   # There must be 2 machines in Idle state
IdleTime = 1800                 # Each machine can be in Idle state up to 30 minutes (after this it will be removed)
MaxBuilds = 100                 # Each machine can handle up to 100 builds in a row (after this it will be removed)
MachineName = &ldquo;ci-auto-scale-%s&rdquo;   # Each machine will have a unique name (&rsquo;%s&rsquo; is required)
MachineDriver = &ldquo;digitalocean&rdquo;  # Docker Machine is using the &lsquo;digitalocean&rsquo; driver
MachineOptions = [
&ldquo;digitalocean-image=coreos-beta&rdquo;,
&ldquo;digitalocean-ssh-user=core&rdquo;,
&ldquo;digitalocean-access-token=&ndash; your new Digital Ocean API Token &ndash;&rdquo;,
&ldquo;digitalocean-region=nyc1&rdquo;,
&ldquo;digitalocean-size=4gb&rdquo;,
&ldquo;digitalocean-private-networking&rdquo;,
&ldquo;engine-registry-mirror=http://10.0.0.1:6000&rdquo;
]
[runners.cache]
Type = &ldquo;s3&rdquo;   # The Runner is using a distributed cache with Amazon S3 service
ServerAddress = &ldquo;10.0.0.2:9005&rdquo;  # minio
AccessKey = &ldquo;&ndash; your minio access key &ndash;&rdquo;
SecretKey = &ldquo;&ndash; your minio secret key&rdquo;
BucketName = &ldquo;runner&rdquo;
Insecure = true # Use Insecure only when using with Minio, without the TLS certificate enabled<span class="hx:absolute hx:-mt-20" id="insecure--true--use-insecure-only-when-using-with-minio-without-the-tls-certificate-enabled"></span>
    <a href="#insecure--true--use-insecure-only-when-using-with-minio-without-the-tls-certificate-enabled" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>And you can restart the runner to pick up the new configuration like this:</p>
<hr>
<h2>gitlab-ci-multi-runner restart<span class="hx:absolute hx:-mt-20" id="gitlab-ci-multi-runner-restart"></span>
    <a href="#gitlab-ci-multi-runner-restart" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>As I said before, you will want to read the extensive <a href="https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/blob/master/docs/configuration/autoscale.md"target="_blank" rel="noopener">official documentation</a> (and every link within).</p>
<p>If you did everything right, changing the correct private IPs for the docker registry and cache, the correct tokens, and so forth, you can log in to your Digital Ocean dashboard and you will see something like this:</p>
<p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/548/big_DigitalOcean_-_Droplets.png" alt="Digital Ocean CI Setup"  loading="lazy" /></p>
<p>And from the <code>ci-runner</code> machine, you can list them like this:</p>
<hr>
<h1>docker-machine ls</h1><h2>NAME                                                ACTIVE   DRIVER         STATE     URL                         SWARM   DOCKER    ERRORS
runner-xxxx-ci-auto-scale-xxxx-xxxx   -        digitalocean   Running   tcp://191.168.0.1:2376            v1.10.3
runner-xxxx-ci-auto-scale-xxxx-xxxx   -        digitalocean   Running   tcp://192.169.0.2:2376           v1.10.3<span class="hx:absolute hx:-mt-20" id="runner-xxxx-ci-auto-scale-xxxx-xxxx------------digitalocean---running---tcp192169022376-----------v1103"></span>
    <a href="#runner-xxxx-ci-auto-scale-xxxx-xxxx------------digitalocean---running---tcp192169022376-----------v1103" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>They should not list any errors, meaning that they are up and running, waiting for new builds to start.</p>
<p>There will be 2 new machines listed in your Digital Ocean dashboard, named &ldquo;runner-xxxxx-ci-auto-scale-xxxxx&rdquo;. This is what <code>IdleCount = 2</code> does. If they stay idle for more than 30 minutes (<code>IdleTime = 1800 </code>) they will be shut down so you don&rsquo;t get charged.</p>
<p>You can have several &ldquo;runner&rdquo; definitions, each with a <code>limit</code> of builds/machines that can be spawned in Digital Ocean. You can have other runner definitions for other providers, for example. But in this example we are limited to at most 15 machines, so 15 concurrent builds.</p>
<p>The <code>concurrent</code> limit is a global setting. So if I had 3 runner definitions, each with a <code>limit</code> of 15, they would still be globally limited to 20 as defined in the <code>concurrent</code> global variable.</p>
<p>You can use different providers for specific needs, for example, to run OS X builds or Raspberry Pi builds or other exotic kinds of builds. In the example I am keeping it simple and just setting many builds in the same provider (Digital Ocean).</p>
<p>And don&rsquo;t worry about the monthly fee for each machine. When used in this manner, you will be paying per hour.</p>
<p>Also, make sure you spun up all your machines (docker-registry, minio cache, CI runner) all with <strong>private networking enabled</strong> (so they talk through the internal VLAN instead of having to go all the way through the public internet) and that they are all in the same region data center (NYC1 is New York 1 - New York has 3 sub-regions, for example). Don&rsquo;t start machines in different regions.</p>
<p>Because we have Docker proxy/cache and Minio/S3 cache, your builds will take longer the first time (let&rsquo;s say, 5 minutes), and then subsequent builds will fetch everything from the cache (taking, let&rsquo;s say, 1:30 minute). It&rsquo;s fast and it&rsquo;s convenient.</p>
<h3>Setting up each Project for the Runner<span class="hx:absolute hx:-mt-20" id="setting-up-each-project-for-the-runner"></span>
    <a href="#setting-up-each-project-for-the-runner" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The Runner is one of the newest pieces of the GitLab ecosystem so you might have some trouble at first to figure out a decent configuration. But once you have the whole infrastructure figured out as described in the previous section, now it&rsquo;s as easy as adding a <code>.gitlab-ci.yml</code> file to your root directory. Something like this:</p>
<p>&mdash; yaml</p>
<h1>This file is a template, and might need editing before it works on your project.</h1><p>image: codeminer42/ci-ruby:2.3</p>
<h1>Pick zero or more services to be used on all builds.</h1><h1>Only needed when using a docker container to run your tests in.</h1><h1>Check out: <a href="http://docs.gitlab.com/ce/ci/docker/using_docker_images.html#what-is-service"target="_blank" rel="noopener">http://docs.gitlab.com/ce/ci/docker/using_docker_images.html#what-is-service</a></h1><p>services:</p>
<ul>
<li>postgres:latest</li>
<li>redis:latest</li>
</ul>
<p>cache:
key: your-project-name
untracked: true
paths:
- .ci_cache/</p>
<p>variables:
RAILS_ENV: &rsquo;test&rsquo;
DATABASE_URL: postgresql://postgres:@postgres
CODECLIMATE_REPO_TOKEN: &ndash; your codeclimate project token &ndash;</p>
<p>before_script:</p>
<ul>
<li>bundle install &ndash;without development production -j $(nproc) &ndash;path .ci_cache</li>
<li>cp .env.sample .env</li>
<li>cp config/database.yml.example config/database.yml</li>
<li>bundle exec rake db:create db:migrate</li>
</ul>
<h2>test:
script:
- xvfb-run bundle exec rspec<span class="hx:absolute hx:-mt-20" id="--xvfb-run-bundle-exec-rspec"></span>
    <a href="#--xvfb-run-bundle-exec-rspec" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>My team at <a href="http://www.codeminer42.com"target="_blank" rel="noopener">Codeminer 42</a> prepared a <a href="https://hub.docker.com/r/codeminer42/ci-ruby/"target="_blank" rel="noopener">simple Docker image</a> with useful stuff pre-installed (such as the newest phantomjs, xvfb, etc), so it&rsquo;s now super easy to enable automated builds within GitLab by just adding this file to the repositories. (Thanks to Carlos Lopes, Danilo Resende and Paulo Diovanni - who will be talking <a href="http://www.rubyconf.com.br/pt-BR/speakers#Paulo%20Diovani%20Gon%c3%a7alves"target="_blank" rel="noopener">about Docker at Rubyconf Brasil 2016</a>, by the way)</p>
<p>GitLab-CI even supports building a pending Merge Request, and you can enforce the request so it can only be merged if builds pass, just like in GitHub + Travis. And as Code Climate is agnostic to Repository host or CI runner, you can easily integrate it as well.</p>
<p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/551/big_Settings___Codeminer42___CM-Fulcrum___GitLab.png" alt="Project Force Successful Build to Merge"  loading="lazy" /></p>
<h2>Conclusion<span class="hx:absolute hx:-mt-20" id="conclusion"></span>
    <a href="#conclusion" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The math is hard to argue against: the GitLab + GitLab-CI + Digital Ocean combo is a big win. GitLab&rsquo;s interface is very familiar so users from GitHub or Bitbucket will feel quite at home in no time.</p>
<p>We can use all the <a href="https://about.gitlab.com/2014/09/29/gitlab-flow/"target="_blank" rel="noopener">Git flows</a> we&rsquo;re used to.</p>
<p>GitLab-CE is still a work in progress though, the team is increasing their pace but there are currently more than <a href="https://gitlab.com/gitlab-org/gitlab-ce/issues"target="_blank" rel="noopener">4,200 open issues</a>. But as this is all Ruby on Rails and Ruby tooling, you can easily jump in and contribute. No contribution is too small. Just reporting how to reproduce a bug is help enough to assist the developers to figure out how to improve faster.</p>
<p>But don&rsquo;t shy away because of the open issues, it&rsquo;s fully functional as of right now and I have not found any bugs that could be considered show stoppers.</p>
<p>They have many things right. First of all, it&rsquo;s a &ldquo;simple&rdquo; Ruby on Rails project. It&rsquo;s a no-frills front-end with plain jQuery. The choice of HAML for the views is questionable but it doesn&rsquo;t hurt. They use good old Sidekiq+Redis for asynchronous jobs. No black magic here. A pure monolith that&rsquo;s not difficult to understand and to contribute.</p>
<p>The APIs are all written using Grape. They have the <a href="https://gitlab.com/gitlab-org/gitlab-ce"target="_blank" rel="noopener">GitLab CE</a> project separated from other components, such as the <a href="https://gitlab.com/gitlab-org/gitlab-shell"target="_blank" rel="noopener">GitLab-Shell</a> and <a href="https://gitlab.com/gitlab-org/gitlab-ci-multi-runner"target="_blank" rel="noopener">GitLab-CI-Multi-Runner</a>.</p>
<p>They also forked <a href="https://gitlab.com/gitlab-org/omnibus-gitlab"target="_blank" rel="noopener">Omnibus</a> in order to be able to package the CE Rails project as a &ldquo;.deb&rdquo;. Everything is orchestrated with Docker. And when a new version is available, you only need to <code>apt-get update &amp;&amp; apt-get upgrade</code> and it will do all the work of backing up and migrating Postgresql, updating the code, bundling in new dependencies, restarting the services and so forth. It&rsquo;s super convenient and you should take a look at this project if you have complicated Rails deployments into your own infrastructure (out of Heroku, for example).</p>
<p>I am almost done moving hundreds of repositories from both BitBucket and GitHub to GitLab right now and the developers from my company are already using it on a daily basis without any problems. We are almost at the point where we can disengage from BitBucket, GitHub and external CIs.</p>
<p>You will be surprised how easily your company can do it too and save a couple thousand dollars in the process, while having fun doing it!</p>
]]></content:encoded><category>version-control</category><category>devops</category></item><item><title>Is your Rails app ready for Production?</title><link>https://akitaonrails.github.io/en/2016/03/22/is-your-rails-app-ready-for-production/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2016/03/22/is-your-rails-app-ready-for-production/</guid><pubDate>Tue, 22 Mar 2016 15:11:00 -0300</pubDate><description>&lt;p&gt;This is a checklist. You should follow these instructions if you want some peace of mind after deploying an application to production. If you&amp;rsquo;re experienced, please contribute in the comments section below. But if you&amp;rsquo;re a beginner, you should pay attention.&lt;/p&gt;
&lt;h3&gt;Where to Deploy? TL;DR: Heroku&lt;span class="hx:absolute hx:-mt-20" id="where-to-deploy-tldr-heroku"&gt;&lt;/span&gt;
&lt;a href="#where-to-deploy-tldr-heroku" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;Many people will first target the cheap choices. Usually EC2 micro instances or small Digital Ocean droplets. Unless you&amp;rsquo;re aware that you &lt;strong&gt;must&lt;/strong&gt; constantly monitor and upgrade those instances, &lt;strong&gt;DON&amp;rsquo;T&lt;/strong&gt; do it.&lt;/p&gt;</description><content:encoded><![CDATA[<p>This is a checklist. You should follow these instructions if you want some peace of mind after deploying an application to production. If you&rsquo;re experienced, please contribute in the comments section below. But if you&rsquo;re a beginner, you should pay attention.</p>
<h3>Where to Deploy? TL;DR: Heroku<span class="hx:absolute hx:-mt-20" id="where-to-deploy-tldr-heroku"></span>
    <a href="#where-to-deploy-tldr-heroku" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Many people will first target the cheap choices. Usually EC2 micro instances or small Digital Ocean droplets. Unless you&rsquo;re aware that you <strong>must</strong> constantly monitor and upgrade those instances, <strong>DON&rsquo;T</strong> do it.</p>
<p>Instead choose a PaaS solution such as Heroku. It&rsquo;s a no-brainer. Software in production is not a one-off thing. For security alone you should not do it by yourself unless you know how to harden an OS distribution. For example, most people don&rsquo;t even know what <a href="http://www.fail2ban.org/wiki/index.php/Main_Page"target="_blank" rel="noopener">fail2ban</a> is. Don&rsquo;t assume that because you use SSH with keypairs you&rsquo;re secure. You&rsquo;re not.</p>
<p>For example, did you know that we just went through a <a href="https://blog.heroku.com/archives/2016/3/21/patching-glibc-security-hole"target="_blank" rel="noopener">glibc security crisis</a>? If you don&rsquo;t and you haven&rsquo;t touched your production servers in some time to upgrade all your components, you&rsquo;re open, wide open. I had many clients who hired freelancers to implement and deploy their apps and then went without anyone checking. You&rsquo;re all unsafe.</p>
<p>Heroku is no guarantee either, because app libraries get old, vulnerabilities get discovered, and you should upgrade them. Rails has an <a href="https://groups.google.com/forum/#!forum/rubyonrails-security"target="_blank" rel="noopener">active security team</a> releasing security patches all the time. If you haven&rsquo;t been upgrading, you&rsquo;re unsafe.</p>
<p>But 24x7 support is not cheap, if you have your own infrastructure or use an IaaS platform such as AWS EC2, you should have 24x7 personnel, this means at least 2 full-time system administrators/devops taking care of things. Or, you use a PaaS that at least keeps those basic sanitations in check and a part-time developer to at least keep upgrading your apps.</p>
<p>If you don&rsquo;t, you&rsquo;re opening your business to serious liability. And if you&rsquo;re a developer, do the responsible thing: disclose this fact to your clients and let them make an informed choice. They can choose to stay cheap, but at least they know what this actually means.</p>
<h3>Which web server? TL;DR: Passenger or Puma<span class="hx:absolute hx:-mt-20" id="which-web-server-tldr-passenger-or-puma"></span>
    <a href="#which-web-server-tldr-passenger-or-puma" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Most Rails apps lack basic libraries that should be installed to make them work correctly.</p>
<p>I&rsquo;ve seen people running production apps by just firing up <tt>rails server</tt> and nothing else! This is running the basic Webrick web server, in production! You must choose between Puma, Unicorn, Passenger. Unicorn actually does not protect your app against slow clients, so avoid it if you can.</p>
<p>If you&rsquo;re in doubt, <a href="https://www.phusionpassenger.com/library/walkthroughs/deploy/ruby/heroku/standalone/oss/deploy_app_main.html"target="_blank" rel="noopener">install Passenger</a> and call it a day.</p>
<p>The most common choice nowadays is <a href="https://devcenter.heroku.com/articles/deploying-rails-applications-with-the-puma-web-server"target="_blank" rel="noopener">using Puma</a>. But if you&rsquo;re running under Heroku or using small instances, keep in mind your RAM usage. A normal Rails application consumes at least 150Mb to 200Mb or more depending on the size of your app. The smallest Heroku dyno has only 512Mb of RAM, that means you should not configure Puma or Unicorn to have more than 2 instances. I&rsquo;d recommend going for the 2X Dyno (1GB) and have at most 3 instances (which means 3 possible concurrent requests being processed at once).</p>
<p>It&rsquo;s not uncommon for your app to keep increasing RAM usage over time and once every couple of days you see R14 happening, then your dyno reboots and it starts working better for a while until it reaches the ceiling again.</p>
<p>If you don&rsquo;t know why that&rsquo;s happening, and it&rsquo;s infrequent, you can temporarily use <a href="https://github.com/schneems/puma_worker_killer"target="_blank" rel="noopener">Puma Worker Killer</a> - if you&rsquo;re using Puma, of course - to keep monitoring your instances until they reach a certain lower ceiling and they automatically recycle before your users see the R14 errors.</p>
<p>You may also have heard of <a href="http://tmm1.net/ruby21-oobgc"target="_blank" rel="noopener">Out-of-Band GC</a>. Just <a href="https://github.com/puma/puma/issues/450"target="_blank" rel="noopener">be warned</a> that Puma in threaded mode won&rsquo;t work well with it. If you&rsquo;re using Unicorn, by all means do use it.</p>
<h3>Measure, Measure, Measure. TL;DR: New Relic<span class="hx:absolute hx:-mt-20" id="measure-measure-measure-tldr-new-relic"></span>
    <a href="#measure-measure-measure-tldr-new-relic" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/535/big_heroku-metrics.png" alt="Heroku Metrics"  loading="lazy" /></p>
<p>You should <strong>always</strong> monitor your app. Be it with custom installed software such as Nagios, Munin, Zabbix, Zenoss, Ganglia, etc or at the very least the metrics that Heroku provides you, as in the image above.</p>
<p>If your instances are behaving strangely, you should take a look at the warnings. A frequent set of <a href="https://devcenter.heroku.com/articles/ruby-memory-use"target="_blank" rel="noopener">R14</a> errors means that your app&rsquo;s memory consumption is growing over time and blowing the maximum amount of RAM available in your instance (a probable memory leaking). At the very least this means that you should decrease the number of concurrency/workers in your Puma/Unicorn and also increase the number of dynos to compensate if you do have enough traffic to justify it. But this is one metrics-oriented action that you can take because you have the information to make an informed decision.</p>
<p>But you should also be storing and indexing your logs using custom installed software such as Greylog2 or Logstash or using a SaaS alternative such as <a href="https://elements.heroku.com/addons/papertrail"target="_blank" rel="noopener">Papertrail</a>. There are many reasons why an app might be leaking, or why everything seems fine in the metrics but you&rsquo;re having random errors. Always query the logs for answers.</p>
<p>Better yet, <a href="https://docs.newrelic.com/docs/agents/ruby-agent/miscellaneous/ruby-agent-heroku"target="_blank" rel="noopener">install New Relic APM</a>. You will have both the metrics, the automated alerts, the errors measurement and really deep diagnostics to see exactly what line in your source code is guilty for your poor performance or frequent errors or erratic behavior. If you&rsquo;re in doubt, just install it, the basic plan is free anyway.</p>
<h3>Some Rails Dependencies<span class="hx:absolute hx:-mt-20" id="some-rails-dependencies"></span>
    <a href="#some-rails-dependencies" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>You should <strong>NOT</strong> be using Rails below version 4.0 by now. 3.2 is still widely used but it will stop being supported soon. Do not use Rails 5.0 right now unless you&rsquo;re experienced enough to know what to do. Anything from 4.0 to 4.2 should be ok, but be aware of the minor releases that fix security vulnerabilities.</p>
<p>You should <strong>NOT</strong> be using Ruby below version 2.2 by now. Security upgrades for 2.0 ended this February 2016. 2.1 was not a good stable release, but 2.2 and 2.3 are very good and you should be using them.</p>
<p>So the combination of Ruby 2.3.0 and Rails 4.2.5.2 (by March 2016) is the optimal choice for best stability, security and ecosystem support.</p>
<p>You should have at least the following dependencies installed by default:</p>
<ul>
<li><a href="https://github.com/heroku/rails_12factor"target="_blank" rel="noopener">Rails 12 Factor gem</a></li>
<li><a href="http://rtomayko.github.io/rack-cache/"target="_blank" rel="noopener">Rack-Cache middleware</a>
<ul>
<li><a href="https://devcenter.heroku.com/articles/memcachier#rails-3-and-4"target="_blank" rel="noopener">Dalli gem with Memcached or Memcachier</a> - even if you&rsquo;re not using fragment caching or AR caching, this is still useful at least for the <a href="https://devcenter.heroku.com/articles/rack-cache-memcached-rails31"target="_blank" rel="noopener">Rack-Cache gem</a>.</li>
</ul>
</li>
<li><a href="https://github.com/kickstarter/rack-attack"target="_blank" rel="noopener">Rack-Attack</a> - read how to properly configure!</li>
<li><a href="http://www.sinatrarb.com/rack-protection/"target="_blank" rel="noopener">Rack-Protection</a> - read how to properly configure and what protections you want to activate, not everything, but at least CSRF and XSS.</li>
<li><a href="http://www.schneems.com/blogs/2016-02-18-speeding-up-sprockets/"target="_blank" rel="noopener">Sprockets 3.3+</a> - make sure you&rsquo;re using the Sprockets version that fixes the slow speeds and makes it reasonably faster</li>
</ul>
<h3>Which Database? TL;DR: use Postgresql<span class="hx:absolute hx:-mt-20" id="which-database-tldr-use-postgresql"></span>
    <a href="#which-database-tldr-use-postgresql" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p><strong>Do NOT</strong> choose MongoDB or similar unless you have a very strong reason to and you are confident that you&rsquo;re very good at what you&rsquo;re doing and you will be sticking around to maintain what you deploy. Otherwise stick to Postgresql and the &ldquo;pg&rdquo; gem, you won&rsquo;t regret it, nor will your client.</p>
<p>Do use small Redis instances (particularly SaaS options such as <a href="https://elements.heroku.com/addons/heroku-redis"target="_blank" rel="noopener">Heroku Redis</a> ) with <a href="https://github.com/mperham/sidekiq/wiki/Deployment"target="_blank" rel="noopener">Sidekiq</a>. You can choose to use the lighter Sucker Punch but be aware that it will increase the RAM usage of your instances considerably depending on the size of your queues and the serialized input data for the workers. It&rsquo;s not always trivial how many Sidekiq workers you should have, but <a href="http://manuel.manuelles.nl/sidekiq-heroku-redis-calc/"target="_blank" rel="noopener">this calculator</a> could help.</p>
<p>There are at least 2 big mistakes most people make using Sidekiq:</p>
<ul>
<li>Having heavy workers hitting your master database. For example a big analytics procedure fetching large sets of data while your main website also has heavy user activity writing data all the time. Definitely add a <a href="https://devcenter.heroku.com/articles/heroku-postgres-follower-databases"target="_blank" rel="noopener">Follower database</a> and point the Sidekiq workers (read-only) there for analytics, reports and similar workloads.</li>
<li>Fetching large datasets in-memory and blowing your worker dynos. If you do anything similar to &ldquo;YourModel.all.each { |r| &hellip; }&rdquo; you&rsquo;re shooting yourself in the foot. Rails has easy ways to <a href="http://api.rubyonrails.org/classes/ActiveRecord/Batches.html"target="_blank" rel="noopener">fetch small sets in batch</a> for you to process, use them.</li>
</ul>
<h3>Are you using a CDN? TL;DR: just do<span class="hx:absolute hx:-mt-20" id="are-you-using-a-cdn-tldr-just-do"></span>
    <a href="#are-you-using-a-cdn-tldr-just-do" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>This is easy, use <a href="https://devcenter.heroku.com/articles/using-amazon-cloudfront-cdn"target="_blank" rel="noopener">AWS CloudFront</a> or <a href="https://devcenter.heroku.com/articles/fastly"target="_blank" rel="noopener">Fastly</a> or something else, but <strong>USE</strong> a CDN. It&rsquo;s a no-brainer.</p>
<p>Otherwise your assets will be served by your already small web dyno instances. And this is heavy lifting that&rsquo;s better served from a much faster CDN. Your users will perceive orders of magnitude in usability by changing just a couple of configuration lines in your code.</p>
<p>If you implemented your Rails views, templates, stylesheets using the &ldquo;image_tag&rdquo;, &ldquo;asset_path&rdquo; helpers, it&rsquo;s just automatic. Just do it already.</p>
<p>And while we&rsquo;re on the subject of assets, if you happen to have image uploading and you&rsquo;re using the simple way of posting a multipart form directly to your Rails app, you will have trouble because Heroku imposes - a correct - maximum of 30 seconds of timeout. If you have many users uploading many high-resolution and heavy sized images, they will see increasing amounts of timeouts.</p>
<p>Use the <a href="http://cloudinary.com/documentation/rails_additional_topics"target="_blank" rel="noopener">Attachinary gem</a> to direct upload via Javascript, from the browser, to the Cloudinary service, completely bypassing your Rails app, which will just receive the ID once the upload finishes. Again, there is a very generous free plan and it&rsquo;s so simple to implement that you must do it right away.</p>
<h3>You laid off your developer, what to do? TL;DR: revoke access!<span class="hx:absolute hx:-mt-20" id="you-laid-off-your-developer-what-to-do-tldr-revoke-access"></span>
    <a href="#you-laid-off-your-developer-what-to-do-tldr-revoke-access" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>This is something that happens a lot: a small company needs to cut costs and lay off developers.</p>
<p>Make sure you revoke access. In AWS it&rsquo;s a lot more convoluted if you don&rsquo;t know your way through their IAM authorization tool. Or worse: if they have the private keys (.pem files) to your EC2 instances!! You will have to create new keypairs, deactivate the old EC2 instances and create new ones. A royal PITA.</p>
<p>You must change passwords to your DNS service, Github/Bitbucket organization, remove SSH keys from the .ssh/authorized_keys files in all your servers, etc.</p>
<p>Make sure your company has good legal support and make every collaborator, employee or 3rd party, sign a non-disclosure agreement, non-compete or confidentiality agreements.</p>
<p>You <strong>MUST</strong> have at least one key person in the company that has control over the keys, passwords and access to every service your company depends on to run. Otherwise don&rsquo;t be surprised when your app goes offline.</p>
<p>And don&rsquo;t fool yourself into thinking that just because an app is online and running it will keep that way for long. Security vulnerabilities are in the wild, it&rsquo;s just a matter of time until some glitch puts your service down and you better have someone to put it back online for you if you don&rsquo;t know how to do it.</p>
<h3>Big Site? Congratulations, what to do now?<span class="hx:absolute hx:-mt-20" id="big-site-congratulations-what-to-do-now"></span>
    <a href="#big-site-congratulations-what-to-do-now" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>If your business is thriving, blooming, congratulations. You&rsquo;re in the very very small percentage that actually made it.</p>
<p>Soon your brand new app will become that ugly, dreaded <strong>Legacy</strong> system. Worse, it&rsquo;s probably one of those feared <strong>monoliths</strong> or even worse, you let a hipster developer create a spaghetti architecture full of unchecked <strong>microservices</strong>. You&rsquo;re doomed either way.</p>
<p>This situation requires you to be grounded: there is no silver bullet, just business as usual.</p>
<p>You must keep ahead of the game, and the monitoring part I mentioned plays a big part to steer you through the many possible directions you can go. For example, for a short period of time you can increase the plans of your SaaS services such as databases, caches, queues, and also add more parallel web dynos.</p>
<p>While that will hold for a while (your metrics can help you estimate for how long), now you need to come up with a plan to optimize what you have. And no, rewriting everything is the very last thing you want to do, unless you intend to not have a life for the next couple of years.</p>
<p>Trust Pareto: in real life, 80% of the main problems can be solved with 20% of the effort. The main big tip: open New Relic - now loaded with real production data - and check the top worst performers and start from there. The first one you solve (be it an ugly N+1 query to your database, slow web service integration), will give you a lot of room to breathe.</p>
<h3>Conclusion<span class="hx:absolute hx:-mt-20" id="conclusion"></span>
    <a href="#conclusion" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>What I just said is nothing but the tip of the iceberg in terms of Rails performance optimization. And I can assure you that there is a lot of room to improve without having to resort to rewriting everything in the next hip language out there. I recommend you buy Nate Berkopec&rsquo;s <a href="https://www.railsspeed.com/"target="_blank" rel="noopener">&ldquo;The Complete Guide to Rails Performance&rdquo;</a> to really learn your turf.</p>
<p>This is a very quick compilation that I just dumped out of my mind, but there is much more that we can consider. Shoot questions in the comments section and other important items that I may have overlooked.</p>
]]></content:encoded><category>tutorials</category><category>devops</category><category>rails</category></item><item><title>Ex Pusher Lite - Part 2 - First Working Core!</title><link>https://akitaonrails.github.io/en/2015/12/14/ex-pusher-lite-part-2-first-working-core/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2015/12/14/ex-pusher-lite-part-2-first-working-core/</guid><pubDate>Mon, 14 Dec 2015 15:04:00 -0200</pubDate><description>&lt;p&gt;In &lt;a href="http://www.akitaonrails.com/en/2015/12/09/ex-pusher-lite-parte-1-phoenix-channels-e-aplicacoes-rails"target="_blank" rel="noopener"&gt;Part 1&lt;/a&gt; I basically started with &lt;a href="https://labs.opendoor.com/phoenix-on-rails-for-client-push-notifications"target="_blank" rel="noopener"&gt;Daniel Neighman&amp;rsquo;s&lt;/a&gt; tutorial.&lt;/p&gt;
&lt;p&gt;In Part 2 I will add the proper mechanisms to make a minimal core that is actually useful and deploy it to Heroku. In order to do that I need to implement the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a simple Administration authentication (a hardcoded admin_username and admin_password will do for now)&lt;/li&gt;
&lt;li&gt;an admin restricted &lt;strong&gt;&amp;quot;/api/admin/apps&amp;quot;&lt;/strong&gt; endpoint to manage new Applications. Each Application should have a randomly generated key and secret.&lt;/li&gt;
&lt;li&gt;the existing &amp;ldquo;/events&amp;rdquo; endpoint from Part 1 should be moved to &lt;strong&gt;&amp;quot;/api/apps/:app_id/events&amp;quot;&lt;/strong&gt; and have access restricted to the authentication of the key and secret of the Application identified as &amp;ldquo;app_id&amp;rdquo;.&lt;/li&gt;
&lt;li&gt;for now, the Event will just broadcast to the appropriate topic. We want to be able to broadcast to everyone from an Application as well to specific topics within the Application scope.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;As usual, the code for this section will be tagged as &lt;strong&gt;v0.2&lt;/strong&gt; in both the &lt;a href="https://github.com/akitaonrails/pusher_lite_demo/tree/v0.2"target="_blank" rel="noopener"&gt;client demo&lt;/a&gt; and &lt;a href="https://github.com/akitaonrails/ex_pusher_lite/tree/v0.2"target="_blank" rel="noopener"&gt;server-side&lt;/a&gt; Github repositories.&lt;/p&gt;</description><content:encoded><![CDATA[<p>In <a href="http://www.akitaonrails.com/en/2015/12/09/ex-pusher-lite-parte-1-phoenix-channels-e-aplicacoes-rails"target="_blank" rel="noopener">Part 1</a> I basically started with <a href="https://labs.opendoor.com/phoenix-on-rails-for-client-push-notifications"target="_blank" rel="noopener">Daniel Neighman&rsquo;s</a> tutorial.</p>
<p>In Part 2 I will add the proper mechanisms to make a minimal core that is actually useful and deploy it to Heroku. In order to do that I need to implement the following:</p>
<ul>
<li>a simple Administration authentication (a hardcoded admin_username and admin_password will do for now)</li>
<li>an admin restricted <strong>&quot;/api/admin/apps&quot;</strong> endpoint to manage new Applications. Each Application should have a randomly generated key and secret.</li>
<li>the existing &ldquo;/events&rdquo; endpoint from Part 1 should be moved to <strong>&quot;/api/apps/:app_id/events&quot;</strong> and have access restricted to the authentication of the key and secret of the Application identified as &ldquo;app_id&rdquo;.</li>
<li>for now, the Event will just broadcast to the appropriate topic. We want to be able to broadcast to everyone from an Application as well to specific topics within the Application scope.</li>
</ul>
<p>As usual, the code for this section will be tagged as <strong>v0.2</strong> in both the <a href="https://github.com/akitaonrails/pusher_lite_demo/tree/v0.2"target="_blank" rel="noopener">client demo</a> and <a href="https://github.com/akitaonrails/ex_pusher_lite/tree/v0.2"target="_blank" rel="noopener">server-side</a> Github repositories.</p>
<h3>The App Resource<span class="hx:absolute hx:-mt-20" id="the-app-resource"></span>
    <a href="#the-app-resource" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>If this project of ours is to behave like Pusher.com, we need a way to create new &ldquo;Applications&rdquo;. Each client connecting to this service will be bound to this Application. Events should be restricted to the Application boundary. This is how we will isolate different clients connecting to the same server. So you can have one core serving several different web applications.</p>
<p>Once a new application is created, the client/consumer web app will have the pair of key and secret tokens that it will use to connect both the server-side triggers as well as the client-side Websocket listeners.</p>
<p>As a disclaimer, at this stage of development I will not implement any sophisticated authentication system such as OAuth2 or JWT. I will save this for posts to follow. For now I will use the Application&rsquo;s key and secret just as simple username and password in an HTTP Basic Auth. This should be good enough for our purposes for the time being.</p>
<p>So, the very first step is to create such an &ldquo;Application&rdquo; resource and we can resort to Phoenix&rsquo;s built-in JSON scaffold generator:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>mix phoenix.gen.json App apps name:string slug:string key:string secret:string active:boolean</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>Most tutorials will show you the &ldquo;<tt>phoenix.gen.html</tt>&rdquo; generator, which behaves like Rails&rsquo;s &ldquo;scaffold&rdquo;, generating HTML templates for each of the CRUD verbs. This is similar but it skips HTML and assumes this is going to be a JSON CRUD API.</p>
<p>We need to manually update the &ldquo;<tt>web/router.ex</tt>&rdquo; file like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># web/router.ex</span>
</span></span><span class="line"><span class="cl"><span class="n">scope</span> <span class="s2">&#34;/api&#34;</span><span class="p">,</span> <span class="no">ExPusherLite</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">pipe_through</span> <span class="ss">:api</span>
</span></span><span class="line"><span class="cl">  <span class="n">post</span> <span class="s2">&#34;/apps/:app_slug/events&#34;</span><span class="p">,</span> <span class="no">EventsController</span><span class="p">,</span> <span class="ss">:create</span>
</span></span><span class="line"><span class="cl">  <span class="n">scope</span> <span class="s2">&#34;/admin&#34;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="n">resources</span> <span class="s2">&#34;/apps&#34;</span><span class="p">,</span> <span class="no">AppController</span><span class="p">,</span> <span class="ss">except</span><span class="p">:</span> <span class="o">[</span><span class="ss">:new</span><span class="p">,</span> <span class="ss">:edit</span><span class="o">]</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>The &ldquo;<tt>EventsController</tt>&rdquo; is the one we implemented in Part 1 and that we will overhaul during this Part 2.</p>
<p>The generator gave us this new &ldquo;<tt>AppController</tt>&rdquo;, and similarly to Rails&rsquo; routes, the DSL is remarkably similar here. If you&rsquo;re a Railer I bet you can instantly recognize the routes this DSL is generating.</p>
<p>The generator also created a proper migration for us:</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="c1"># priv/repo/migrations/20151210131528_create_app.exs</span>
</span></span><span class="line"><span class="cl"><span class="n">defmodule</span> <span class="no">ExPusherLite</span><span class="o">.</span><span class="n">Repo</span><span class="o">.</span><span class="n">Migrations</span><span class="o">.</span><span class="n">CreateApp</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">use</span> <span class="no">Ecto</span><span class="o">.</span><span class="n">Migration</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">change</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="n">create</span> <span class="n">table</span><span class="p">(</span><span class="ss">:apps</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">      <span class="n">add</span> <span class="ss">:name</span><span class="p">,</span> <span class="ss">:string</span>
</span></span><span class="line"><span class="cl">      <span class="n">add</span> <span class="ss">:slug</span><span class="p">,</span> <span class="ss">:string</span>
</span></span><span class="line"><span class="cl">      <span class="n">add</span> <span class="ss">:key</span><span class="p">,</span> <span class="ss">:string</span>
</span></span><span class="line"><span class="cl">      <span class="n">add</span> <span class="ss">:secret</span><span class="p">,</span> <span class="ss">:string</span>
</span></span><span class="line"><span class="cl">      <span class="n">add</span> <span class="ss">:active</span><span class="p">,</span> <span class="ss">:boolean</span><span class="p">,</span> <span class="ss">default</span><span class="p">:</span> <span class="kp">false</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">      <span class="n">timestamps</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">    <span class="n">create</span> <span class="n">index</span><span class="p">(</span><span class="ss">:apps</span><span class="p">,</span> <span class="o">[</span><span class="ss">:name</span><span class="o">]</span><span class="p">,</span> <span class="ss">unique</span><span class="p">:</span> <span class="kp">true</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">create</span> <span class="n">index</span><span class="p">(</span><span class="ss">:apps</span><span class="p">,</span> <span class="o">[</span><span class="ss">:slug</span><span class="o">]</span><span class="p">,</span> <span class="ss">unique</span><span class="p">:</span> <span class="kp">true</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Again, remarkably similar to ActiveRecord&rsquo;s Migration DSL. Migrations behave as you expect. You must run:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>mix ecto.create # if you haven&#39;t already
mix ecto.migrate</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>This App resource will need the ability to create slugs out of the names (which we will use as &ldquo;app_id&rdquo;) and also generate random key and secret values. So we must add these dependencies to the &ldquo;<tt>mix.exs</tt>&rdquo; file:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># mix.exs</span>
</span></span><span class="line"><span class="cl"><span class="n">defp</span> <span class="n">deps</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="o">[...</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">   <span class="p">{</span><span class="ss">:secure_random</span><span class="p">,</span> <span class="s2">&#34;~&gt; 0.2.0&#34;</span><span class="p">},</span>
</span></span><span class="line"><span class="cl">   <span class="p">{</span><span class="ss">:slugger</span><span class="p">,</span> <span class="s2">&#34;~&gt; 0.0.1&#34;</span><span class="p">}</span><span class="o">]</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>The <a href="https://github.com/akitaonrails/ex_pusher_lite/blob/v0.2/web/models/app.ex"target="_blank" rel="noopener">final App model</a> is quite long, so I will break it down for you:</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="c1"># web/models/app.ex</span>
</span></span><span class="line"><span class="cl"><span class="n">defmodule</span> <span class="no">ExPusherLite</span><span class="o">.</span><span class="n">App</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">use</span> <span class="no">ExPusherLite</span><span class="o">.</span><span class="n">Web</span><span class="p">,</span> <span class="ss">:model</span>
</span></span><span class="line"><span class="cl">  <span class="k">alias</span> <span class="no">ExPusherLite</span><span class="o">.</span><span class="n">Repo</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="n">schema</span> <span class="s2">&#34;apps&#34;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="n">field</span> <span class="ss">:name</span><span class="p">,</span> <span class="ss">:string</span>
</span></span><span class="line"><span class="cl">    <span class="n">field</span> <span class="ss">:slug</span><span class="p">,</span> <span class="ss">:string</span>
</span></span><span class="line"><span class="cl">    <span class="n">field</span> <span class="ss">:key</span><span class="p">,</span> <span class="ss">:string</span>
</span></span><span class="line"><span class="cl">    <span class="n">field</span> <span class="ss">:secret</span><span class="p">,</span> <span class="ss">:string</span>
</span></span><span class="line"><span class="cl">    <span class="n">field</span> <span class="ss">:active</span><span class="p">,</span> <span class="ss">:boolean</span><span class="p">,</span> <span class="ss">default</span><span class="p">:</span> <span class="kp">true</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">timestamps</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="vi">@required_fields</span> <span class="o">~</span><span class="n">w</span><span class="p">(</span><span class="nb">name</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="vi">@optional_fields</span> <span class="o">~</span><span class="n">w</span><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>This block declares the model Schema. Be careful if you generate a migration and then change its fields settings: you must remember to update the schema in the model. In my first attempt I didn&rsquo;t include the &ldquo;slug&rdquo; field, so I rolled back the database migration (with &ldquo;<tt>mix ecto.rollback</tt>&rdquo;), changed the migration to add the &ldquo;slug&rdquo; field and re-ran the &ldquo;<tt>ecto.migrate</tt>&rdquo; task.</p>
<p>I was puzzled with the model not picking up the new field; after some time I remembered that Ecto models don&rsquo;t attempt to fetch the real database schema and generate accessors dynamically, instead it relies on the explicitly declared schema block as shown above. After I added the new &ldquo;slug&rdquo; field in the schema block, then the model would properly use the new field.</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="c1"># web/models/app.ex</span>
</span></span><span class="line"><span class="cl"><span class="o">...</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">get_by_slug</span><span class="p">(</span><span class="n">slug</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="no">Repo</span><span class="o">.</span><span class="n">get_by!</span><span class="p">(</span><span class="n">__MODULE__</span><span class="p">,</span> <span class="ss">slug</span><span class="p">:</span> <span class="n">slug</span><span class="p">,</span> <span class="ss">active</span><span class="p">:</span> <span class="kp">true</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">hashed_secret</span><span class="p">(</span><span class="n">model</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="no">Base</span><span class="o">.</span><span class="n">encode64</span><span class="p">(</span><span class="s2">&#34;</span><span class="si">#{</span><span class="n">model</span><span class="o">.</span><span class="n">key</span><span class="si">}</span><span class="s2">:</span><span class="si">#{</span><span class="n">model</span><span class="o">.</span><span class="n">secret</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">end</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>These are just helper functions to use in the AppController. The odd bit might be &ldquo;<strong>MODULE</strong>&rdquo; but this is just a shortcut for the atom representation of the current module, which is &ldquo;<tt>ExPusherLite.App</tt>&rdquo;. This is how you make a simple query to the model, it resembles Rails&rsquo; &ldquo;<tt>App.get_by(slug: slug, active: true)</tt>&rdquo;.</p>
<p>In Elixir convention, Ecto has functions with and without bangs (&quot;<tt>get_by!</tt>&quot; and &ldquo;<tt>get_by</tt>&rdquo;). If you want to catch an error you use the version without bangs and it will return either a &ldquo;<tt>{:ok, result}</tt>&rdquo; tuple or a &ldquo;<tt>{:error, result}</tt>&rdquo; and you can pattern match them. Or you can use the bang version and it will raise an exception. Depends on what you want to 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-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># web/models/app.ex</span>
</span></span><span class="line"><span class="cl"><span class="o">...</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">changeset</span><span class="p">(</span><span class="n">model</span><span class="p">,</span> <span class="n">params</span> <span class="p">\\</span> <span class="ss">:empty</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">model</span>
</span></span><span class="line"><span class="cl">  <span class="o">|&gt;</span> <span class="n">cast</span><span class="p">(</span><span class="n">params</span><span class="p">,</span> <span class="vi">@required_fields</span><span class="p">,</span> <span class="vi">@optional_fields</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="o">|&gt;</span> <span class="n">validate_length</span><span class="p">(</span><span class="ss">:name</span><span class="p">,</span> <span class="ss">min</span><span class="p">:</span> <span class="mi">5</span><span class="p">,</span> <span class="ss">max</span><span class="p">:</span> <span class="mi">255</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="o">|&gt;</span> <span class="n">unique_constraint</span><span class="p">(</span><span class="ss">:name</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="o">|&gt;</span> <span class="n">generate_key</span>
</span></span><span class="line"><span class="cl">  <span class="o">|&gt;</span> <span class="n">generate_secret</span>
</span></span><span class="line"><span class="cl">  <span class="o">|&gt;</span> <span class="n">slugify</span>
</span></span><span class="line"><span class="cl">  <span class="o">|&gt;</span> <span class="n">unique_constraint</span><span class="p">(</span><span class="ss">:slug</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">end</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>Second only to the Schema block I mentioned above, this &ldquo;<tt>changeset/2</tt>&rdquo; function is the most important part of a Model.</p>
<p>In Rails you just have the concept of a &ldquo;Model&rdquo; which is considered &ldquo;Fat&rdquo; because it deals with database operations, business logic and framework hooks all in the same place. In Phoenix you have to deal with at least 3 different concepts:</p>
<ul>
<li>You have a <strong>Repo</strong>, which receives a Changeset and uses it to insert or update the designated rows in the database table. You will see &ldquo;<tt>Repo.get</tt>&rdquo; or &ldquo;<tt>Repo.insert</tt>&rdquo;, not &ldquo;<tt>App.find</tt>&rdquo; or &ldquo;<tt>App.save</tt>&rdquo;.</li>
<li>Then you have the <strong>Changeset</strong> which are just Elixir Maps (the ones with the syntax &ldquo;%<tt>{key =&gt; value}</tt>&rdquo;). It&rsquo;s just a Hash, a Dictionary, a collection of key-value pairs. A Repo accepts Maps for its operations.</li>
<li>Finally, you have the <strong>Model</strong> which actually give meaning and context to the raw Changesets. And the function &ldquo;<tt>changeset/2</tt>&rdquo; above is the one responsible for receiving a raw Map, piping it through a validation and transformation chain and return a valid Changeset for the Repo to use.</li>
</ul>
<p>So, in a controller you will usually find code like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># web/controllers/app_controller.ex</span>
</span></span><span class="line"><span class="cl"><span class="o">...</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">create</span><span class="p">(</span><span class="n">conn</span><span class="p">,</span> <span class="sx">%{&#34;app&#34; =&gt; app_params}</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">changeset</span> <span class="o">=</span> <span class="no">App</span><span class="o">.</span><span class="n">changeset</span><span class="p">(</span><span class="o">%</span><span class="no">App</span><span class="p">{},</span> <span class="n">app_params</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl">  <span class="k">case</span> <span class="no">Repo</span><span class="o">.</span><span class="n">insert</span><span class="p">(</span><span class="n">changeset</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span><span class="ss">:ok</span><span class="p">,</span> <span class="n">app</span><span class="p">}</span> <span class="o">-&gt;</span>
</span></span><span class="line"><span class="cl">      <span class="n">conn</span>
</span></span><span class="line"><span class="cl">      <span class="o">|&gt;</span> <span class="o">...</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span><span class="ss">:error</span><span class="p">,</span> <span class="n">changeset</span><span class="p">}</span> <span class="o">-&gt;</span>
</span></span><span class="line"><span class="cl">      <span class="n">conn</span>
</span></span><span class="line"><span class="cl">      <span class="o">|&gt;</span> <span class="o">...</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</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>This is how you create a new, validated, changeset and then pass it to the Repo, treating the results in a pattern match block. Just compare the above changeset line with the beginning of the &ldquo;changeset/2&rdquo; function:</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="o">...</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">changeset</span><span class="p">(</span><span class="n">model</span><span class="p">,</span> <span class="n">params</span> <span class="p">\\</span> <span class="ss">:empty</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">model</span>
</span></span><span class="line"><span class="cl">  <span class="o">|&gt;</span> <span class="n">cast</span><span class="p">(</span><span class="n">params</span><span class="p">,</span> <span class="vi">@required_fields</span><span class="p">,</span> <span class="vi">@optional_fields</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="o">|&gt;</span> <span class="n">validate_length</span><span class="p">(</span><span class="ss">:name</span><span class="p">,</span> <span class="ss">min</span><span class="p">:</span> <span class="mi">5</span><span class="p">,</span> <span class="ss">max</span><span class="p">:</span> <span class="mi">255</span><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>It maps the &ldquo;<tt>%App{}</tt>&rdquo; empty record to the &ldquo;model&rdquo; argument and the &ldquo;app_params&rdquo; that comes from the request (a map of the format &ldquo;<tt>%{name =&gt; &lsquo;foo&rsquo;, active: =&gt; &rsquo;true&rsquo;}</tt>&rdquo;) to the argument &ldquo;params&rdquo;. Then it pipes the model and params to the &ldquo;<tt>cast/4</tt>&rdquo; function which will copy the values from the params map to the model map/changeset. And it keep passing the resulting changeset to the following functions, such as &ldquo;<tt>validated_length/3</tt>&rdquo; below, and so on. If the chain ends with no exceptions, you end up with a clean, validated changeset that you can just pass to the Repo to blindly insert to the database.</p>
<p>In the above implementation we are chaining filters to generate the key, secret and slug, and this is the implementation as private functions:</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="c1"># web/models/app.ex</span>
</span></span><span class="line"><span class="cl"><span class="o">...</span>
</span></span><span class="line"><span class="cl">  <span class="n">defp</span> <span class="n">generate_key</span><span class="p">(</span><span class="n">model</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">get_field</span><span class="p">(</span><span class="n">model</span><span class="p">,</span> <span class="ss">:key</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">      <span class="n">model</span>
</span></span><span class="line"><span class="cl">    <span class="k">else</span>
</span></span><span class="line"><span class="cl">      <span class="n">model</span>
</span></span><span class="line"><span class="cl">      <span class="o">|&gt;</span> <span class="n">put_change</span><span class="p">(</span><span class="ss">:key</span><span class="p">,</span> <span class="no">SecureRandom</span><span class="o">.</span><span class="n">uuid</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="n">defp</span> <span class="n">generate_secret</span><span class="p">(</span><span class="n">model</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">get_field</span><span class="p">(</span><span class="n">model</span><span class="p">,</span> <span class="ss">:secret</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">      <span class="n">model</span>
</span></span><span class="line"><span class="cl">    <span class="k">else</span>
</span></span><span class="line"><span class="cl">      <span class="n">model</span>
</span></span><span class="line"><span class="cl">      <span class="o">|&gt;</span> <span class="n">put_change</span><span class="p">(</span><span class="ss">:secret</span><span class="p">,</span> <span class="no">SecureRandom</span><span class="o">.</span><span class="n">uuid</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="n">defp</span> <span class="n">slugify</span><span class="p">(</span><span class="n">model</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="nb">name</span> <span class="o">=</span> <span class="n">get_change</span><span class="p">(</span><span class="n">model</span><span class="p">,</span> <span class="ss">:name</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">      <span class="n">model</span>
</span></span><span class="line"><span class="cl">      <span class="o">|&gt;</span> <span class="n">put_change</span><span class="p">(</span><span class="ss">:slug</span><span class="p">,</span> <span class="no">Slugger</span><span class="o">.</span><span class="n">slugify_downcase</span><span class="p">(</span><span class="nb">name</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">    <span class="k">else</span>
</span></span><span class="line"><span class="cl">      <span class="n">model</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>The logic is set so new key/secret are generated only if the fields are empty and a new slug is generated only if the name has changes. And this is it, I told you the model code would be a bit large. You can see how to use the Slugger and SecureRandom libraries we added in the &ldquo;mix.exs&rdquo; before.</p>
<p>I also want to add the equivalent of a Rails seed file to create a test application so it&rsquo;s easier for newcomers to know what to do. Phoenix has seeds and you can implement it like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># priv/repo/seeds.exs</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">alias</span> <span class="no">ExPusherLite</span><span class="o">.</span><span class="n">App</span>
</span></span><span class="line"><span class="cl"><span class="k">alias</span> <span class="no">ExPusherLite</span><span class="o">.</span><span class="n">Repo</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># not using the App.changeset should just avoid all validations and generations</span>
</span></span><span class="line"><span class="cl"><span class="no">Repo</span><span class="o">.</span><span class="n">insert!</span> <span class="o">%</span><span class="no">App</span><span class="p">{</span> <span class="ss">slug</span><span class="p">:</span> <span class="s2">&#34;test-app&#34;</span><span class="p">,</span> <span class="nb">name</span><span class="p">:</span> <span class="s2">&#34;Test App&#34;</span><span class="p">,</span> <span class="ss">key</span><span class="p">:</span> <span class="s2">&#34;test-app-fake-key&#34;</span><span class="p">,</span> <span class="ss">secret</span><span class="p">:</span> <span class="s2">&#34;test-app-fake-secret&#34;</span><span class="p">,</span> <span class="ss">active</span><span class="p">:</span> <span class="kp">true</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>Remember how I detailed the role of the &ldquo;changeset/2&rdquo; function in creating a clean and validated changeset, which is just a Map? You can skip that function altogether and hand craft your own final Map and pass it to the Repo. The Repo doesn&rsquo;t care if this is a valid Map or not it will just try to insert it into the database regardless. And in this case the App Model prevents us from hardcoding keys and secrets, so this is how we do it in a seed file.</p>
<p>We can just run it directly like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>mix run priv/repo/seeds.exs</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>The AppController just needs 2 changes. The first is to search the App through the slug field instead of the default &lsquo;id&rsquo; field. This is simple enough, we just replace all calls to &ldquo;<tt>app = Repo.get!(App, id)</tt>&rdquo; to &ldquo;<tt>app = App.get_by_slug(id)</tt>&rdquo;, which is why we implemented this function in the model above.</p>
<p>The second thing is Authentication.</p>
<h3>Adding Authentication<span class="hx:absolute hx:-mt-20" id="adding-authentication"></span>
    <a href="#adding-authentication" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Now that we have an App model that can generate secure random UUIDs for key and secret, I will add a second level of authentication for administrators to be able to create such Apps.</p>
<p>For that I will just hard-code a secret in the config file of the application itself to serve as a development default. Like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># config/config.exs</span>
</span></span><span class="line"><span class="cl"><span class="o">...</span>
</span></span><span class="line"><span class="cl"><span class="n">config</span> <span class="ss">:ex_pusher_lite</span><span class="p">,</span> <span class="ss">:admin_authentication</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="ss">username</span><span class="p">:</span> <span class="s2">&#34;pusher_admin_username&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="ss">password</span><span class="p">:</span> <span class="s2">&#34;pusher_admin_password&#34;</span>
</span></span><span class="line"><span class="cl"><span class="o">...</span>
</span></span><span class="line"><span class="cl"><span class="n">import_config</span> <span class="s2">&#34;</span><span class="si">#{</span><span class="no">Mix</span><span class="o">.</span><span class="n">env</span><span class="si">}</span><span class="s2">.exs&#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>You must add this block before the &ldquo;<tt>import_config</tt>&rdquo; function. Then you can override those values in the &ldquo;config/prod.secret.exs&rdquo; file, for example, like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># config/prod.secret.exs</span>
</span></span><span class="line"><span class="cl"><span class="o">...</span>
</span></span><span class="line"><span class="cl"><span class="n">config</span> <span class="ss">:ex_pusher_lite</span><span class="p">,</span> <span class="ss">:admin_authentication</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="ss">username</span><span class="p">:</span> <span class="s2">&#34;14e86e5fee3335fa88b0&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="ss">password</span><span class="p">:</span> <span class="s2">&#34;2b94ff0f07ce9769567f&#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>Of course, generate your own pair of secure username and password and replace it in the production environment if you intend to actually use this. For Heroku, we will still have to tweak this further, so keep this in mind.</p>
<p>Just to make the process easier, I also added the following helper function:</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="c1"># lib/ex_pusher_lite.ex</span>
</span></span><span class="line"><span class="cl"><span class="o">...</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># Return this applicaton administration Basic HTTP Auth hash</span>
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">admin_secret</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="n">admin_username</span> <span class="o">=</span> <span class="no">Application</span><span class="o">.</span><span class="n">get_env</span><span class="p">(</span><span class="ss">:ex_pusher_lite</span><span class="p">,</span> <span class="ss">:admin_authentication</span><span class="p">)</span><span class="o">[</span><span class="ss">:username</span><span class="o">]</span>
</span></span><span class="line"><span class="cl">    <span class="n">admin_password</span> <span class="o">=</span> <span class="no">Application</span><span class="o">.</span><span class="n">get_env</span><span class="p">(</span><span class="ss">:ex_pusher_lite</span><span class="p">,</span> <span class="ss">:admin_authentication</span><span class="p">)</span><span class="o">[</span><span class="ss">:password</span><span class="o">]</span>
</span></span><span class="line"><span class="cl">    <span class="n">secret</span> <span class="o">=</span> <span class="no">Base</span><span class="o">.</span><span class="n">encode64</span><span class="p">(</span><span class="s2">&#34;</span><span class="si">#{</span><span class="n">admin_username</span><span class="si">}</span><span class="s2">:</span><span class="si">#{</span><span class="n">admin_password</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>This is how you fetch the configuration values. I am generating a simple Base64 encoded string out of the username concatenated with the password with a comma, which is what Basic HTTP Auth requires. I will use this admin hash for the &ldquo;AppController&rdquo; and each client must provide the key/secret in its own App instance to be able to trigger the &ldquo;EventsController&rdquo;.</p>
<p>For both controllers I will create a single Authentication Plug, like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># lib/ex_pusher_lite/authentication.ex</span>
</span></span><span class="line"><span class="cl"><span class="n">defmodule</span> <span class="no">ExPusherLite</span><span class="o">.</span><span class="n">Authentication</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">import</span> <span class="no">Plug</span><span class="o">.</span><span class="n">Conn</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">alias</span> <span class="no">ExPusherLite</span><span class="o">.</span><span class="n">App</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">init</span><span class="p">(</span><span class="n">assigns</span> <span class="p">\\</span> <span class="o">[</span><span class="ss">admin</span><span class="p">:</span> <span class="kp">false</span><span class="o">]</span><span class="p">),</span> <span class="k">do</span><span class="p">:</span> <span class="n">assigns</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">call</span><span class="p">(</span><span class="n">conn</span><span class="p">,</span> <span class="n">assigns</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="n">token</span> <span class="o">=</span>
</span></span><span class="line"><span class="cl">      <span class="k">if</span> <span class="n">assigns</span><span class="o">[</span><span class="ss">:admin</span><span class="o">]</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">        <span class="no">ExPusherLite</span><span class="o">.</span><span class="n">admin_secret</span>
</span></span><span class="line"><span class="cl">      <span class="k">else</span>
</span></span><span class="line"><span class="cl">        <span class="n">params</span> <span class="o">=</span> <span class="n">fetch_query_params</span><span class="p">(</span><span class="n">conn</span><span class="p">)</span><span class="o">.</span><span class="n">params</span>
</span></span><span class="line"><span class="cl">        <span class="n">params</span><span class="o">[</span><span class="s2">&#34;app_slug&#34;</span><span class="o">]</span> <span class="o">|&gt;</span> <span class="no">App</span><span class="o">.</span><span class="n">get_by_slug</span> <span class="o">|&gt;</span> <span class="no">App</span><span class="o">.</span><span class="n">hashed_secret</span>
</span></span><span class="line"><span class="cl">      <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;Basic &#34;</span> <span class="o">&lt;&gt;</span> <span class="n">auth_token</span> <span class="o">=</span> <span class="n">hd</span><span class="p">(</span><span class="n">get_req_header</span><span class="p">(</span><span class="n">conn</span><span class="p">,</span> <span class="s2">&#34;authorization&#34;</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="no">Plug</span><span class="o">.</span><span class="n">Crypto</span><span class="o">.</span><span class="n">secure_compare</span><span class="p">(</span><span class="n">auth_token</span><span class="p">,</span> <span class="n">token</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">      <span class="n">conn</span>
</span></span><span class="line"><span class="cl">    <span class="k">else</span>
</span></span><span class="line"><span class="cl">      <span class="n">conn</span> <span class="o">|&gt;</span> <span class="n">send_resp</span><span class="p">(</span><span class="mi">401</span><span class="p">,</span> <span class="s2">&#34;&#34;</span><span class="p">)</span> <span class="o">|&gt;</span> <span class="n">halt</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span> </span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>As I explained in previous articles, a Plug is like a chainable Rails Middleware or even a Rack application. It must have a single &ldquo;<tt>call/2</tt>&rdquo; that receives a Plug.Conn structure and returns it back, allowing to form a chain/pipeline of Plugs.</p>
<p>We check if we want to compare with the Admin token or the App token and then retrieve the Basic HTTP authorization token that&rsquo;s in the HTTP request connection structure (we retrieve individual header values through the &ldquo;<tt>get_req_header/2</tt>&rdquo; function). Finally we make a secure compare between the tokens.</p>
<p>To enable this plug in the controllers we just add it like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># web/controllers/app_controller.ex</span>
</span></span><span class="line"><span class="cl"><span class="n">defmodule</span> <span class="no">ExPusherLite</span><span class="o">.</span><span class="n">AppController</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">use</span> <span class="no">ExPusherLite</span><span class="o">.</span><span class="n">Web</span><span class="p">,</span> <span class="ss">:controller</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">alias</span> <span class="no">ExPusherLite</span><span class="o">.</span><span class="n">App</span>
</span></span><span class="line"><span class="cl">  <span class="n">plug</span> <span class="no">ExPusherLite</span><span class="o">.</span><span class="n">Authentication</span><span class="p">,</span> <span class="o">[</span><span class="ss">admin</span><span class="p">:</span> <span class="kp">true</span><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>
<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">defmodule</span> <span class="no">ExPusherLite</span><span class="o">.</span><span class="n">EventsController</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">   <span class="n">use</span> <span class="no">ExPusherLite</span><span class="o">.</span><span class="n">Web</span><span class="p">,</span> <span class="ss">:controller</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="o">-</span>  <span class="n">plug</span> <span class="ss">:authenticate</span>
</span></span><span class="line"><span class="cl"><span class="o">+</span>  <span class="n">plug</span> <span class="no">ExPusherLite</span><span class="o">.</span><span class="n">Authentication</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>In Part 1 we had a simpler &ldquo;<tt>plug :authenticate</tt>&rdquo; in the EventsController. We can remove it and also the &ldquo;<tt>authenticate/2</tt>&rdquo; function. We just refactored it into a better function that also serves administration authentication now, but the idea is the same.</p>
<p>This is it: the basics for API authentication. Again, this is not the best solution as the username/password pair goes in the URL and it&rsquo;s open to man-in-the-middle attacks. SSL only encrypts the HTTP body but the URL is still open.</p>
<p>For example, if an administrator wants to create a new application, he must do the following:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>curl --data &#34;app[name]=foo-app&#34; http://pusher_admin_username:pusher_admin_password@localhost:4000/api/admin/apps</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>And this would be one example of the resulting JSON representation of the new app:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>{&#34;data&#34;:{&#34;slug&#34;:&#34;foo-app&#34;,&#34;secret&#34;:&#34;8ef69064-0d7e-c9ef-ac14-b6b1db303e7a&#34;,&#34;name&#34;:&#34;foo-app&#34;,&#34;key&#34;:&#34;9400ad21-eed8-117a-bce5-845262e0a09e&#34;,&#34;id&#34;:5,&#34;active&#34;: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>With this new key and secret in hand, we can update our client demo to make use of the new app.</p>
<h3>Configuring the Client Demo<span class="hx:absolute hx:-mt-20" id="configuring-the-client-demo"></span>
    <a href="#configuring-the-client-demo" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>We must start by adding the proper Application details in the &ldquo;<tt>.env</tt>&rdquo; file:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>PUSHER_URL: &#34;localhost:4000&#34;
PUSHER_APP_ID: &#34;foo-app&#34;
PUSHER_KEY: &#34;9400ad21-eed8-117a-bce5-845262e0a09e&#34;
PUSHER_SECRET: &#34;8ef69064-0d7e-c9ef-ac14-b6b1db303e7a&#34;
PUSHER_CHANNEL: &#34;foo-topic&#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>We must also tweak the &ldquo;<tt>config/secrets.yml</tt>&rdquo; to reflect the new metadata (development, test, and production must follow this):</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>development:
  secret_key_base: ded7c4a2a298c1b620e462b50c9ca6ccb60130e27968357e76cab73de9858f14556a26df885c8aa5004d0a7ca79c0438e618557275bdb28ba67a0ffb0c268056
  pusher_url: &lt;%= ENV[&#39;PUSHER_URL&#39;] %&gt;
  pusher_app_id: &lt;%= ENV[&#39;PUSHER_APP_ID&#39;] %&gt;
  pusher_key: &lt;%= ENV[&#39;PUSHER_KEY&#39;] %&gt;
  pusher_secret: &lt;%= ENV[&#39;PUSHER_SECRET&#39;] %&gt;
  pusher_channel: &lt;%= ENV[&#39;PUSHER_CHANNEL&#39;] %&gt;
  ...</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>And we can create an initializer to make it easier to use this metadata properly:</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="c1"># config/initializers/pusher_lite.rb</span>
</span></span><span class="line"><span class="cl"><span class="k">module</span> <span class="nn">PusherLite</span>
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">uri</span>
</span></span><span class="line"><span class="cl">    <span class="n">key</span>    <span class="o">=</span> <span class="no">Rails</span><span class="o">.</span><span class="n">application</span><span class="o">.</span><span class="n">secrets</span><span class="o">.</span><span class="n">pusher_key</span>
</span></span><span class="line"><span class="cl">    <span class="n">secret</span> <span class="o">=</span> <span class="no">Rails</span><span class="o">.</span><span class="n">application</span><span class="o">.</span><span class="n">secrets</span><span class="o">.</span><span class="n">pusher_secret</span>
</span></span><span class="line"><span class="cl">    <span class="n">app_id</span> <span class="o">=</span> <span class="no">Rails</span><span class="o">.</span><span class="n">application</span><span class="o">.</span><span class="n">secrets</span><span class="o">.</span><span class="n">pusher_app_id</span>
</span></span><span class="line"><span class="cl">    <span class="n">url</span>    <span class="o">=</span> <span class="no">Rails</span><span class="o">.</span><span class="n">application</span><span class="o">.</span><span class="n">secrets</span><span class="o">.</span><span class="n">pusher_url</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">uri</span> <span class="o">=</span> <span class="s2">&#34;http://</span><span class="si">#{</span><span class="n">key</span><span class="si">}</span><span class="s2">:</span><span class="si">#{</span><span class="n">secret</span><span class="si">}</span><span class="s2">@</span><span class="si">#{</span><span class="n">url</span><span class="si">}</span><span class="s2">/api/apps/</span><span class="si">#{</span><span class="n">app_id</span><span class="si">}</span><span class="s2">/events&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="no">URI</span><span class="o">.</span><span class="n">parse</span><span class="p">(</span><span class="n">uri</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span> </span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Again, the Rails app will trigger the ExPusherLite server using the Basic HTTP Auth. Do not be fooled into thinking this is &ldquo;secure&rdquo;, it just &ldquo;feels a bit secure through obscurity&rdquo;. You have been warned, wait for the next articles on this subject. But this is usable in controlled environments.</p>
<p>To finalize the upgrades, we must change the client-side access to the new metadata, first changing the application layout:</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-html" data-lang="html"><span class="line"><span class="cl"><span class="c">&lt;!-- app/views/layouts/application.html.erb --&gt;</span>
</span></span><span class="line"><span class="cl">...
</span></span><span class="line"><span class="cl">+  <span class="p">&lt;</span><span class="nt">meta</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;pusher_host&#34;</span> <span class="na">content</span><span class="o">=</span><span class="s">&#34;&lt;%= Rails.application.secrets.pusher_url %&gt;&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">-  <span class="p">&lt;</span><span class="nt">meta</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;pusher_key&#34;</span> <span class="na">content</span><span class="o">=</span><span class="s">&#34;&lt;%= Rails.application.secrets.pusher_key %&gt;&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">+  <span class="p">&lt;</span><span class="nt">meta</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;pusher_app_id&#34;</span> <span class="na">content</span><span class="o">=</span><span class="s">&#34;&lt;%= Rails.application.secrets.pusher_app_id %&gt;&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">   <span class="p">&lt;</span><span class="nt">meta</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;pusher_channel&#34;</span> <span class="na">content</span><span class="o">=</span><span class="s">&#34;&lt;%= Rails.application.secrets.pusher_channel %&gt;&#34;</span><span class="p">&gt;</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>The javascript &ldquo;<tt>index.es6</tt>&rdquo; fetches from this meta headers, so we must change them there:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="err">#</span> <span class="nx">app</span><span class="o">/</span><span class="nx">assets</span><span class="o">/</span><span class="nx">javascripts</span><span class="o">/</span><span class="nx">application</span><span class="o">/</span><span class="nx">pages</span><span class="o">/</span><span class="nx">home</span><span class="o">/</span><span class="nx">index</span><span class="p">.</span><span class="nx">es6</span>
</span></span><span class="line"><span class="cl"><span class="p">...</span>
</span></span><span class="line"><span class="cl">     <span class="kd">let</span> <span class="nx">guardianToken</span> <span class="o">=</span> <span class="nx">$</span><span class="p">(</span><span class="s2">&#34;meta[name=guardian-token]&#34;</span><span class="p">).</span><span class="nx">attr</span><span class="p">(</span><span class="s2">&#34;content&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">     <span class="kd">let</span> <span class="nx">csrfToken</span>     <span class="o">=</span> <span class="nx">$</span><span class="p">(</span><span class="s2">&#34;meta[name=guardian-csrf]&#34;</span><span class="p">).</span><span class="nx">attr</span><span class="p">(</span><span class="s2">&#34;content&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> 
</span></span><span class="line"><span class="cl"><span class="o">+</span>    <span class="kd">let</span> <span class="nx">pusherHost</span>    <span class="o">=</span> <span class="nx">$</span><span class="p">(</span><span class="s2">&#34;meta[name=pusher_host]&#34;</span><span class="p">).</span><span class="nx">attr</span><span class="p">(</span><span class="s2">&#34;content&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="o">-</span>    <span class="kd">let</span> <span class="nx">pusherKey</span>     <span class="o">=</span> <span class="nx">$</span><span class="p">(</span><span class="s2">&#34;meta[name=pusher_key]&#34;</span><span class="p">).</span><span class="nx">attr</span><span class="p">(</span><span class="s2">&#34;content&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="o">+</span>    <span class="kd">let</span> <span class="nx">pusherApp</span>     <span class="o">=</span> <span class="nx">$</span><span class="p">(</span><span class="s2">&#34;meta[name=pusher_app_id]&#34;</span><span class="p">).</span><span class="nx">attr</span><span class="p">(</span><span class="s2">&#34;content&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">     <span class="kd">let</span> <span class="nx">pusherChannel</span> <span class="o">=</span> <span class="nx">$</span><span class="p">(</span><span class="s2">&#34;meta[name=pusher_channel]&#34;</span><span class="p">).</span><span class="nx">attr</span><span class="p">(</span><span class="s2">&#34;content&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> 
</span></span><span class="line"><span class="cl"><span class="o">-</span>    <span class="kd">let</span> <span class="nx">socket</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Socket</span><span class="p">(</span><span class="s2">&#34;ws://localhost:4000/socket&#34;</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"><span class="o">+</span>    <span class="kd">let</span> <span class="nx">socket</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Socket</span><span class="p">(</span><span class="sb">`ws://</span><span class="si">${</span><span class="nx">pusherHost</span><span class="si">}</span><span class="sb">/socket`</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">       <span class="nx">params</span><span class="o">:</span> <span class="p">{</span> <span class="nx">guardian_token</span><span class="o">:</span> <span class="nx">guardianToken</span><span class="p">,</span> <span class="nx">csrf_token</span><span class="o">:</span> <span class="nx">csrfToken</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="nx">socket</span><span class="p">.</span><span class="nx">connect</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"> 
</span></span><span class="line"><span class="cl">     <span class="c1">// Now that you are connected, you can join channels with a topic:
</span></span></span><span class="line"><span class="cl"><span class="o">-</span>    <span class="kd">let</span> <span class="nx">channel</span> <span class="o">=</span> <span class="nx">socket</span><span class="p">.</span><span class="nx">channel</span><span class="p">(</span><span class="nx">pusherChannel</span><span class="p">,</span> <span class="p">{})</span>
</span></span><span class="line"><span class="cl"><span class="o">+</span>    <span class="kd">let</span> <span class="nx">channel</span> <span class="o">=</span> <span class="nx">socket</span><span class="p">.</span><span class="nx">channel</span><span class="p">(</span><span class="sb">`public:</span><span class="si">${</span><span class="nx">pusherApp</span><span class="si">}</span><span class="sb">`</span><span class="p">,</span> <span class="p">{})</span>
</span></span><span class="line"><span class="cl">     <span class="nx">channel</span><span class="p">.</span><span class="nx">join</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">       <span class="p">.</span><span class="nx">receive</span><span class="p">(</span><span class="s2">&#34;ok&#34;</span><span class="p">,</span> <span class="nx">resp</span> <span class="p">=&gt;</span> <span class="p">{</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">&#34;Joined successfully&#34;</span><span class="p">,</span> <span class="nx">resp</span><span class="p">)</span> <span class="p">})</span>
</span></span><span class="line"><span class="cl">       <span class="p">.</span><span class="nx">receive</span><span class="p">(</span><span class="s2">&#34;error&#34;</span><span class="p">,</span> <span class="nx">resp</span> <span class="p">=&gt;</span> <span class="p">{</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">&#34;Unable to join&#34;</span><span class="p">,</span> <span class="nx">resp</span><span class="p">)</span> <span class="p">})</span>
</span></span><span class="line"><span class="cl"> 
</span></span><span class="line"><span class="cl"><span class="o">-</span>    <span class="nx">channel</span><span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="s2">&#34;msg&#34;</span><span class="p">,</span> <span class="nx">data</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"><span class="o">+</span>    <span class="nx">channel</span><span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="sb">`</span><span class="si">${</span><span class="nx">pusherChannel</span><span class="si">}</span><span class="sb">:msg`</span><span class="p">,</span> <span class="nx">data</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">       <span class="kd">let</span> <span class="nx">new_line</span> <span class="o">=</span> <span class="sb">`&lt;p&gt;&lt;strong&gt;</span><span class="si">${</span><span class="nx">data</span><span class="p">.</span><span class="nx">name</span><span class="si">}</span><span class="sb">&lt;strong&gt;: </span><span class="si">${</span><span class="nx">data</span><span class="p">.</span><span class="nx">message</span><span class="si">}</span><span class="sb">&lt;/p&gt;`</span>
</span></span><span class="line"><span class="cl">       <span class="nx">$</span><span class="p">(</span><span class="s2">&#34;.message-receiver&#34;</span><span class="p">).</span><span class="nx">append</span><span class="p">(</span><span class="nx">new_line</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">+</span>
</span></span><span class="line"><span class="cl"><span class="o">+</span>    <span class="nx">channel</span><span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="s2">&#34;msg&#34;</span><span class="p">,</span> <span class="nx">data</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"><span class="o">+</span>      <span class="kd">let</span> <span class="nx">new_line</span> <span class="o">=</span> <span class="sb">`&lt;p&gt;&lt;strong&gt;Broadcast to all channels&lt;/strong&gt;: </span><span class="si">${</span><span class="nx">data</span><span class="p">.</span><span class="nx">message</span><span class="si">}</span><span class="sb">&lt;/p&gt;`</span>
</span></span><span class="line"><span class="cl"><span class="o">+</span>      <span class="nx">$</span><span class="p">(</span><span class="s2">&#34;.message-receiver&#34;</span><span class="p">).</span><span class="nx">append</span><span class="p">(</span><span class="nx">new_line</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="o">+</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>One important modification from Part 1 is that the WebSocket host was hardcoded to &ldquo;localhost&rdquo; and here we are making it configurable through the meta tags. Right now, for localhost tests, we are using the plain &ldquo;ws://&rdquo; protocol but when we deploy to Heroku we will change it to &ldquo;wss://&rdquo; for SSL. Same thing for the &ldquo;PusherLite&rdquo; initializer. Keep that in mind.</p>
<p>Now it&rsquo;s subscribing to a different format of topic/channel. In Part 1 it would be something like: &ldquo;<tt>public:test_channel</tt>&rdquo; now we are listening to &ldquo;<tt>public:foo-app</tt>&rdquo;, so the application is the Websocket subscription &ldquo;topic&rdquo;.</p>
<p>Then we are changing the socket listener to listen for 2 different events. The first one is in the format &ldquo;<tt>test_channel:msg</tt>&rdquo;. So this is how we must now send messages to a specific &ldquo;channel&rdquo; within an &ldquo;app/topic&rdquo;.</p>
<p>And last we still listen to the old &ldquo;msg&rdquo; event, but this serves as a &ldquo;broadcast&rdquo; event for all connected clients subscribed to this particular &ldquo;foo-app&rdquo; Application. Now web clients can listen to specific &ldquo;channels&rdquo; within the &ldquo;app&rdquo; but also receive system wide &ldquo;broadcast&rdquo; messages. This is a big improvement and it didn&rsquo;t require much on the Javascript side.</p>
<p>But what more does it take to make this <strong>&ldquo;channel-only and broadcast&rdquo;</strong> system work? First, we start changing the web form to allow a user to choose between sending a channel-only message or a broadcast, like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl"><span class="c">&lt;!-- app/views/home/index.html.erb --&gt;</span>
</span></span><span class="line"><span class="cl">...
</span></span><span class="line"><span class="cl">     <span class="err">&lt;</span>%= f.text_field :name, placeholder: &#34;Name&#34; %&gt;
</span></span><span class="line"><span class="cl">     <span class="err">&lt;</span>%= f.text_field :message, placeholder: &#34;Message&#34; %&gt;
</span></span><span class="line"><span class="cl">+    <span class="err">&lt;</span>%= f.check_box :broadcast %&gt;
</span></span><span class="line"><span class="cl">     <span class="err">&lt;</span>%= f.submit &#34;Send message&#34;, class: &#34;pure-button pure-button-primary&#34; %&gt;
</span></span><span class="line"><span class="cl">   <span class="p">&lt;/</span><span class="nt">fieldset</span><span class="p">&gt;</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>Now the EventsController must accept this new parameter:</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="c1"># app/controllers/events_controller.rb</span>
</span></span><span class="line"><span class="cl"><span class="o">...</span>
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">event_params</span>
</span></span><span class="line"><span class="cl">    <span class="n">params</span><span class="o">.</span><span class="n">require</span><span class="p">(</span><span class="ss">:pusher_event</span><span class="p">)</span><span class="o">.</span><span class="n">permit</span><span class="p">(</span><span class="ss">:name</span><span class="p">,</span> <span class="ss">:message</span><span class="p">,</span> <span class="ss">:broadcast</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</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>Finally, the Model must use this new information before posting to the ExPusherLite server:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># app/models/pusher_event.rb</span>
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">PusherEvent</span>
</span></span><span class="line"><span class="cl">  <span class="kp">include</span> <span class="no">ActiveModel</span><span class="o">::</span><span class="no">Model</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="kp">attr_accessor</span> <span class="ss">:name</span><span class="p">,</span> <span class="ss">:message</span><span class="p">,</span> <span class="ss">:broadcast</span>
</span></span><span class="line"><span class="cl">  <span class="n">validates</span> <span class="ss">:name</span><span class="p">,</span> <span class="ss">:message</span><span class="p">,</span> <span class="ss">presence</span><span class="p">:</span> <span class="kp">true</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">save</span>
</span></span><span class="line"><span class="cl">    <span class="n">topic</span> <span class="o">=</span> <span class="k">if</span> <span class="n">broadcast</span> <span class="o">==</span> <span class="s2">&#34;1&#34;</span>
</span></span><span class="line"><span class="cl">              <span class="s2">&#34;#general&#34;</span>
</span></span><span class="line"><span class="cl">            <span class="k">else</span>
</span></span><span class="line"><span class="cl">              <span class="no">Rails</span><span class="o">.</span><span class="n">application</span><span class="o">.</span><span class="n">secrets</span><span class="o">.</span><span class="n">pusher_channel</span>
</span></span><span class="line"><span class="cl">            <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="no">Net</span><span class="o">::</span><span class="no">HTTP</span><span class="o">.</span><span class="n">post_form</span><span class="p">(</span><span class="no">PusherLite</span><span class="o">.</span><span class="n">uri</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;topic&#34;</span> <span class="o">=&gt;</span> <span class="n">topic</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;event&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;msg&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;scope&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;public&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;payload&#34;</span> <span class="o">=&gt;</span> <span class="p">{</span><span class="s2">&#34;name&#34;</span> <span class="o">=&gt;</span> <span class="nb">name</span><span class="p">,</span> <span class="s2">&#34;message&#34;</span> <span class="o">=&gt;</span> <span class="n">message</span><span class="p">}</span><span class="o">.</span><span class="n">to_json</span>
</span></span><span class="line"><span class="cl">    <span class="p">})</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</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>I am just assuming a hard-coded &ldquo;<tt>#general</tt>&rdquo; string to serve as the broadcast trigger for the server. Now we must make the server accept this new protocol schema, so let&rsquo;s go back to Elixir.</p>
<p>First we must start with the counterpart for the previous POST trigger, <tt>ExPusherLite.EventsController</tt>:</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="c1"># web/controllers/events_controller.ex</span>
</span></span><span class="line"><span class="cl"> <span class="n">defmodule</span> <span class="no">ExPusherLite</span><span class="o">.</span><span class="n">EventsController</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">   <span class="n">use</span> <span class="no">ExPusherLite</span><span class="o">.</span><span class="n">Web</span><span class="p">,</span> <span class="ss">:controller</span>
</span></span><span class="line"><span class="cl"> 
</span></span><span class="line"><span class="cl"><span class="o">-</span>  <span class="n">plug</span> <span class="ss">:authenticate</span>
</span></span><span class="line"><span class="cl"><span class="o">+</span>  <span class="n">plug</span> <span class="no">ExPusherLite</span><span class="o">.</span><span class="n">Authentication</span>
</span></span><span class="line"><span class="cl"> 
</span></span><span class="line"><span class="cl"><span class="o">-</span>  <span class="k">def</span> <span class="nf">create</span><span class="p">(</span><span class="n">conn</span><span class="p">,</span> <span class="n">params</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl"><span class="o">-</span>    <span class="n">topic</span> <span class="o">=</span> <span class="n">params</span><span class="o">[</span><span class="s2">&#34;topic&#34;</span><span class="o">]</span>
</span></span><span class="line"><span class="cl"><span class="o">-</span>    <span class="n">event</span> <span class="o">=</span> <span class="n">params</span><span class="o">[</span><span class="s2">&#34;event&#34;</span><span class="o">]</span>
</span></span><span class="line"><span class="cl"><span class="o">+</span>  <span class="k">def</span> <span class="nf">create</span><span class="p">(</span><span class="n">conn</span><span class="p">,</span> <span class="sx">%{&#34;app_slug&#34; =&gt; app_slug, &#34;event&#34; =&gt; event, &#34;topic&#34; =&gt; topic, &#34;scope&#34; =&gt; scope}</span> <span class="o">=</span> <span class="n">params</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">     <span class="n">message</span> <span class="o">=</span> <span class="p">(</span><span class="n">params</span><span class="o">[</span><span class="s2">&#34;payload&#34;</span><span class="o">]</span> <span class="o">||</span> <span class="s2">&#34;{}&#34;</span><span class="p">)</span> <span class="o">|&gt;</span> <span class="no">Poison</span><span class="o">.</span><span class="n">decode!</span>
</span></span><span class="line"><span class="cl"><span class="o">-</span>    <span class="no">ExPusherLite</span><span class="o">.</span><span class="n">Endpoint</span><span class="o">.</span><span class="n">broadcast!</span> <span class="n">topic</span><span class="p">,</span> <span class="n">event</span><span class="p">,</span> <span class="n">message</span>
</span></span><span class="line"><span class="cl"><span class="o">+</span>    <span class="n">topic_event</span> <span class="o">=</span>
</span></span><span class="line"><span class="cl"><span class="o">+</span>      <span class="k">if</span> <span class="n">topic</span> <span class="o">==</span> <span class="s2">&#34;#general&#34;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl"><span class="o">+</span>        <span class="n">event</span>
</span></span><span class="line"><span class="cl"><span class="o">+</span>      <span class="k">else</span>
</span></span><span class="line"><span class="cl"><span class="o">+</span>        <span class="s2">&#34;</span><span class="si">#{</span><span class="n">topic</span><span class="si">}</span><span class="s2">:</span><span class="si">#{</span><span class="n">event</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="o">+</span>      <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="o">+</span>    <span class="no">ExPusherLite</span><span class="o">.</span><span class="n">Endpoint</span><span class="o">.</span><span class="n">broadcast!</span> <span class="s2">&#34;</span><span class="si">#{</span><span class="n">scope</span><span class="si">}</span><span class="s2">:</span><span class="si">#{</span><span class="n">app_slug</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">,</span> <span class="n">topic_event</span><span class="p">,</span> <span class="n">message</span>
</span></span><span class="line"><span class="cl">     <span class="n">json</span> <span class="n">conn</span><span class="p">,</span> <span class="sx">%{}</span>
</span></span><span class="line"><span class="cl">   <span class="k">end</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>The first difference is that I am pattern matching from the arguments directly to the &ldquo;topic&rdquo; and &ldquo;event&rdquo; variables. This function is also aware of the &ldquo;<tt>#general</tt>&rdquo; string the client can send to indicate an app-wide broadcast. And the new topic is the concatenation of &ldquo;topic&rdquo; and &ldquo;event&rdquo; to allow for &ldquo;channel-only&rdquo; messages.</p>
<p>To connect this all to the WebSocket handler, we must make the following changes:</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="c1"># web/channels/room_channel.ex</span>
</span></span><span class="line"><span class="cl"><span class="o">-</span>  <span class="k">def</span> <span class="nf">handle_in</span><span class="p">(</span><span class="s2">&#34;msg&#34;</span><span class="p">,</span> <span class="n">payload</span><span class="p">,</span> <span class="n">socket</span> <span class="o">=</span> <span class="sx">%{ topic: &#34;public:&#34; &lt;&gt; _ }</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl"><span class="o">-</span>    <span class="n">broadcast</span> <span class="n">socket</span><span class="p">,</span> <span class="s2">&#34;msg&#34;</span><span class="p">,</span> <span class="n">payload</span>
</span></span><span class="line"><span class="cl"><span class="o">+</span>  <span class="k">def</span> <span class="nf">handle_in</span><span class="p">(</span><span class="n">topic_event</span><span class="p">,</span> <span class="n">payload</span><span class="p">,</span> <span class="n">socket</span> <span class="o">=</span> <span class="sx">%{ topic: &#34;public:&#34; &lt;&gt; _ }</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl"><span class="o">+</span>    <span class="n">broadcast</span> <span class="n">socket</span><span class="p">,</span> <span class="n">topic_event</span><span class="p">,</span> <span class="n">payload</span>
</span></span><span class="line"><span class="cl">     <span class="p">{</span> <span class="ss">:noreply</span><span class="p">,</span> <span class="n">socket</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">   <span class="k">end</span>
</span></span><span class="line"><span class="cl"> 
</span></span><span class="line"><span class="cl"><span class="o">-</span>  <span class="k">def</span> <span class="nf">handle_in</span><span class="p">(</span><span class="s2">&#34;msg&#34;</span><span class="p">,</span> <span class="n">payload</span><span class="p">,</span> <span class="n">socket</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl"><span class="o">+</span>  <span class="k">def</span> <span class="nf">handle_in</span><span class="p">(</span><span class="n">topic_event</span><span class="p">,</span> <span class="n">payload</span><span class="p">,</span> <span class="n">socket</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">     <span class="n">claims</span> <span class="o">=</span> <span class="no">Guardian</span><span class="o">.</span><span class="n">Channel</span><span class="o">.</span><span class="n">claims</span><span class="p">(</span><span class="n">socket</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">     <span class="k">if</span> <span class="n">permitted_topic?</span><span class="p">(</span><span class="n">claims</span><span class="o">[</span><span class="ss">:publish</span><span class="o">]</span><span class="p">,</span> <span class="n">socket</span><span class="o">.</span><span class="n">topic</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl"><span class="o">-</span>      <span class="n">broadcast</span> <span class="n">socket</span><span class="p">,</span> <span class="s2">&#34;msg&#34;</span><span class="p">,</span> <span class="n">payload</span>
</span></span><span class="line"><span class="cl"><span class="o">+</span>      <span class="n">broadcast</span> <span class="n">socket</span><span class="p">,</span> <span class="n">topic_event</span><span class="p">,</span> <span class="n">payload</span>
</span></span><span class="line"><span class="cl">       <span class="p">{</span> <span class="ss">:noreply</span><span class="p">,</span> <span class="n">socket</span> <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>Now, the Channel does not pattern match on a specific event, it lets it through without further validation, trusting that the EventsController is doing the right thing. I will come back to this piece for improvements in the future, possibly.</p>
<h3>Deploying our first Phoenix app to Heroku!<span class="hx:absolute hx:-mt-20" id="deploying-our-first-phoenix-app-to-heroku"></span>
    <a href="#deploying-our-first-phoenix-app-to-heroku" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>In this section we will just follow the <a href="http://www.phoenixframework.org/docs/heroku"target="_blank" rel="noopener">official documentation</a>, so read it if you want more details.</p>
<p>Let&rsquo;s get started:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>heroku apps:create your-expusherlite --buildpack &#34;https://github.com/HashNuke/heroku-buildpack-elixir.git&#34;
heroku buildpacks:add https://github.com/gjaldon/heroku-buildpack-phoenix-static.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>I am naming the application &ldquo;your-expusherlite&rdquo; but you should change it to your own name, of course. And the rest of the configuration data are all examples that you must change for your own needs.</p>
<p>Heroku relies on environment variables. So we start by erasing &ldquo;<tt>config/prod.secret.exs</tt>&rdquo; and change &ldquo;<tt>config/prod.exs</tt>&rdquo; to look like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">config</span> <span class="ss">:ex_pusher_lite</span><span class="p">,</span> <span class="no">ExPusherLite</span><span class="o">.</span><span class="n">Endpoint</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="ss">http</span><span class="p">:</span> <span class="o">[</span><span class="ss">port</span><span class="p">:</span> <span class="p">{</span><span class="ss">:system</span><span class="p">,</span> <span class="s2">&#34;PORT&#34;</span><span class="p">}</span><span class="o">]</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="ss">url</span><span class="p">:</span> <span class="o">[</span><span class="ss">scheme</span><span class="p">:</span> <span class="s2">&#34;https&#34;</span><span class="p">,</span> <span class="ss">host</span><span class="p">:</span> <span class="s2">&#34;your-expusherlite.herokuapp.com&#34;</span><span class="p">,</span> <span class="ss">port</span><span class="p">:</span> <span class="mi">443</span><span class="o">]</span><span class="p">,</span> <span class="ss">force_ssl</span><span class="p">:</span> <span class="o">[</span><span class="ss">rewrite_on</span><span class="p">:</span> <span class="o">[</span><span class="ss">:x_forwarded_proto</span><span class="o">]]</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="ss">cache_static_manifest</span><span class="p">:</span> <span class="s2">&#34;priv/static/manifest.json&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="ss">secret_key_base</span><span class="p">:</span> <span class="no">System</span><span class="o">.</span><span class="n">get_env</span><span class="p">(</span><span class="s2">&#34;SECRET_KEY_BASE&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Configure your database</span>
</span></span><span class="line"><span class="cl"><span class="n">config</span> <span class="ss">:ex_pusher_lite</span><span class="p">,</span> <span class="no">ExPusherLite</span><span class="o">.</span><span class="n">Repo</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="ss">adapter</span><span class="p">:</span> <span class="no">Ecto</span><span class="o">.</span><span class="n">Adapters</span><span class="o">.</span><span class="n">Postgres</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="ss">url</span><span class="p">:</span> <span class="no">System</span><span class="o">.</span><span class="n">get_env</span><span class="p">(</span><span class="s2">&#34;DATABASE_URL&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="ss">pool_size</span><span class="p">:</span> <span class="mi">20</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">config</span> <span class="ss">:ex_pusher_lite</span><span class="p">,</span> <span class="ss">:admin_authentication</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="ss">username</span><span class="p">:</span> <span class="no">System</span><span class="o">.</span><span class="n">get_env</span><span class="p">(</span><span class="s2">&#34;PUSHER_ADMIN_USERNAME&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="ss">password</span><span class="p">:</span> <span class="no">System</span><span class="o">.</span><span class="n">get_env</span><span class="p">(</span><span class="s2">&#34;PUSHER_ADMIN_PASSWORD&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># remove this line:</span>
</span></span><span class="line"><span class="cl"><span class="c1"># import_config &#34;prod.secret.exs&#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>Now we must configure the environment variables &ldquo;SECRET_KEY_BASE&rdquo;, &ldquo;PUSHER_ADMIN_USERNAME&rdquo; and &ldquo;PUSHER_ADMIN_PASSWORD&rdquo;. Use the included &ldquo;<tt>mix phoenix.gen.secret</tt>&rdquo; to generate those.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>heroku config:set SECRET_KEY_BASE=&#34;`mix phoenix.gen.secret`&#34;
heroku config:set PUSHER_ADMIN_USERNAME=&#34;FPO0QUkqbAP6EGjElqBzDQuMs8bhFS3&#34;
heroku config:set PUSHER_ADMIN_PASSWORD=&#34;n78DPGmK3DBQy8YAVyshiGqcXjjSXSD&#34;</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Then it&rsquo;s just a matter of waiting for the good old &ldquo;<tt>git push heroku master</tt>&rdquo; to finish compiling everything for the first time. And because this is the first deploy you should not forget to run &ldquo;<tt>heroku run mix ecto.migrate</tt>&rdquo; to create the database table.</p>
<p>Now, if I did everything right, as an Administrator that knows the above hardcoded secrets I should be able to create a new Application like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>curl --data &#34;app[name]=shiny-new-app&#34; https://FPO0QUkqbAP6EGjElqBzDQuMs8bhFS3:n78DPGmK3DBQy8YAVyshiGqcXjjSXSD@your-expusherlite.herokuapp.com/api/admin/apps</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>And this is the result I got!</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>{&#34;data&#34;:{&#34;slug&#34;:&#34;shiny-new-app&#34;,&#34;secret&#34;:&#34;42560373-0fe1-506e-28ca-35ab5221fb3d&#34;,&#34;name&#34;:&#34;shiny-new-app&#34;,&#34;key&#34;:&#34;958c16e7-ab93-dac0-0fc6-6cb864e26358&#34;,&#34;id&#34;:1,&#34;active&#34;: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>Great, now that we have a valid Application key and secret we can configure our Rails Client Demo and deploy it to Heroku as well.</p>
<h3>Deploying the Rails Client to Heroku<span class="hx:absolute hx:-mt-20" id="deploying-the-rails-client-to-heroku"></span>
    <a href="#deploying-the-rails-client-to-heroku" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>This is a simple Rails application, we can just create the app and deploy right away:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>heroku create your-expusherlite-client
heroku config:set PUSHER_URL=your-expusherlite.herokuapp.com
heroku config:set PUSHER_APP_ID=shiny-new-app
heroku config:set PUSHER_KEY=958c16e7-ab93-dac0-0fc6-6cb864e26358
heroku config:set PUSHER_SECRET=42560373-0fe1-506e-28ca-35ab5221fb3d
heroku config:set PUSHER_CHANNEL=shiny-new-topic
git push heroku master</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>I&rsquo;m assuming the readers of this post already know how to configure a Rails app properly for Heroku. Just to mention it, I configure this app with the 12 factor and Puma gems and added a proper Procfile. Another very small change was changing the &ldquo;pusher_lite.rb&rdquo; initializer to create a URI with &ldquo;https&rdquo; because the ExPusherLite we deployed to production requires SSL by default.</p>
<p>There is one more caveat. Being led by experienced web programmers, they made sure that, unlike this bare-bones exercise here, the Phoenix framework itself is secure. One such example is to disallow Websocket connections from different hosts.</p>
<p>Out of the box, the &ldquo;phoenix.js&rdquo; Socket will fail connection when we try to connect from the &ldquo;your-expusherlite-client.herokuapp.com&rdquo; Rails app host to the Phoenix app in &ldquo;your-expusherlite.herokuapp.com&rdquo; with the following error:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>WebSocket connection to &#39;wss://your-expusherlite.herokuapp.com/socket/websocket?guardian_token=N_YCG6hGK7…iOlsicHVibGljOioiXX0._j6s2LiaKde9rBhnTMxDkm0XV5u89pNh1AdLFY6Rlt8&amp;vsn=1.0.0&#39; failed: Error during WebSocket handshake: Unexpected response code: 403</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>And in the Phoenix log we will see this very helpful message:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>[error] Could not check origin for Phoenix.Socket transport.

This happens when you are attempting a socket connection to
a different host than the one configured in your config/
files. For example, in development the host is configured
to &#34;localhost&#34; but you may be trying to access it from
&#34;127.0.0.1&#34;. To fix this issue, you may either:

  1. update [url: [host: ...]] to your actual host in the
     config file for your current environment (recommended)

  2. pass the :check_origin option when configuring your
     endpoint or when configuring the transport in your
     UserSocket module, explicitly outlining which origins
     are allowed:

        check_origin: [&#34;https://example.com&#34;,
                       &#34;//another.com:888&#34;, &#34;//other.com&#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>Unless you know what a <a href="https://www.christian-schneider.net/CrossSiteWebSocketHijacking.html"target="_blank" rel="noopener">Cross-Site Web Socket Hijacking</a> is, you will prefer to keep the default settings as they are. In a green-field Phoenix app, the Web part will connect to the Web Socket in the same app and, therefore, in the same host, so this is not an issue.</p>
<p>In this case I am making a separate micro-service to mimic Pusher.com behavior so it should be able to accept Web Socket connections from different hosts.</p>
<p>If you control the applications being created, you will likely prefer to make the &ldquo;<tt>check_origin</tt>&rdquo; setting read from your database for the exact hosts. As a feature for next time I could add a &ldquo;host&rdquo; field in the &ldquo;App&rdquo; model and use it to validate connections in the transport configuration. For the time being I will just make it accept any hosts:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code># web/channels/user_socket.ex
defmodule ExPusherLite.UserSocket do
  use Phoenix.Socket

  ## Channels
  channel &#34;*&#34;, ExPusherLite.RoomChannel

  ## Transports
  transport :websocket, Phoenix.Transports.WebSocket, check_origin: false
  ...</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>And this is it! Now the Rails app should be able to connect and send messages! And you should be able to create any number of new apps and connect all of them to this same service.</p>
<p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/534/big_Screenshot_from_2015-12-14_14_58_22.png" alt="Final Heroku Client"  loading="lazy" /></p>
<h3>Conclusion<span class="hx:absolute hx:-mt-20" id="conclusion"></span>
    <a href="#conclusion" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Right now, we have a functional, albeit bare-bones, Pusher.com clone that will work for any number of use cases where Pusher.com would be used.</p>
<p>As I warned many times before, the security part is still flaky and needs work. I will still extend on what Daniel began with Guardian to authenticate Web Socket users to private channels as well. And the core should also receive auditing and reporting capabilities (to be able to report usage, number of active connections, throughput of events, keep at least a short history of events so new connections can retrieve the last sent messages, and so on).</p>
<p>But from here it&rsquo;s a matter of adding features to an already working core. And this is nothing more than Phoenix out-of-the-box without too much added on top of it! It says a lot about the current state of maturity of this very capable framework.</p>
<p>In terms of performance, for this very simple example, I spun up 1 free Heroku dyno for each app.</p>
<p>The Rails app is able to respond to the front-end user interface in around 2ms. And the Sucker Punch job - which does the heavy HTTP POST to ExPusherLite - takes in the order of 30ms or less.</p>
<p>The Phoenix server receives the HTTP POST and performs the broadcast in around less than 6ms. Also quite fast. The times will vary a lot because I believe the free dyno is not only slow but also stays in highly shared metal boxes, getting impact from other neighbor apps running in the same box.</p>
<p>Because we already have an administrative API to create and manage apps (create new ones, delete, update, etc) we can already create a separate application in any other framework to make a dashboard for admins or for a self-serving front-end for developers to register new apps and receive key/secret pair to add in their own applications.</p>
<p>Both the ExPusherLite server and the demo client are deployed in Heroku and you can test the client right now <a href="https://expusherlite-client.herokuapp.com/"target="_blank" rel="noopener">clicking here</a>. The admin keys are different from what I showed in this post, of course, so you won&rsquo;t be able to create new apps, but you can deploy it yourself to your own environment.</p>
]]></content:encoded><category>elixir-and-erlang</category><category>distributed-systems</category><category>devops</category></item><item><title>Chatting with Adam Jacob</title><link>https://akitaonrails.github.io/en/2009/11/18/chatting-with-adam-jacob/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2009/11/18/chatting-with-adam-jacob/</guid><pubDate>Wed, 18 Nov 2009 11:46:00 -0200</pubDate><description>&lt;p&gt;&lt;img src="http://s3.amazonaws.com/akitaonrails/assets/2009/11/18/Opscode_logo_final_full_aspect_medium_original.png" alt="" loading="lazy" /&gt;&lt;/p&gt;
&lt;p&gt;Configuration Management is a tricky subject. For non-starters, when you&amp;rsquo;re a developer and you have few boxes to take care of, you can usually get away with just managing them manually. People are probably just used to pop in a CD, double-click the &amp;ldquo;install&amp;rdquo; program and click &amp;ldquo;next&amp;rdquo;, &amp;ldquo;next&amp;rdquo; until the end, then you manually log in to backup (when you remember it), and sometimes you do apply some security updates when you remember about them.&lt;/p&gt;</description><content:encoded><![CDATA[<p><img src="http://s3.amazonaws.com/akitaonrails/assets/2009/11/18/Opscode_logo_final_full_aspect_medium_original.png" alt=""  loading="lazy" /></p>
<p>Configuration Management is a tricky subject. For non-starters, when you&rsquo;re a developer and you have few boxes to take care of, you can usually get away with just managing them manually. People are probably just used to pop in a CD, double-click the &ldquo;install&rdquo; program and click &ldquo;next&rdquo;, &ldquo;next&rdquo; until the end, then you manually log in to backup (when you remember it), and sometimes you do apply some security updates when you remember about them.</p>
<p>But then you have more than a dozen machines, things start to get uglier, you end up making more mistakes, forgetting important steps, and all of a sudden managing machines becomes a nightmare. You end up being woken up in the middle of the night because you forgot to install some crucial component, and so on and so forth.</p>
<p>The same way you need testing, continuous integration tools when you&rsquo;re a developer, you also need automated, reliable and flexible tools for the system administrator role. That&rsquo;s where tools such as <strong>Chef</strong> kick in to help.</p>
<p>From <a href="http://opscode.com/"target="_blank" rel="noopener">Opscode Inc.</a>, we have <a href="http://twitter.com/adamhjk"target="_blank" rel="noopener">Adam Jacob</a> (CTO) and <a href="http://twitter.com/jesserobbins"target="_blank" rel="noopener">Jesse Robbins</a> (CEO) to talk about the new contender in the automated system administration field, <a href="http://www.opscode.com/chef/"target="_blank" rel="noopener">Chef</a>, already in use by many companies which are pushing the cutting edge to maintain their datacenters.</p>
<p><strong>AkitaOnRails:</strong> To kick start this interview, it would be great to have more background info about you guys. So, how did you end up in the configuration management space?</p>
<p><img src="http://s3.amazonaws.com/akitaonrails/assets/2009/11/18/adam_jacob_original_original.png" alt=""  loading="lazy" /></p>
<p><strong>Adam:</strong> I&rsquo;ve been a systems administrator for 13 years, and for a majority of that time I was working for companies who did a lot of mergers and acquisition work. Every couple of months we would acquire a new company, and it was my job to help figure out how to absorb those companies into the whole. I got really good at looking at an application I had nothing to do with creating, and figuring out what needed to be done to make it scale (or at least make it run.)</p>
<p>One thing we couldn&rsquo;t do was tell the people who had built the applications that they needed to be radically altered (in many cases, they weren&rsquo;t even around anymore.) What that meant in practice was we needed to have a very flexible, modular underlying architecture – everything we did had to be in service of the application, not the other way around. By necessity that meant becoming a tools developer – if we didn&rsquo;t have the tools we needed, we would never be able to do the job in front of us.</p>
<p>Eventually I co-founded a consulting firm called HJK Solutions. We built fully automated infrastructure for startups – everything from OS installation to application deployment, all fully automated (including Identity Management, Monitoring and Trending, etc.) Over the course of the next two years we built infrastructure for 12 different startups, whose products ranged from electrical car fleet management to online dating.</p>
<p>So I come to configuration management through the trenches – as a line-level systems administrator trying to make my life easier, as a consultant helping others to reap the benefits, and now as a tool builder trying to move the state of the art forward.</p>
<p><img src="http://s3.amazonaws.com/akitaonrails/assets/2009/11/18/Jesse_Robbins_original_original.png" alt=""  loading="lazy" /></p>
<p><strong>Jesse:</strong> Jesse Robbins is CEO of Opscode (makers of Chef) and a recognized expert in Infrastructure, Web Operations, and Emergency Management. He serves as co-chair of the Velocity Web Performance &amp; Operations Conference and contributes to the O&rsquo;Reilly Radar. Prior to co-founding Opscode, he worked at Amazon.com with a title of &ldquo;Master of Disaster&rdquo; where he was responsible for Website Availability for every property bearing the Amazon brand. Jesse is a volunteer Firefighter/EMT and Emergency Manager, and led a task force deployed in Operation Hurricane Katrina. His experiences in the fire service profoundly influence his efforts in technology, and he strives to distill his knowledge from these two worlds and apply it in service of both.</p>
<p><strong>AkitaOnRails:</strong> What&rsquo;s the story behind Opscode, what&rsquo;s its mission, and what&rsquo;s the story for Chef&rsquo;s creation?</p>
<p><strong>Opscode:</strong> The story behind Opscode really starts when we met Jesse. He had written an article for O&rsquo;Reilly Radar about <a href="http://radar.oreilly.com/2007/10/operations-is-a-competitive-ad.html"target="_blank" rel="noopener">Operations being the new secret sauce</a> for startups. He and I met for coffee, became friends, and stayed in touch. Jesse understands operations culture at a visceral level, and he&rsquo;s very well connected to a huge community of like-minded people that I didn&rsquo;t even know existed.</p>
<p>As our consulting company grew, we reached a crossroads – we clearly had a nice business going, and it wasn&rsquo;t difficult for us to find more work. What was difficult was finding people who had the skills to actually deploy a new infrastructure, and to adapt the stack we had developed for a new application. We couldn&rsquo;t avoid the fact that there was a couple of weeks of very high-touch work that was required to get the entire infrastructure up and running for a new client.</p>
<p>We tried to recruit Jesse during this period, since he was one of those rare people who could do that initial high-touch engagement. He turned us down – primarily because he saw what we did: we could build a consulting company that was huge, but it would still be down to us in the trenches every day. Unless we could get over that hump, there were probably less stressful ways to make a living. :)</p>
<p>So we started looking at what was stopping us from being able to get that initial part of the engagement done as quickly as possible. What was stopping us from literally having a customer fill out a questionnaire, and letting that data drive 95% of the decisions about the infrastructure?</p>
<p>It turned out the answer was, in a word, &ldquo;everything&rdquo;. The entire stack of open source tools we were using had been built in a different era, and they saw the world through a very different lens. We needed everything to have an API, we needed everything to be more open with its data, and we needed everything to be flexible enough to handle the next evolution of application architectures (whatever that may be.)</p>
<p>Once we had that revelation, the next step was figuring out what a &rsquo;new stack&rsquo; would really look like. If we could start from scratch, what would we take with us, and what would we leave behind? Amazon had done such a great job showing us what an API over the bootstrapping process could look like, and the kind of benefits that could be had from the approach. So assuming something like that existed for bootstrapping, what about the next layer of the stack (configuration)?</p>
<p>We started experimenting with Chef during this period of questioning. We started working on Chef with the goal of putting ourselves out of business – making the barrier to entry to having a fully automated infrastructure so low that any developer or systems administrator could just do it. We built a prototype, showed it to Jesse, and he agreed to come on board.</p>
<p>Opscode was born then, and our mission came out of those experiments: we are bringing Infrastructure Automation to the Masses. We want to tear down the barriers to entry that stop people from having really great, repeatable, automated infrastructure. Our role is to bring developers and systems administrators the best tools possible, so that they can build the systems they have always wanted.</p>
<p>We raised 2.5 million from DFJ in January of 2009 on that vision, and have been at it ever since.</p>
<p><strong>AkitaOnRails:</strong> I&rsquo;d like to say that only amateur sysadmins do everything manually, but I think most small to medium corporations at least still do everything manually or with random scripts spread all over the place. The notion of &ldquo;configuration management&rdquo; is still new to a lot of people. Could you briefly explain what it is, and why it is important?</p>
<p><strong>Opscode:</strong> To me, &ldquo;Configuration Management&rdquo;, at its core, is all the stuff you have to do to take a system from &ldquo;running an operating system&rdquo; to &ldquo;doing its job&rdquo;. When a systems administrator configures a system by hand, and posts her notes on a Wiki, she&rsquo;s practicing the most primitive form of Configuration Management.</p>
<p>Now, having those notes is better than nothing – when she needs to do that task again, she can at least go back and read them to remember what she did last time. She still has to do it over again, though – and the repetition gets tiresome. So she starts writing scripts that encapsulate that knowledge in code, so that now she only has to run a series of smaller commands.</p>
<p>Time passes, though, and entropy sets in. The systems begin to drift from where they were when the systems administrator wrote the scripts. Next thing you know, the scripts don&rsquo;t run anymore, or if they did, the configuration they build is wrong. Our intrepid admin then starts editing the scripts, or making new ones, to deal with the system when it&rsquo;s in this new state. This is the stage we call &ldquo;tool sprawl&rdquo; – you have a tool for each different phase of a system&rsquo;s observable life-cycle.</p>
<p>Modern configuration management tools solve this problem by providing a framework for describing the final state a system should be in, rather than the discrete steps we should take to get there at any given time. Rather than writing a script that lists the commands to install Apache, write the configuration file, and start the service, you would describe that &ldquo;apache should be installed&rdquo;, the configuration file &ldquo;should look like this&rdquo;, and the service &ldquo;should be running&rdquo;.</p>
<p>When the configuration management system runs it looks at each of these descriptions individually, and makes sure that they are in the proper state. We no longer care what the initial state of the system is – the configuration management system will only take action at each step in the process if the system is not in the state you described. If you run it again, and nothing has changed, the system takes no action. (Configuration management geeks call this property <a href="http://en.wikipedia.org/wiki/Idempotence"target="_blank" rel="noopener">idempotence</a>)</p>
<p>This means that you can describe your systems once in code, and as they change over time, simply update that code to reflect what you want the state of those systems to be. The impact of this model on the daily life a systems administrator cannot be overstated – it makes everything easier.</p>
<p>It also has huge impacts outside the systems administrators world. You now have a living document that describes how all your servers are configured – you can share it, you can put it in revision control, you can print it out for an auditor. Business processes that previously required manual intervention frequently can be boiled down to discrete changes. The more you work in this way, the more the impacts spread throughout the organization.</p>
<p><strong>AkitaOnRails:</strong> Even though the Ruby community probably compares Chef to Puppet, I think one of the most widely used systems is CFEngine2. How does Chef compare with CFEngine2?</p>
<p><strong>Opscode:</strong> Cfengine 2 is where almost everyone in the configuration management world cut their teeth. Mark Burgess is responsible for the academic papers that outlined the idea that each part of the system should be idempotent, and his work in studying how real-world systems can be managed at scale has done more to impact the evolution of configuration management than any other individual.</p>
<p>In addition to the idea of idempotent resources, Burgess introduced the larger concept of &ldquo;convergence&rdquo;. The basic idea here is that if you have the description of how every finite resource should be configured, given enough time, those resources will bring the system into a compliant state. The order in which the resources run fundamentally does not matter – eventually, they will all wind up in the right place.</p>
<p>While this model works at a fundamental level, it has some pretty dramatic inefficiencies. Cfengine applies resources based on their type – all the files are managed at once, then all the services, then all the packages. You can control the order they are run with the &lsquo;actionsequence&rsquo;, but each system gets only one order. So if you need to have a file copied, a package installed, and a service restarted, it&rsquo;s easy enough to model. When you start getting into more complex configurations, however, it becomes more and more difficult to get an actionsequence assembled that allows you to configure your entire system in a single run.</p>
<p>This is not a bug in Cfengine – it&rsquo;s by design. Convergence is the answer – it&rsquo;s okay that it has to run more than once, it will get there eventually. If you are thinking too much about the order things should happen in, you&rsquo;re probably not thinking about idempotent descriptions.</p>
<p>In my experience, though, this was frustrating at scale. It meant that the amount of time it took to configure a system increased as the configuration became more complicated, and your ability to model complicated interactions in the system became increasingly opaque.</p>
<p>For the era in which it was written, this problem mattered a lot less. If it takes you 6 to 8 weeks to get a server even ordered, then another week to get it racked, stacked, and the OS installed, the fact that Cfengine needs to run 3 times to configure it doesn&rsquo;t matter very much. (You have lots of bigger problems!) In a world where many of us can go from bare-metal to running operating system in 5 minutes, either via our own install systems or an API call to AWS, it starts to matter a whole lot.</p>
<p>Chef takes a different approach. We start with the idea of an idempotent &ldquo;resource&rdquo; – a package, a service, etc. We add the idea of &ldquo;actions&rdquo; – these are all the different states you might want to request a resource to be in. Then you put these resources into &ldquo;recipes&rdquo;, which are evaluated in the order they are written. You can then have recipes rely on other recipes having completed before they are run, giving you the ability to say &ldquo;make sure Apache is installed before you configure my Web Application&rdquo;.</p>
<p>The goal is that, with Chef, you can always bring the system into the proper state with a single Chef run. It sees convergence as a response to a bug – a system that is 1/2 configured is, in fact, broken. If the cause of the bug is environmental – a network service is not available, for example – then running Chef again will likely fix things. If the issue is that you haven&rsquo;t specified the order that you want resources to be configured in, though, then it&rsquo;s a bug in your recipes. If you can&rsquo;t write a recipe to configure your system in a single Chef run, it&rsquo;s a bug in Chef.</p>
<p>This also has side-effects for reasoning about the system at scale. Given the same configuration and attributes, Chef will always behave the same way, on every system. As you add more and more things to the system, it&rsquo;s still easy to reason about when, and how, they will be configured.</p>
<p>Interestingly enough, Cfengine 3 implements a system that is very similar to Chef. Rather than a single global actionsequence, it uses a &lsquo;bundlesequence&rsquo;, where bundles are roughly analogous to Chef&rsquo;s concept of a recipe.</p>
<p>Another major difference is that Chef allows you to &rsquo;look up&rsquo; at your entire infrastructure when configuring a system. This ability comes in handy when you want to do things like configure a monitoring system, or a load balancer. You can ask Chef questions like &ldquo;what are all the servers running my application in production&rdquo;, and use the response to configure your system. (You can see an example of this on <a href="http://www.opscode.com/blog/2009/10/07/preview-chef-0-8-and-the-opscode-platform/"target="_blank" rel="noopener">our blog</a>)</p>
<p>From a systems architecture point of view, Chef and Cfengine are actually fairly similar. They both push the majority of the hard work of configuring a system out to the systems themselves. This is highly advantageous at scale – the Chef and Cfengine servers are really glorified file transfer systems.</p>
<p>We differ pretty deeply on our approach to what a language for configuration management should look like, but I&rsquo;ll talk more about that in a later question.</p>
<p>Cfengine is at work in some of the largest data centers in the industry, and it fundamentally altered the landscape of systems management. While it&rsquo;s not the tool I want anymore, you can&rsquo;t understate the impact it&rsquo;s had on the design of every configuration management tool that came after it – including Chef.</p>
<p><strong>AkitaOnRails:</strong> Chef has a lot of components to it. Could you briefly describe all the main components that work together? The client side, the server side, cookbooks?</p>
<p><strong>Opscode:</strong> Sure! The most important part of Chef is the cookbooks – they are where you actually describe your configuration. They collect recipes, and all the assets needed for the recipe to run (files, templates, etc.). <a href="http://cookbooks.opscode.com"target="_blank" rel="noopener">Cookbooks</a> are very often share-able, and lots of cookbooks already exist.</p>
<p>Chef can run your cookbooks in two modes – a client/server mode (chef-client), or a stand-alone mode (chef-solo). When you run chef-solo, you pass a URL to a tarball full of the cookbooks you want to have run. There is no more infrastructure than that – put your tarballs someplace you can download them, and go nuts.</p>
<p>In client/server mode, each Chef client is configured to talk to a Chef Server. The Server stores information about each client (a whole lot of it – things like IP addresses, loaded kernel modules, and more), and distributes the cookbooks they need to configure themselves. It also provides a REST API and an interactive Web UI, so you can easily alter the configuration of your systems centrally. Finally, all the data the server collects is indexed and searchable – you can then use this in your recipes to configure services that require complex, dynamic configuration. (Some examples would be dynamically discovering a master MySQL server, or finding all the memcached servers in a cluster)</p>
<p><strong>AkitaOnRails:</strong> What would you say about Chef&rsquo;s maturity? CFEngine has more than a decade of usage, which is difficult to beat. Would you say that it&rsquo;s &ldquo;mature enough&rdquo;? Meaning, it&rsquo;s already in production in companies of many sizes, its APIs don&rsquo;t change too much and my cookbooks will probably work if I upgrade to newer version of Chef?</p>
<p><strong>Opscode:</strong> Chef is a little over a year old – it was first released to the public on January 15th, 2009. Since then, 42 different developers have committed to the <a href="https://www.ohloh.net/p/opscode-chef/factoids/2025809"target="_blank" rel="noopener">project</a>, around 5 of whom work for Opscode. It&rsquo;s been in production use in the Engine Yard Cloud from the day it was released. Since then, it&rsquo;s seen adoption by companies and universities of all different sizes, from small startups to huge enterprises with very heavy compliance requirements.</p>
<p>So it&rsquo;s definitely &ldquo;mature enough&rdquo; for real world use – lots of people are using it, and relying on it, every day. Balancing that knowledge with the reality that the project is fast moving and evolving is important, and we try and do it in a number of ways:</p>
<ol>
<li>The recipe syntax is considered basically &ldquo;complete&rdquo;. If we do make changes, they are backwards compatible to previous Chef releases. Short of something truly amazing coming along that radically alters the shape of the universe, this will remain true.</li>
<li>We test a lot. Chef has over 2000 unit tests, and functional tests that cover the entirety of the REST API, and much of the individual resources (we&rsquo;re aiming for 100% feature test coverage.) There are more lines of code testing chef than there are lines of code in Chef proper.</li>
<li>If we do break backwards compatibility, we try and make it happen only when we bump the minor revision. If you have an 0.7.x version of Chef installed, it should always work with other versions of Chef in the same release cycle.</li>
</ol>
<p>Most of the work in Chef currently is around adding more functionality, not in changing the way existing functionality works. It&rsquo;s safe to use Chef today – in the future, you&rsquo;ll just keep getting more good sauce to add to the mix, rather than having to deeply re-factor the way you do things.</p>
<p>Finally, the Chef Community is truly epically great – we have lots of people who are spending significant amounts of their time with Chef. Even if they aren&rsquo;t contributing code, they are answering questions, they are writing documentation, they are hanging out on IRC and offering you cupcakes. It&rsquo;s a group of people that are focused on solving real world problems, and helping each other to do the same. It&rsquo;s by far what I&rsquo;m proudest of, and I think it has a significant impact on whether Chef is ready for prime time. The bench is really, really deep.</p>
<p><strong>AkitaOnRails:</strong> Chef uses Ruby directly, which some would say it&rsquo;s both a blessing and a curse. It&rsquo;s probably perfect for Rubists, but I feel that most sysadmins are used to Bash, Python and are not very flexible on change. Why did you choose to use Ruby instead of a simpler language?</p>
<p><strong>Opscode:</strong> The answer to the question of why we use Ruby directly for the configuration language comes in two parts: why we extended a 3GL rather than build a declarative DSL or a complete modeling language, and why we chose Ruby as that 3GL.</p>
<p>First, why we extended a 3GL. Tools like Cfengine and Puppet build a declarative DSL for configuration management – a custom language, which provides a model within which systems administrators or software developers can work to automate their system. Other tools like Elastra&rsquo;s EDML and ECML, OpsWare&rsquo;s DCML, or Bcfg2 give you an XML schema to describe how the system should behave.</p>
<p>The issue with these approaches is that, by definition, they must build an abstraction for every task the end-user may want to perform: an impossible feat. The level of complexity inherent in automation, coupled with the inability to break out to 3GL language constructs when necessary, result in a system that can only target a subset of the total complexity, rather than enable users to find innovative solutions to their specific problems. By leveraging Ruby, adding support for other use cases is a matter of adding new sets of base classes while maintaining consistency and approachability in their interface design, because the full scope of the language is available.</p>
<p>Our goal with Chef was to keep the simplicity that comes from having the focus be on idempotent resource declaration, while giving you the flexibility of a full 3GL. In practical terms, anything you can do with Ruby you can do with Chef – and since Ruby is a 3GL, that amounts to essentially anything.</p>
<p>Larry Wall, the creator of Perl, has a quote that I love:</p>
<blockquote>
  <p>&ldquo;For most people the perceived usefulness of a computer language is inversely proportional to the number of theoretical axes the language intends to grind.&rdquo;</p>

</blockquote>
<p>Since we wanted Chef to have the maximum amount of usefulness, we have actively tried to remove as many theoretical axes as possible – a belief that we can imagine the total breadth of the problem space being one of them. There is more than one way to do it with Chef, and the only valid criteria for rating the success of your automation project is whether it solves your problems in a reliable way.</p>
<p>In practice, you need to know very little Ruby to use Chef. Here is an example of installing the program &ldquo;screen&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-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">package</span> <span class="s2">&#34;screen&#34;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl"> <span class="n">action</span> <span class="ss">:install</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>The same thing in Puppet:</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">package</span> <span class="p">{</span> <span class="s2">&#34;screen&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="cl"> <span class="k">ensure</span> <span class="o">=&gt;</span> <span class="n">present</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>And in Cfengine 2:</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="ss">packages</span><span class="p">:</span>
</span></span><span class="line"><span class="cl"> <span class="n">screen</span> <span class="n">action</span><span class="o">=</span><span class="n">install</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>While all of these systems require learning the syntax, at a base level, there isn&rsquo;t much difference between them in terms of raw learning required. The difference is that when you hit a limitation in Chef, you have the ability to innovate easily, and when you hit those same limitations in other tools, you do not.</p>
<p>We chose Ruby because of its fairly unique ability to create new syntax easily. Tools like Rspec are fine examples of ways you can manipulate Ruby for fun and profit that are very difficult to duplicate in other tools. We wanted to make sure that, even though you were &ldquo;in&rdquo; a 3GL, you didn&rsquo;t have to go through any extra hoops to make the simple things work. Ruby was the language that I was comfortable enough in, that I knew had the ability, to make that a reality.</p>
<p>That said, one of our goals is to extend the ability to write Chef recipes into other 3GL&rsquo;s. We have an example of doing this already with Perl – and we made no changes to the Chef source to accomplish it. You can see the <a href="http://search.cpan.org/~holoway/Chef-0.01/lib/Chef.pm"target="_blank" rel="noopener">demo here on CPAN</a>. It works by using Chef&rsquo;s JSON API to ship the compiled resources over to the Ruby library for execution, and over time we&rsquo;ll be extending those interfaces. You&rsquo;ll be able to have recipes written in Python interoperating with recipes written in Ruby and Perl.</p>
<p><strong>AkitaOnRails:</strong> Does Chef have (or for that matter, need) something like Augeas, which Puppet is trying to support?</p>
<p><strong>Opscode:</strong> Augeas is neat. Chef doesn&rsquo;t have Augeas support today, and the reason is that nobody has needed it badly enough to write the integration. One reason is that, with Chef, it is quite easy to dynamically add to a resource (like, say, a template) or search for particular systems that match a criteria. This means that the use-case for Augeas (which edits files in place) is less necessary – you can often get the data you need to render a template, rather than needing to build it up over time with incremental edits.</p>
<p>We think this is a better practice in general, as it ensures that all the systems you are managing can always be restored to a working state from nothing but the cookbook repository. If you use Augeas to allow idempotent changes of individual lines of a configuration file, it encourages the behavior of individual administrators editing files in place, which is a configuration management anti-pattern.</p>
<p><strong>AkitaOnRails:</strong> Sysadmins used to CFEngine complain about Ruby&rsquo;s dependencies and overall weight. Because for Chef to run you need Ruby installed. Not all distros have Ruby in the same version (although most already migrated to 1.8.7). Then you have the problem of weight. I am not familiar with Chef, but Puppet can grow to hundreds of megabytes. What they don&rsquo;t want is to have clusters of Chef machines (which, by themselves, also need maintenance, adding to the overall complexity). How do you deal with datacenters with thousands of servers? I know it&rsquo;s difficult to measure precisely, but what would be a reasonable ratio between Chef servers x managed servers?</p>
<p><strong>Opscode:</strong> When you are evaluating the scalability of configuration management systems, you want to look at two different axes. The horizontal one, which is the number of systems that can be managed by a single configuration management server at a particular rate of change, and the vertical, which is how much of your infrastructure can be automated by the tool.</p>
<p>On the vertical axis, I think Chef is the clear winner, for reasons I think are pretty well summed up by my answer to question 7. I would put Puppet second, and Cfengine last.</p>
<p>On the horizontal axis, Cfengine is the clear winner. It&rsquo;s written in C, and it has the thinnest possible server component – it does nothing but authenticate clients and serve files, essentially. I know first hand of data-centers that are running huge numbers of servers off a single cfengine server.</p>
<p>One important metric to keep in mind when discussing the horizontal scalability of a configuration management solution is that the most important metric is the <strong>rate of change</strong>. All the tools we&rsquo;ve been talking about are &lsquo;pull&rsquo; based – the clients check in at an &lsquo;interval&rsquo; with the server, and apply a <em>&lsquo;splay&rsquo;</em> to ensure that not all the systems check in at once. A common out of the box configuration is an interval of 30 minutes, with a splay of 15 minutes. (This means a server checks in every 30-45 minutes, depending.) If you are comfortable increasing that interval, you will get more scalability out of fewer resources (by lowering the amount of concurrency.)</p>
<p>So when anyone says &ldquo;I have 10 thousand servers on a single configuration management server&rdquo;, ask them &ldquo;at what interval?&rdquo;.</p>
<p>Chef scales like a web application. The server itself is quite thin, and is responsible for authenticating clients, transferring files, storing node state, and providing a search interface. It scales horizontally by adding new Chef Servers as necessary. The API is RESTful, and there is no session state between the clients and the server. (At least not in Chef 0.8+) When you encounter scalability problems with Chef, the tools you apply are the same ones you apply to any well designed web application.</p>
<p>You asked specifically about memory utilization – Chef does quite well in this regard. Individual server processes usually are between 14-50MB resident. The client itself, running in daemon mode, is usually around 28MB resident.</p>
<p>In our testing, the current bottleneck in a Chef server is CPU. Chef is smart about how it handles file transfers – we only transfer files that have changed on the server. To support this we calculate a checksum for each file requested, and we currently don&rsquo;t cache the results. We&rsquo;re planning on fixing this for the next major release of Chef (0.8.0) which should shift the bottleneck over to RAM.</p>
<p>With Chef 0.7, you should be able to support thousands of clients at a half-hour interval and fifteen minute splay on reasonable commodity hardware. The changes in Chef 0.8 should bring that number up dramatically – I&rsquo;ll get back to you with some benchmarks once the patches are in. :)</p>
<p><strong>AkitaOnRails:</strong> Probably related to the previous question, seems like specially after Sarbanes Oxley there&rsquo;s been an increasing interest in stuff such as ITIL, CoBit. Have you ever seen successful implementations of those in the Web-style infrastructure? I mean, I can see them succeeding in Banks, Aerospace and Defense, etc but I fail to see them working as advertised in a very dynamic environment such as Web services hosting. What are your experiences regarding this issue?</p>
<p><strong>Opscode:</strong> Well, I think you can think of ITIL in the same way you think about the classic &lsquo;Waterfall&rsquo; model of software development. For some kinds of projects and companies, it is essential – it&rsquo;s hard to imagine working a different way. Most often these are companies with huge manufacturing or quality control concerns – medical health, aerospace, banking and finance, etc.</p>
<p>The same thing applies to ITIL – the larger the concern, and the more stringent the requirements, and the longer the lead time, the more the processes they describe start to make sense. Like all large process, though, they tend to de-emphasize the human element – people have roles to play, and forms to file. :)</p>
<p>In the Web Ops culture, things are different. I&rsquo;ve never seen a successful marriage of ITIL and Web Ops, and the reason is that the domains are so very different. If there is a bug on the website, it&rsquo;s better to ship the fix now than wait for a release management process to ensure that the site won&rsquo;t have any more issues based on your fix, for example. It&rsquo;s also a bad cultural fit – in the best Web Ops teams, the focus is heavy on communication, agility, and respect, rather than process, formalism, and tooling. The shift you start too see in the really great Web Ops companies is that their operations personnel become enablers of the organization, rather than end-line blockers of change (to keep stability high.)</p>
<p>In general, I think the 4 steps outlined in visible ops for emergency management are not bad ones, but the devil is always in the details. The guy you should really ask about this is Jesse – he&rsquo;s largely responsible for Amazon&rsquo;s operational culture, and knows what it means to start hacking on that sort of thing from within huge organizations.</p>
<p><strong>AkitaOnRails:</strong> Awesome, I think this is a wrap. Thank you very much for this interview!</p>
]]></content:encoded><category>interviews</category><category>devops</category><category>automation</category></item><item><title>Chatting with Luke Kanies</title><link>https://akitaonrails.github.io/en/2009/11/18/chatting-with-luke-kaines/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2009/11/18/chatting-with-luke-kaines/</guid><pubDate>Wed, 18 Nov 2009 11:45:00 -0200</pubDate><description>&lt;p&gt;Configuration Management is a tricky subject. For non-starters, when you&amp;rsquo;re a developer and you have few boxes to take care of, you can usually get away with just managing them manually. People are probably just used to pop in a CD, double-click the &amp;ldquo;install&amp;rdquo; program and click &amp;ldquo;next&amp;rdquo;, &amp;ldquo;next&amp;rdquo; until the end, then you manually log in to backup (when you remember it), and sometimes you do apply some security updates when you remember about them.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Configuration Management is a tricky subject. For non-starters, when you&rsquo;re a developer and you have few boxes to take care of, you can usually get away with just managing them manually. People are probably just used to pop in a CD, double-click the &ldquo;install&rdquo; program and click &ldquo;next&rdquo;, &ldquo;next&rdquo; until the end, then you manually log in to backup (when you remember it), and sometimes you do apply some security updates when you remember about them.</p>
<p>But then you have more than a dozen machines, things start to get uglier, you end up making more mistakes, forgetting important steps, and all of a sudden managing machines becomes a nightmare. You end up being woken up in the middle of the night because you forgot to install some crucial component, and so on and so forth.</p>
<p><img src="http://s3.amazonaws.com/akitaonrails/assets/2009/11/18/pic2_original.jpg" alt=""  loading="lazy" /></p>
<p>The same way you need testing, continuous integration tools when you&rsquo;re a developer, you also need automated, reliable and flexible tools for the system administrator role. That&rsquo;s where tools such as <strong>Puppet</strong> kick in to help.</p>
<p>This time I&rsquo;ve interviewed <a href="http://twitter.com/puppetmasterd"target="_blank" rel="noopener">Luke Kanies</a>, from <a href="http://reductivelabs.com/"target="_blank" rel="noopener">Reductive Labs</a>, former contributor to the famous CFEngine tool and creator of <a href="http://github.com/reductivelabs/puppet"target="_blank" rel="noopener">Puppet</a>, one of the most acclaimed configuration management tools for 21st century datacenters.</p>
<p><strong>AkitaOnRails:</strong> To kick start this interview, it would be great to have more background info about you. So, how did you end up in the configuration management field? I understand that you have a long history with CFEngine development, right?</p>
<p><img src="http://s3.amazonaws.com/akitaonrails/assets/2009/11/18/luke_kanies_portrait_original.jpg" alt=""  loading="lazy" /></p>
<p><strong>Luke:</strong> I was a Unix admin going back to 1997, always writing scripts and tools to save myself time, and around 2001 I realized that I shouldn&rsquo;t have to write everything myself – that someone somewhere should be able to save me time. After a lot of research and experimentation, I settled on Cfengine, and had enough success with it that I started consulting, publishing, and contributing to the project.</p>
<p><strong>AkitaOnRails:</strong> What&rsquo;s the story behind Reductive Labs, what&rsquo;s its mission, and what&rsquo;s the story for Puppet&rsquo;s creation?</p>
<p><strong>Luke:</strong> After a couple of years with Cfengine, I had a lot more insight but was frustrated because it still seemed to be too hard – no one was sharing Cfengine code, and there were some problems it made you work really hard to solve. The biggest issue, though, was that its development was very closed – it was difficult to contribute much more than just bug fixes.</p>
<p>I got frustrated enough that I stopped consulting and looked for other options. I worked briefly at BladeLogic, a commercial software company in this space, but in the end I decided that the insight I had and the lack of a great solution were a good enough start for a business that I decided to morph my consulting company into a software company and write a new tool.</p>
<p><strong>AkitaOnRails:</strong> I&rsquo;d like to say that only amateur sysadmins do everything manually, but I think most small to medium corporations at least still do everything manually or with random scripts spread all over the place. The notion of &ldquo;configuration management&rdquo; is still new to a lot of people. Could you briefly explain what it is, and why it is important?</p>
<p><strong>Luke:</strong> It&rsquo;s surprisingly difficult to describe it succinctly, but for me, there are two key rules: You shouldn&rsquo;t need to connect directly to a given machine to change its configuration, and you should be able to redeploy any machine on your network very quickly.</p>
<p>These two rules combine to require comprehensive automation and/or centralization for everything that goes into making a machine work. Annoyingly, they also immediately introduce dependency cycles, because your automation server needs to be able to build itself, which is always a bit of a challenge.</p>
<p><strong>AkitaOnRails:</strong> I think one of the most widely used systems is CFEngine2. How does Puppet compare with it? Meaning, what do I have as value added when switching to Puppet, and what are the known caveats?</p>
<p><strong>Luke:</strong> There are multiple important functional differences. The biggest is Puppet&rsquo;s <strong>Resource Abstraction Layer</strong> , which allows Puppet users to avoid a lot of the detail they don&rsquo;t really care about, like how rpm, adduser, or init scripts work – they talk about users, groups, and packages, and Puppet figures out the details.</p>
<p>We also have explicit dependency support, which makes a huge difference – it&rsquo;s easy to order related resources and restart services when their configuration files change, for instance.</p>
<p>The language is also a bit more powerful. Like Cfengine, we have a simple custom language, but Puppet&rsquo;s language provides better support for heterogeneity, along with a composite resource construct that allows you to easily build in-language resource types like Apache virtual hosts that model more complex resources consisting of multiple simple resources.</p>
<p><strong>AkitaOnRails:</strong> Puppet has a lot of components. Could you briefly describe some of the main ones that work together? The client side, the server side, recipes?</p>
<p><strong>Luke:</strong> Most people use Puppet in client/server mode, where the central server is the only machine that has access to all of the code, and it runs a process capable of compiling that code into host-specific configurations. Then each machine runs a client (including the server), which then retrieves and applies that host-specific configuration. This has nice security implications because you&rsquo;ve not shipped your code to every machine on your network.</p>
<p>If this model doesn&rsquo;t work for you, though, it&rsquo;s also easy to run Puppet stand-alone, where each machine has all of the code and compiles it separately. Multiple Puppet users do this for various reasons. This stand-alone &lsquo;puppet&rsquo; executable is a standard interpreter – it can be used to run 1 line scripts or thousands of lines in a complete configuration.</p>
<p>Beyond this, we&rsquo;ve got a few other interesting executables, such as to access our certificate authority functionality, and an interesting executable called &lsquo;ralsh&rsquo; that provides a simple way to directly manage resources from the Unix shell, without having to write a separate script.</p>
<p><strong>AkitaOnRails:</strong> What would you say about Puppet&rsquo;s maturity? CFEngine has more than a decade of usage, which is difficult to beat. Would you say that it&rsquo;s &ldquo;mature enough&rdquo;? Meaning, it&rsquo;s already in production in companies of many sizes, its APIs don&rsquo;t change too much and my recipes will probably work if I upgrade to a newer version of Puppet? I think the 0.x version makes some people nervous :-)</p>
<p><strong>Luke:</strong> Really we should have called a version from 2007 1.0, but it&rsquo;s hard to know how stable a release is going to be until it&rsquo;s been out for a while. :)</p>
<p>It&rsquo;s obviously tough to match Cfengine&rsquo;s long life, although they&rsquo;re somewhat forcibly migrating to Cfengine 3, which is a complete rewrite, so that maturity isn&rsquo;t worth quite as much right now.</p>
<p>However, Puppet&rsquo;s been in production usage around the world since 2006, and it&rsquo;s currently used by more large companies than I could reasonably name – Twitter, Digg, Google, Sun, Red Hat, and lots more – and our community and customer base consider it to be mature. For the line came some time early last year, when I found that the vast majority of issues people had were user issues on their part, rather than some flaw or shortcoming in Puppet.</p>
<p>In general the APIs are quite stable, and we&rsquo;ve done quite well, I think, at maintaining backward compatibility when the APIs have had to change. The point about API stability in a 1.0 release isn&rsquo;t so much to differentiate it from previous efforts as to make a promise for the future. This especially matters for companies like Canonical, who want a release that they can support on Ubuntu for five years.</p>
<p><strong>AkitaOnRails:</strong> Puppet has its own language and you can use Ruby for the advanced cases. It&rsquo;s probably perfect for Rubists, but I feel that most sysadmins are used to Bash, Python and are not very flexible on change. Why did you choose to use Ruby instead of a more widespread language? What do sysadmins need to realize to shift paradigms?</p>
<p><strong>Luke:</strong> Part of it is that most people don&rsquo;t really need to know any ruby to be effective with Puppet. Sure, you can get some more power if you do, but if you&rsquo;re not a language person, you&rsquo;re perfectly functional with just Puppet.</p>
<p>Another nice thing is that we have a pretty smooth scale in terms of Ruby knowledge – you can start out writing ERB templates or five line extensions to <a href="http://github.com/reductivelabs/facter"target="_blank" rel="noopener">Facter</a>, which is our client-side querying system, and grow smoothly through to writing custom resource types.</p>
<p>In the end, I chose Ruby because I was most productive in it. I likely should have chosen Python, given its speed benefit and popularity at Red Hat and other places, but I found I just couldn&rsquo;t write code in it. I started thinking in Ruby after only a few hours of usage, so it was impossible for me to turn away from it.</p>
<p><strong>AkitaOnRails:</strong> Sysadmins used to CFEngine complain about Ruby&rsquo;s dependencies and overall weight. Because for Puppet to run you need Ruby installed. Not all distros have Ruby in the same version (although most already migrated to 1.8.7). Then you have the problem of weight. Puppet can grow to hundreds of megabytes. What they don&rsquo;t want is to have clusters of Puppet machines (which, by themselves, also need maintenance, adding to the overall complexity). How do you deal with datacenters with thousands of servers? I know it&rsquo;s difficult to measure precisely, but what would be a reasonable ratio between Puppet servers x managed servers?</p>
<p><strong>Luke:</strong> It&rsquo;s as impossible to tell you how many clients a Puppet server can handle as it is to tell you how many clients a Rails server can handle – it all depends on the complexity of the tasks. Google scaled to 4500 client machines on a single server, but most people tend to add another server at around 500-1000 clients.</p>
<p>It&rsquo;s true that it&rsquo;s hard to keep memory usage down in a Ruby process, but we&rsquo;ve made great strides in our recent releases by doing things like deduplicating strings in memory and being more efficient in our code paths. Really, though, we&rsquo;ve spent a lot more time on features and bug fixing and less time on optimizing – until recently, we&rsquo;ve been a small development team, and we just didn&rsquo;t have the bandwidth for it.</p>
<p>Now that my company, Reductive Labs, has some investment, we&rsquo;ve been able to add three full time developers, which is going to really help in this area.</p>
<p>As to dependencies, this is one area we break strongly from the Ruby community – we don&rsquo;t require a single gem, other than our own Facter tool (and it&rsquo;s usually not shipped as a gem). Rubyists tend not to worry too much about package dependencies – they just put it in vendor, as I&rsquo;m fond of saying – but that doesn&rsquo;t work when you have to deploy thousands of copies. So yes, you might have to install Ruby, but there won&rsquo;t be any other dependencies you have to deal with, which greatly simplifies it.</p>
<p>It&rsquo;s generally as tough to know how you&rsquo;ll need to size your puppetmaster as it would be to size a web server – it depends on how complicated the workload is. In general, somewhere between 500 and 5000 clients, you&rsquo;ll need to have a second server, but most people probably find it closer to 500. Really, though, if you&rsquo;ve got 3000 clients hitting a service, you probably want to make it horizontally scalable for stability in addition to performance.</p>
<p><strong>AkitaOnRails:</strong> Security is a big concern nowadays, Puppet was worried from the beginning on the handshake procedure between clients and server, can you describe it a little bit? Also, is there any built-in recipes for hardening machines, for example? Or at least any desires to add such tools in the future?</p>
<p><strong>Luke:</strong> Puppet uses standard SSL certificates for authentication and encryption, including a certificate signing phase. By default, the client generates a key and a certificate request (CSR) and then uploads the CSR to its server. This upload, along with the later certificate download, are the only unauthenticated connections that are allowed by default.</p>
<p>From there, a human normally has to trigger the client&rsquo;s certificate to be signed, but many organizations, including Google, automatically sign client certificates because they trust their internal network.</p>
<p>As to automatic hardening, there aren&rsquo;t any recipes that I&rsquo;m aware of right now, but it&rsquo;s something that I&rsquo;m definitely interested in. Years ago I was a big fan of TITAN, which is a hardening package for various *NIX platforms, and it was part of the inspiration to write Puppet – I&rsquo;ve always wanted a portable, executable security policy.</p>
<p><strong>AkitaOnRails:</strong> The puppetmaster uses Webrick by default, but the documentation also describes using Mongrel or Passenger. Are there any real gains in using those? Is it more for convenience or do we have performance/robustness improvements?</p>
<p><strong>Luke:</strong> Holy cow Webrick is slow. It&rsquo;s really fantastic for proof of concepts – get up and running in minutes. Once you get beyond that proof of concept, though, you really need to switch to Mongrel or Passenger. If you get more than one concurrent connection in webrick, your clients start to suffer, but you can scale to far more with the other solutions out there.</p>
<p><strong>AkitaOnRails:</strong> Are there any client cases you are allowed to talk about? Meaning, more details on the kind of infrastructure, difficulties, caveats, best practices?</p>
<p><strong>Luke:</strong> The possibilities here are pretty open ended. Google uses Puppet to maintain their corporate IT, which means they&rsquo;re running it on thousands of laptops and desktops which is pretty different. MessageOne, a division of Dell, is really interesting in that their developers have to ship Puppet code to manage the applications that they ship, so if an app isn&rsquo;t trimming its logs or backing itself up, it&rsquo;s a bug that the app developer has to fix rather than the sysadmin. This really helps to bridge the divide between dev and ops, which has worked out really well for them.</p>
<p>Otherwise, there are lots of stories and best practices, but I&rsquo;m afraid that would be a whole second article. :)</p>
<p><strong>AkitaOnRails:</strong> I&rsquo;ve seen Andrew Shaffer talk about <a href="http://www.slideshare.net/littleidea/agile-infra-agileroots-2009"target="_blank" rel="noopener">Agile Infrastructure</a> for a couple of years now, but I still think most IT organizations are unaware of this concept. Can you elaborate on what it means to be Agile outside of the development field?</p>
<!-- dead Flash embed removed (service offline): flash -->
<p><strong>Luke:</strong> I think Agile Infrastructure has even less adoption than Agile Development. The vast majority of IT shops haven&rsquo;t changed practices significantly in years and are largely unprepared for the growth in server count that they&rsquo;re experiencing. They mostly try to scale by adding more people, which we call the <em>meatcloud</em>, rather than scaling their tools and practice.</p>
<p><strong>AkitaOnRails:</strong> Probably related to the previous question, seems like specially after Sarbanes Oxley there&rsquo;s been an increasing interest in stuff such as ITIL, CoBit. Have you ever seen successful implementations of those in the Web-style infrastructure? I mean, I can see them succeeding in Banks, Aerospace and Defense, etc but I fail to see them working as advertised in a very dynamic environment such as Web services hosting. What are your experiences regarding this issue?</p>
<p><strong>Luke:</strong> In general, I think these kinds of high-level policies are great for filings but aren&rsquo;t so great for actually solving problems. The bigger a company is and the more public they are, the more likely they are to care about ITIL et al, but it doesn&rsquo;t really help them solve problems outside of PR in my experience. You can be ITIL compliant and dysfunctional, or completely out of compliance but in fantastic shape. Considering that the best standards are derived from implementation and best practice, which few of these are, I don&rsquo;t have a lot of hope for these being adopted by the best shops out there.</p>
<p>My personal experience is that very few companies ask or care about these standards, and the ones that do usually do so in a kind of checkbox way, in that they want to make sure they can check off things like CMDB but they aren&rsquo;t really that concerned with the specifics.</p>
<p><strong>AkitaOnRails:</strong> I think this is it! Thank you very much for this conversation!</p>
]]></content:encoded><category>interviews</category><category>devops</category><category>automation</category></item><item><title>Chatting with Hongli Lai and Ninh Bui (Phusion)</title><link>https://akitaonrails.github.io/en/2008/05/06/chatting-with-hongli-lai-and-ninh-bui-phusion/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2008/05/06/chatting-with-hongli-lai-and-ninh-bui-phusion/</guid><pubDate>Tue, 06 May 2008 21:39:00 -0300</pubDate><description>&lt;p&gt;&lt;img src="http://s3.amazonaws.com/akitaonrails/assets/2008/5/7/Picture_4.png" alt="" loading="lazy" /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Hongli Lai&lt;/strong&gt; and &lt;strong&gt;Ninh Bui&lt;/strong&gt; , from Phusion, shook the Rails world a few days ago. They unleashed the Holy Grail of Rails deployment: &lt;strong&gt;mod_rails&lt;/strong&gt; which was received with much fanfare, and they deserved it.&lt;/p&gt;
&lt;p&gt;They finally settled the big issue that &lt;a href="http://www.rubyinside.com/no-true-mod_ruby-is-damaging-rubys-viability-on-the-web-693.html"target="_blank" rel="noopener"&gt;embarrassed&lt;/a&gt; Railers in the past. This will also relieve dozens of hosting services that were clueless on how to solve this equation. Now, those two computer science students are above them all with this clever solution. And they have more to come.&lt;/p&gt;</description><content:encoded><![CDATA[<p><img src="http://s3.amazonaws.com/akitaonrails/assets/2008/5/7/Picture_4.png" alt=""  loading="lazy" /></p>
<p><strong>Hongli Lai</strong> and <strong>Ninh Bui</strong> , from Phusion, shook the Rails world a few days ago. They unleashed the Holy Grail of Rails deployment: <strong>mod_rails</strong> which was received with much fanfare, and they deserved it.</p>
<p>They finally settled the big issue that <a href="http://www.rubyinside.com/no-true-mod_ruby-is-damaging-rubys-viability-on-the-web-693.html"target="_blank" rel="noopener">embarrassed</a> Railers in the past. This will also relieve dozens of hosting services that were clueless on how to solve this equation. Now, those two computer science students are above them all with this clever solution. And they have more to come.</p>
<p>I was very fortunate to be able to interview them. I think this is the second interview, InfoQ broke the news first with this other <a href="http://www.infoq.com/news/2008/04/phusion-passenger-mod-rails-gc"target="_blank" rel="noopener">interview</a> which I highly recommend to understand more of the inner gears of Passenger. They are very easy going and it was a pleasure to talk to them.</p>
<p><img src="http://s3.amazonaws.com/akitaonrails/assets/2008/5/7/Picture_3.png" alt=""  loading="lazy" /></p>
<p><strong>AkitaOnRails:</strong> I think the first thing I&rsquo;d like to know are your backgrounds. Which one came first? <a href="http://www.modrails.com/"target="_blank" rel="noopener">mod_rails</a> or <a href="http://www.phusion.nl"target="_blank" rel="noopener">Phusion</a>? Meaning, did you have a company named Phusion that ended up developing mod_rails, or mod_rails was the reason to build a business around it?</p>
<p><strong>Phusion:</strong> Well, as you&rsquo;ve probably read by now, we&rsquo;re both computer science students at the <a href="http://www.utwente.nl/en/"target="_blank" rel="noopener">University of Twente</a> and we&rsquo;re currently in the final stages of finishing our bachelor degrees. From the beginning, we had already inferred that study can&rsquo;t teach you everything, so over the years, we&rsquo;ve also been doing a lot of work both commercially as well as for the open source community.</p>
<p>After becoming familiar with each others skills, philosophies and personal traits and from our past experience in working in IT, we thought it would be a good idea to combine our forces into a company that we could call our own, and so Phusion came to be.</p>
<p>So Phusion actually came before Passenger (mod_rails). More specifically, our intent was to start a company that strictly provides top-of-the-bill IT products, ideas and services to support our clients&rsquo; businesses. Passenger coincidentally happens to be our first product, and from what we&rsquo;ve been hearing from both the community and companies, it seems to be perfectly in line with our company&rsquo;s philosophy.</p>
<p>Also, after doing years of (web) development in other languages/frameworks such as PHP, J2EE, ASP.NET, etc. we&rsquo;ve really come to appreciate Ruby on Rails. We think it&rsquo;s really elegant and easy to use. Unfortunately deployment seemed to be out of line with the rest of the Rails experience, especially the convention over configuration part. This is why we&rsquo;ve created Passenger: we want the deployment experience to be just as smooth.</p>
<p><strong>AkitaOnRails:</strong> Is Phusion a consulting company or a product company? Right now you released one of the most interesting projects in Rails land so far, which is mod_rails. Now you&rsquo;re preparing to release Ruby Enterprise Edition. Any new products in your pipeline that you could tell us about?</p>
<p><strong>Phusion:</strong> Well, Phusion is primarily a service oriented company and we think our mission statement answers this question best:</p>
<p><em>&ldquo;To exclusively provide top-of-the-line products, services and ideas, in order to support our clients&rsquo; businesses.&rdquo;</em></p>
<p>If our clients&rsquo; businesses require consultancy, then we&rsquo;re able to provide that as well seeing as we&rsquo;ve had quite a few years of experience in that particular line of business.</p>
<p><img src="http://s3.amazonaws.com/akitaonrails/assets/2008/5/7/Picture_1.png" alt=""  loading="lazy" /></p>
<p>In order to achieve this mission, it is crucial for us to be well-versed in a variety of skills and we&rsquo;re well aware of the saying <em>&ldquo;jack of all trades, master of none&rdquo;</em>. It is for this reason that our team comprises of only highly skilled people from those parts of the industry that are of particular interest to our clients&rsquo; businesses. The reader will probably note that this allows for a dynamic configuration in organization as well as keeping the work challenging: we don&rsquo;t believe in <a href="http://thedailywtf.com/Articles/Up-or-Out-Solving-the-IT-Turnover-Crisis.aspx"target="_blank" rel="noopener">keeping people around forever for no rational reasons</a>, so it is crucial for our employees to keep learning within our organization (hence the alias, <em>&ldquo;the computer science company&rdquo;</em>). This not only opens up a lot of doors on the innovation side, but also results in people becoming masters in several areas. So <em>&ldquo;jack of all trades, master of none&rdquo;</em> may hold true, we believe <em>&ldquo;master of a lot of areas&rdquo;</em> could just as easily hold true. Needless to say, this is crucial since computer science and IT cover such a wide area, and this particularly holds true for our clients&rsquo; businesses.</p>
<p>Ruby Enterprise Edition has actually been called to life as a result of Passenger: it strives to dramatically reduce memory usage of Ruby on Rails applications that are run through Passenger.</p>
<p>As for new products, we&rsquo;re currently working on a few subscription-based web services. Our friend David Heinemeier Hansson has recently given a talk on startups which elaborates on the reasons why one would want to follow such a path. We&rsquo;d highly recommend the reader to <a href="http://www.justin.tv/hackertv/97862/DHH_Talk__Startup_School_2008"target="_blank" rel="noopener">check that out</a> and for strategic reasons, we&rsquo;d like to leave it at this for now.</p>
<p><img src="http://s3.amazonaws.com/akitaonrails/assets/2008/5/7/2400947055_2ce7bcf70b.jpg" alt=""  loading="lazy" /><br>
Hongli Lai</p>
<p><strong>AkitaOnRails:</strong> For a long time the whole Rails community was rather too much comfortable with the Mongrel solution. When Zed Shaw ranted about the community, it was a wake up call and from there we saw alternatives emerging overnight such as Thin and Ebb. What did you think about that situation back then?</p>
<p><strong>Phusion:</strong> From what we understand, solutions such as Mongrel, Thin and Ebb fall under the same conceptual category. That is, for the end-user, they work in a similar way, even though they are technically different. In other words, Thin and Ebb seem to be like <em>&ldquo;Mongrel, but faster/smaller&rdquo;</em>.</p>
<p>Passenger however has taken an approach to deployment that&rsquo;s different from Mongrel et al: it intends to make deployment as simple as possible, and to lower system maintenance overhead as much as possible. For example, while Passenger handles this automatically, Mongrel, Thin and Ebb all require the administrator to manage clusters. As you&rsquo;ve stated, the tech savvy have already become quite familiar with the latter, but one also needs to take the newcomers into account. For them, it&rsquo;s quite an obstacle to overcome to just simply deploy an application. If we were to consider the backgrounds of these newcomers, many of them are probably already familiar with the PHP way of deployment, that is, in an upload-and-go manner. With Passenger we&rsquo;ve tried to accommodate these people, as we think that this is important to the growth of the Ruby on Rails market. That doesn&rsquo;t however say that we haven&rsquo;t taken issues such as robustness, performance and stability into account.</p>
<p>As a matter of fact, independent tests have already shown that Passenger is on par or faster than the aforementioned solutions. Another problem is that shared hosts are already running Apache as their main web server, and we&rsquo;ve already seen what configuration needs to be done in order to get a Mongrel cluster up and running with Apache… Needless to say, this is quite tedious and error-prone, even for many tech savvy to say the least. Passenger literally reduces this process to a matter of seconds instead of minutes. This should not only save a hosting company a great deal of time but should also keep their server administrators sane (and from our <a href="http://blog.dreamhost.com/2008/01/07/how-ruby-on-rails-could-be-much-better/"target="_blank" rel="noopener">experience</a>, they&rsquo;re the last people you want to offend ;-)).</p>
<p>Also, setting up a Mongrel-like cluster requires quite some resources on a shared host, and it is for reasons such as these that the costs of shared Ruby on Rails hosting plans are usually higher than that of PHP hosts. Passenger however, takes on this issue as well by providing improved efficiency over the aforementioned solutions, especially in conjunction with Ruby Enterprise Edition. Early testing has already shown that the latter has resulted in a dramatic reduction in memory usage and we&rsquo;ll elaborate on this soon.</p>
<p>If we consider these factors, it becomes apparent that Passenger (in conjunction with Ruby Enterprise Edition) has the potential to unleash a revolution within shared hosting. Not only in cost-reduction for hosting companies (and hopefully consumers), but also in increasing the popularity of Ruby on Rails.</p>
<p><strong>AkitaOnRails:</strong> mod_ruby seems to have stalled for a long time and needs some love. It is said that it works reasonably well for small websites but lots of &lsquo;black magic&rsquo; in Rails makes it not suitable for mod_ruby. What makes mod_rails different than mod_ruby, in the fundamental way they operate with complex frameworks as Rails?</p>
<p><strong>Phusion:</strong> We&rsquo;re not really able to comment on that, because we&rsquo;re not intimately familiar with mod_ruby. However, mod_ruby is not actively maintained, and there are a lot of negative comments about mod_ruby in general (such as on the Ruby on Rails wiki). It also seems that nobody really uses it. It is for these reasons that we&rsquo;ve never really given mod_ruby a try.</p>
<p>From the aforementioned and what people often tend to forget, is that only delivering a(n excellent) product will not suffice. Marketing and promotion are really important for a product&rsquo;s acceptance. We&rsquo;ve not only done a lot of development, but also spent a lot of time in promoting Passenger. The fact that we&rsquo;re having this interview right now is probably the result of our promotion campaign, so we&rsquo;re quite happy with that. In this regard, mod_ruby doesn&rsquo;t seem to be actively promoted, and perhaps this is one of the reasons why it&rsquo;s relatively unknown.</p>
<p><strong>AkitaOnRails:</strong> why do you think it took so long for someone to come up with a solution for an Apache module that could operate similarly to the usual mod_php or mod_python? What did you think was the most challenging aspect in the development of mod_rails?</p>
<p><strong>Phusion:</strong> Well, as you&rsquo;ve stated, we believe that experienced Ruby on Rails developers don&rsquo;t consider deployment to be a problem; probably annoying at most. They&rsquo;ve automated/standardized the deployment process for a great deal with Capistrano scripts. Newcomers however see deployment as an insurmountable problem. Consequently, we end up having a situation in which the people who can solve the problem either don&rsquo;t see a problem or aren&rsquo;t motivated enough to fix the problem, while the people who do see the problem cannot solve it.</p>
<p>Furthermore, from our personal experiences, newcomers usually want to see immediate results. It&rsquo;s very demotivating for those people to continue using Ruby on Rails if they have to manage tons of obscure configurations and clusters to <em>&lsquo;just successfully deploy an application&rsquo;</em>. This leads to very intelligent conclusions, such as that &ldquo;Ruby on Rails sucks&rdquo;. ;-)</p>
<p>The most challenging aspect is probably cross-platform support and to elaborate on this in a nutshell:</p>
<ul>
<li>We have to support a whole bunch of Unix-based systems, like Linux, BSD and OS X. Passenger works on a fairly low-level (i.e. it uses advanced Unix system features), and different systems tend to behave in subtly different ways such that they can break Passenger if we&rsquo;re not careful.</li>
<li>A lot of challenges are created by MacOS X and Apache. There are literally tons of different ways in which Apache can be compiled and installed, and this is especially true on MacOS X. Every single Apache installation can be slightly different, and we have to support all of them. We had to write hundreds of lines of Apache auto-detection code in order to make the installation as smooth as possible. And support for OS X-specific things such as universal binaries didn&rsquo;t make the challenge any easier.</li>
</ul>
<p><img src="http://s3.amazonaws.com/akitaonrails/assets/2008/5/7/134717629_6_Q1hc.jpeg" alt=""  loading="lazy" /><br>
Ninh Bui</p>
<p><strong>AkitaOnRails:</strong> the usual Shared Hosting approach with Rails involves FastCGI, I think that&rsquo;s where mod_rails will make a big impact first. In the VPS arena people are usually more comfortable just loading up a mongrel cluster, maybe a monit or god to monitor them or even use Swiftply to better control mongrel instances. There&rsquo;s some tweaking involved in having permanent Ruby interpretors running, that&rsquo;s mainly because of the fact that if at some point the VM needs more memory, it will allocate as much as it needs, but not return it back to the OS. Does mod_rails have something to avoid a monitoring system like Monit or is it still needed?</p>
<p><strong>Phusion:</strong> We&rsquo;ve taken this challenge into account as well. Rails applications that have been idle for a while are shut down, thereby releasing all of their resources to the operating system. Furthermore, it is possible to use Passenger in conjunction with monitoring tools such as Monit. If Monit kills a Rails process, then Passenger will automatically (re)start it when needed, as if nothing bad ever happened.</p>
<p><strong>AkitaOnRails:</strong> I understand that mod_rails operates more or less like a manager for a pool of Ruby VMs. You have a minimum and maximum number of processes allowed, mod_rails starts as many as it needs depending on load and if a process is sitting idle, it will kill it after a timeout period. Most of the time a minimum amount of processes will stay running. Is that how it operates?</p>
<p><strong>Phusion:</strong> It is as you describe it, with the exception that there&rsquo;s no such thing as a minimum amount of processes. The reason for this is that, once an application for a certain Rails directory is spawned, that application process cannot be reused for a different Rails directory. This makes the concept of a minimum amount quite useless, except in servers that only host a single Rails application.</p>
<p>Of course, not having a minimum amount of processes will mean that the first request after the idle timeout will undergo a performance penalty and be slow. We&rsquo;ve solved this issue by using spawner servers. Spawner servers cache Ruby on Rails framework code and application code, and they can reduce the spawn time by about 50% and 90%, respectively. You might have noticed these spawner server processes in the output of &lsquo;ps&rsquo;. The secret in reducing spawn time would lie in altering the idle timeouts of these spawner servers. We are currently working on a series of articles, in which we elaborate on the spawn times of Rails applications, what the exact roles of the spawner servers are, and how system administrators can optimize server settings to reduce spawn time.</p>
<p>Much of the magic that resides in Passenger still needs to be unveiled and we&rsquo;ve only just started, but this is mainly for the interested people out there, i.e. the tech savvy. There is a saying in Dutch that summarizes the thought behind why we haven&rsquo;t done this up till now: <em>&ldquo;Goede wijn behoeft geen krans&rdquo;</em>, which would roughly translate to that a good product does not need any supporting elaboration. A scientific publication on the other hand does need this kind of elaboration and it is also for this reason that we&rsquo;re writing these articles as well.</p>
<p><strong>AkitaOnRails:</strong> By staying in the same memory space as Apache itself you don&rsquo;t have the overhead of a FastCGI or HTTP call over to a mongrel instance. Does this difference account for much in your benchmarks? Usually, in your experience, how much faster is mod_rails right now compared to a bare bone mongrel cluster running the same apps? Are the gains in speed constant or it varies enough that it is not really measurable?</p>
<p><strong>Phusion:</strong> This is actually not true: the Rails processes don&rsquo;t reside in the same memory space as Apache. Instead, they really are separate processes, that each can be individually shut down without affecting all the others. It is not a very good idea to embed a Ruby interpreter in Apache because of various issues, such as stability, heap fragmentation and unnecessary bloating of a process&rsquo;s VM size. From what we understand, mod_ruby takes the approach of embedding Ruby inside Apache, and we suspect that this is one of the reasons why people are reporting so many problems with it.</p>
<p>Our own tests have shown that Passenger&rsquo;s performance is faster than a Mongrel cluster behind Apache+mod_proxy_balancer. Independent tests have shown that Passenger&rsquo;s performance is either on par with or slightly faster than Mongrel cluster and this seems to be perfectly in line with what we&rsquo;ve been able to infer. So far we haven&rsquo;t seen any tests in which Passenger is slower than a Mongrel cluster. The speed gains are not constant: they are dependent on the specific application.</p>
<p><img src="http://s3.amazonaws.com/akitaonrails/assets/2008/5/7/2400947461_dfa24d2212.jpg" alt=""  loading="lazy" /><br>
Phusion folks at <a href="http://www.fngtps.com"target="_blank" rel="noopener">Fingertips</a></p>
<p><strong>AkitaOnRails:</strong> Ruby Enterprise Edition, for what I understand, is the outcome of that &lsquo;copy-on-write&rsquo; series we talked about. Essentially a way to patch the Ruby VM to enable a modified GC that won&rsquo;t mark every object, thus allowing COW to do its work and save a considerable amount of memory. You stated 33%. I would assume that it is &lsquo;up to 33%&rsquo;, right? What&rsquo;s the usual range of saved memory you experience in your tests? What affects this the most in typical Rails apps, any gotchas worth mentioning for Rails developers?</p>
<p><strong>Phusion:</strong> The mentioned 33% is not a maximum, but an average. We&rsquo;ve tested with real-life applications such as Typo, and so far we&rsquo;ve inferred an average memory saving of 33%. The actual memory savings is dependent of the specific application so your mileage may vary.</p>
<p>There are no gotchas for application developers. If their application works with Passenger, then Ruby Enterprise Edition will work as well, without any need for complicated configuration options. In other words, it&rsquo;s a transparent augmentation. In any case, we can guarantee you that memory usage will be no higher than when you use standard Ruby.</p>
<p><strong>AkitaOnRails:</strong> I understand that you will talk more about the Enterprise Edition at RailsConf but I was hoping that you could disclose a little bit more about it. You are just finishing the second batch of donations for Enterprise licenses (I donated! :-). First things first: when are you going to release it? Is this product supposed to stay commercial only or do you intend to collaborate the GC patch back to the Ruby trunk one day?</p>
<p><strong>Phusion:</strong> Thanks for donating, and as for the latter: yes, it is our intention that it will one day be merged back upstream. The alert reader will probably notice that we&rsquo;ve dodged one question like Neo dodged Agent Smith&rsquo;s bullets in the Matrix: skilled, except for the last bullet, that one actually hit, but enough about that ;-).</p>
<p><strong>AkitaOnRails:</strong> &ldquo;mod_rails&rdquo; is actually the alias for &ldquo;Passenger&rdquo;, some people are asking if this is going to stay Rails only or if you intend to make it Rack-compatible so other frameworks as Merb and Ramaze could run under its control. Is that possible? Are you considering it?</p>
<p><strong>Phusion:</strong> Let&rsquo;s just say that we would prefer you to call it Passenger instead of just mod_rails eventually. ;-)</p>
<p><strong>AkitaOnRails:</strong> Apache is the powerhouse for Web servers and it makes a lot of sense for an Apache module like that. Maybe I am speculating too much, but do you think it is possible to refactor it into other web server&rsquo;s modules like nGinx or Litespeed? Because of the lack of a good mod_rails in the past, the Rails community drifted away to other alternatives. Is it reasonable to expect non-Apache modules?</p>
<p><strong>Phusion:</strong> It&rsquo;s technically possible. To keep you guys in suspense and to keep you speculating on such matters, we won&rsquo;t dive into this further for the time being. ;-)</p>
<p><img src="http://s3.amazonaws.com/akitaonrails/assets/2008/5/7/2401777262_a7fa751c79.jpg" alt=""  loading="lazy" /><br>
Passenger being installed!</p>
<p><strong>AkitaOnRails:</strong> The way it is right now, it feels like mod_rails is a drop-in replacement for whatever strategy we had before using Apache. Are there any gotchas worth mentioning for anyone planning to migrate? I think complicated mod_rewrite rules would be the first thing?</p>
<p><strong>Phusion:</strong> There&rsquo;s a minor gotcha. By default we override mod_rewrite the best we can. This is because Rails projects come with a .htaccess file by default, and this .htaccess contains mod_rewrite rules, which result in all requests being forwarded to CGI or FastCGI.</p>
<p>This is trivially solved by deleting the default .htaccess and by enabling a configuration option which tells Passenger not to override mod_rewrite. This allows the developer to use arbitrary mod_rewrite rules to his liking.</p>
<p>Other than that, the remaining gotcha would be that rails deployment has become quite boring thanks to Passenger, or so people say. ;-)</p>
<p>On a final note, we&rsquo;d like to thank all you guys out there for the love and (financial) support on Passenger. We really appreciate it!</p>
<p>With kind regards we are, your friends at Phusion,</p>
<p>Hongli Lai</p>
<p>Ninh Bui</p>
]]></content:encoded><category>interviews</category><category>rails</category><category>devops</category></item><item><title>Chatting with Jamis Buck</title><link>https://akitaonrails.github.io/en/2007/08/06/chatting-with-jamis-buck/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2007/08/06/chatting-with-jamis-buck/</guid><pubDate>Fri, 03 Aug 2007 22:46:00 -0300</pubDate><description>&lt;p&gt;&lt;img src="http://s3.amazonaws.com/akitaonrails/assets/2007/8/3/245186199_71f55cc3c7.jpg" alt="" loading="lazy" /&gt;&lt;/p&gt;
&lt;p&gt;This is another big interview. This time with Jamis Buck, the programmer that helped David Hansson himself right at the beginning of Rails, at 37signals.&lt;/p&gt;
&lt;p&gt;Today most renowned for his achievements in Capistrano and a lot of other Ruby libraries as sqlite-ruby bindings and Net::SSH. Jamis was very kind to give us the opportunity to know more about his career and the beginnings of the Ruby on Rails story.&lt;/p&gt;</description><content:encoded><![CDATA[<p><img src="http://s3.amazonaws.com/akitaonrails/assets/2007/8/3/245186199_71f55cc3c7.jpg" alt=""  loading="lazy" /></p>
<p>This is another big interview. This time with Jamis Buck, the programmer that helped David Hansson himself right at the beginning of Rails, at 37signals.</p>
<p>Today most renowned for his achievements in Capistrano and a lot of other Ruby libraries as sqlite-ruby bindings and Net::SSH. Jamis was very kind to give us the opportunity to know more about his career and the beginnings of the Ruby on Rails story.</p>
<p><strong>AkitaOnRails:</strong> Ok, so let’s get started. first thing I always ask my guests: what’s your background? I mean, when, how, why did you start on computer programming? Did it begin the classic way, as a hobby, or was it a more recent thing because of career or something else?</p>
<p><strong>Jamis Buck:</strong> I started programming when I was in the 10th grade, when my mom got a brand-spanking-new Tandy personal computer. It had all of 20 megs of disk space, and came with <a href="http://en.wikipedia.org/wiki/GW-BASIC"target="_blank" rel="noopener">GW-BASIC</a>. I’d done some programming before, turtle-graphics and a bit of BASIC, but that Tandy was when I got serious about it. I taught myself GW-BASIC from the little reference book the computer came with and wrote a few simple games and utilities. Then I taught myself Turbo Pascal and Turbo C++ in 11th and 12th grades, and it’s gone from there :)</p>
<p><strong>AkitaOnRails:</strong> Then you decided to go for CS in college? I read at your profile in <a href="http://www.workingwithrails.com/person/5309-jamis-buck"target="_blank" rel="noopener">WorkingWithRails</a> that you’re a repentant Java programmer? How long have you been programming in Java?</p>
<p><strong>Jamis Buck:</strong> Yeah, I then jumped into Computer Science when I got to college. I graduated a bit late, in 1999, after working full-time for the university (<a href="http://www.byu.edu/webapp/home/index.jsp"target="_blank" rel="noopener">BYU</a>) as a programmer for a couple of years. That was where I did all my Java work. We were actually doing web-apps in C, which was not as insane as it sounds! But then we had a change of leadership when a new CIO came in, and he basically issued a blind mandate: <em>“learn Java, ditch C!”</em> So we had to massively retool and get certified and so forth. I was on the team that led the research into what Java tools we should use, and so got a lot of experience that way. I did Java work full-time there for about 2 years, until <a href="http://37signals.com/"target="_blank" rel="noopener">37signals</a> came along in Feb 2005 and made me a better offer, and I’ve never looked back!</p>
<p><strong>AkitaOnRails:</strong> Wow, so you jumped right in from Java in the university to Rails at 37signals? This is a little earlier than I expected, but as you already mentioned it: how did you get in touch with the 37signals guys? How did you start on Ruby being so overwhelmed with Java before?</p>
<p><strong>Jamis Buck:</strong> Well, I’d been involved with Ruby (primarily as a hobby) since about 2001. I’d written a few libraries, like the <a href="http://sqlite-ruby.rubyforge.org/"target="_blank" rel="noopener">sqlite</a> and sqlite3 bindings, and at RailsConf 2004, I met DHH. I did a bit of work at the conference there to make Rails speak to sqlite databases. After that, around November 2004, I think, David asked if I would be interested in doing some consulting for 37signals. In December and January I did a few odd jobs for them like adding SFTP and time zone support to Basecamp. And then in February they flew me to Seattle to attend the <a href="http://www.37signals.com/workshop-062504.php"target="_blank" rel="noopener">Building of Basecamp</a> workshop there and they made me an offer.</p>
<p><strong>AkitaOnRails:</strong> I understand that 37signals is a small company in terms of number of employees and most of its inner power comes from this very fact. Did it change much since you began, I mean, in the last 2 ~ 3 years? What did you like the most back then and what do you like the most right now?</p>
<p><img src="http://s3.amazonaws.com/akitaonrails/assets/2007/8/3/conf_room.jpg" alt=""  loading="lazy" /></p>
<p><strong>Jamis Buck:</strong> Well, when I came on, I was the 5th employee, and the second programmer. Previous to my coming, David did all the programming and sysadmin work, so I think it was a big relief to him to have someone to share the load! About a year later, we hired two more programmers, and we’ve since hired two more people, including a full-time sysadmin. The changes have been for the better :) I like having a real sysadmin. I can’t tell you how much I hated being a sysadmin, since I had little idea what I was doing!</p>
<p><strong>AkitaOnRails:</strong> You seem to be the kind of guy that like the <em>kitchen-sink</em> because you’ve been involved with ‘low-level’ stuff like sqlite-bindings, Net::SSH and so on. Is this the kind of thing that excites you the most? Or it’s just a coincidence and you actually rather do GUI work as well?</p>
<p><strong>Jamis Buck:</strong> I actually swore, some years ago, that I would never be a web-app developer :) I’ve always preferred writing the tools as opposed to writing the apps. But I’ve really enjoyed writing web-apps at 37signals. The team is great and the toolset (Rails!) is amazing. I still do enjoy writing libraries (working on <a href="http://raa.ruby-lang.org/project/net-ssh/"target="_blank" rel="noopener">Net::SSH</a> and <a href="http://raa.ruby-lang.org/project/net-sftp/"target="_blank" rel="noopener">Net::SFTP</a> v2 right now, in fact) but apps aren’t so bad either :)</p>
<p><strong>AkitaOnRails:</strong> But back then when you were literally second-in-command you didn’t have much choice, did you? I bet you did some of the Basecamp interfaces? And you mentioned that you didn’t like much of the sysadmin work because you didn’t know what you were doing, but now you probably are an expert because Capistrano rocks in this field. I remember it being called SwitchTower before. Can you tell us its story?</p>
<p><strong>Jamis Buck:</strong> I’ve done quite a bit of the work on all of the apps, some more than others. I don’t actually write the UI’s (that’s the domain of Jason Fried and Ryan Singer), but I’ve plugged them in and made them work.</p>
<p>I learned a ton about sysadmin work from when I had to do it. I’m still a major newbie at it but I learned more than I ever thought I’d learn about how mysql really works, for instance :)</p>
<p>When I first released Capistrano, a year and a half ago or so, it was called SwitchTower, but around March 2006 I got a <a href="http://en.wikipedia.org/wiki/Cease_and_desist"target="_blank" rel="noopener">C&amp;D</a> from a company that had the name trademarked, and so I had to scramble for a new name. The Rails Core Team helped immensely at that time, they offered a TON of suggestions for names. Marcel eventually IM’d me and suggested “Capistrano” which just clicked for me and I went with it. I got some flack from some people, who thought the name was lousy, but I laugh now because most people that use cap today probably came to it after the name change and never knew any other name, and they don’t even think twice about it.</p>
<p><img src="http://s3.amazonaws.com/akitaonrails/assets/2007/8/3/logo-big.png" alt=""  loading="lazy" /></p>
<p><strong>AkitaOnRails:</strong> You are probably very overwhelmed now because of the Capistrano 2.0 release. It is a major departure from the 1.4.1 if I understand correctly. You rewrote a lot of stuff, had to break backward compatibility. Do you feel you got what you wanted with this release or are you aiming for more in a 3.0 release?</p>
<p><strong>Jamis Buck:</strong> Cap2 is pretty much just what I wanted it to be. There are few minor tweaks in the pipeline, but nothing significant. It is about 90% backwards compatible with Cap1, so most people should have no problems switching. And since the 2.0 release, the maintenance load for cap has been really small. I’m very pleased with it. I still need to get busy with documentation though :) That’s the thing that still holds a lot of people back.</p>
<p><strong>AkitaOnRails:</strong> What is it that you like the most about cap? You obviously use it at your own deployments at 37signals. I fantasize you having a bunch of shell scripts more than a year ago and then weaving them together to build what was the first release of Capistrano.</p>
<p><strong>Jamis Buck:</strong> Prior to SwitchTower/Capistrano, we had a tool that David had written for deploying Basecamp. Back then, Basecamp ran on a single server, so deployment was little more than “svn up”. Later, we moved Basecamp to multiple servers and launched Backpack, and we realized we needed something more powerful for deployments. So I was tasked with writing something and Capistrano was the result. The thing I like most about it is how it takes the tedium out of being a sysadmin. The “cap shell”, in particular, lets you immediately and easily execute arbitrary commands on multiple servers, simultaneously, which is really handy. Of course, “cap deploy” is nice too :)</p>
<p><strong>AkitaOnRails:</strong> I remember one episode where David reported about a 72 hours glitch that let Basecamp out. Were you involved in that episode? :-) Headaches like this seems funny when you remember about them now, but obviously not so funny at the time.</p>
<p><strong>Jamis Buck:</strong> ha, yeah, that actually happened a few days after I was hired :) We were adding UTF-8 support to Basecamp and the database migration did not go well. We pulled an all-nighter. Quite the introduction to 37signals’ culture :) Fortunately, that kind of thing hasn’t happened since, though we’ve had our share of other emergencies, of course.</p>
<p><strong>AkitaOnRails:</strong> Lessons learned? I think that’s the most important outcome of these situations :-)</p>
<p><strong>Jamis Buck:</strong> The main lesson I learned that time: when you’re mucking with charsets, make sure you test it on as many non-Latin1 alphabets as possible :) Naturally, we tested it a lot before we deployed, but we didn’t really think to check any Russian or Japanese data that was in our database after the test, which seems obvious in hindsight. We did try some non-Latin1 alphabets during testing but the ones we tried just happened to work in spite of the bugs. Another lesson learned: no matter how hard you prepare, something’s going to fall through the cracks!</p>
<p><img src="http://s3.amazonaws.com/akitaonrails/assets/2007/8/3/245186199_71f55cc3c73.png" alt=""  loading="lazy" /></p>
<p><strong>AkitaOnRails:</strong> I headed a group of very committed volunteers and got your <a href="http://gettingreal.37signals.com/GR_por.php"target="_blank" rel="noopener">Getting Real</a> book translated into Brazilian Portuguese a while back. I believe you were involved in it too? This book is iconic to picture the 37signals spirit I guess. Do you really move the way it’s written? :-)</p>
<p><strong>Jamis Buck:</strong> I was actually not too involved in the composition of the book, though I gave some feedback during the writing process. But yes, that book describes our own processes. There really isn’t anything in that book that we don’t actually do.</p>
<p><strong>AkitaOnRails:</strong> How do you operate? I believe that not everybody work in the same physical location? Do you do a lot of remote work? How many programmers/sysadmins/designers are there now?</p>
<p><strong>Jamis Buck:</strong> Chicago is definitely the “heart” of 37signals: Jason, Ryan, David, and Sam all live and work there. We have an office there as well, and the Chicagoites go there pretty regularly to work (though they also work from home some, too) Matt Linderman is our writer, he lives and works in NYC. Mark Imbriaco, our sysadmin, lives and works in Chesapeake, Virginia and Jeremy Kemper is another programmer, living in Pasadena, California now. The distributed nature of it works very well for us. I really love working from home, it lets me be more involved in my kids’ lives, for instance, than I would otherwise be.</p>
<p><img src="http://s3.amazonaws.com/akitaonrails/assets/2007/8/3/building_djd.jpg" alt=""  loading="lazy" /></p>
<p><strong>AkitaOnRails:</strong> I hear you’re from Provo, Utah? It is a coincidence as my current boss, <a href="/en/2007/7/20/conversando-com-carl-youngblood">Carl Youngblood</a>, is also from there and also studied at BYU :-) You don’t possibly know each other, do you?</p>
<p><strong>Jamis Buck:</strong> I actually live in Caldwell, Idaho now, but I was in Provo for about 8 years and yes, I do know Carl! We were both in the <a href="http://groups.google.com/group/urug?hl=en"target="_blank" rel="noopener">Utah Ruby Users Group</a>. He’s a great guy.</p>
<p><strong>AkitaOnRails:</strong> You attended many conference for the Ruby community. I don’t remember if you wrote any books. Are you writing some now?</p>
<p><strong>Jamis Buck:</strong> I was originally going to write a book about Capistrano for Apress, but that’s fizzled. I’m not actively involved in any book writing at the moment, and don’t really have much interest in it, honestly. I much prefer writing software! And I try to post to my blog once in awhile, too.</p>
<p><strong>AkitaOnRails:</strong> Yes, the <a href="http://weblog.jamisbuck.org/"target="_blank" rel="noopener">buckblog</a>, it has lots of great tips that I like to read. Did you start it because of Rails or you already had a Java blog back then?</p>
<p><strong>Jamis Buck:</strong> I started it pre-Rails. If you read the early articles, you’ll see they were pretty eclectic. I was still trying to figure out this whole “blog” thing :) I’ve since come to focus on software-development and use my <a href="http://family.jamisbuck.org"target="_blank" rel="noopener">family blog</a> for more personal posts these days.</p>
<p><strong>AkitaOnRails:</strong> I was a Java programmer myself and I still try to maintain myself informed about what’s going on in other communities as Java, some PHP, Python. What do you think or feel about other frameworks and technologies as CakePHP, Avi Bryant’s <a href="http://www.seaside.st/"target="_blank" rel="noopener">Seaside</a> or even Django?</p>
<p><strong>Jamis Buck:</strong> Honestly, I’m a bit ashamed to admit that I don’t keep up on other frameworks as much as I should. Seaside has some neat ideas and my desire to improve my Smalltalk programming skills has made me want to poke into that further. But other than that, I haven’t shopped around much (to my own personal detriment, I’m sure).</p>
<p><strong>AkitaOnRails:</strong> And still about Java, I am very excited nowadays about JRuby. I interviewed <a href="/en/2007/06/21/conversando-com-ola-bini-jruby-core-team-member">Ola Bini</a> and I’m also kind of stalking Charles Nutter. Did you know the guys? Did you got interested in trying it?</p>
<p><strong>Jamis Buck:</strong> I know who they are, and I’ve met Charles at a few conferences, but I’ve never really had a chance to talk to them in depth. JRuby definitely sounds exciting. I’m very curious, in fact, to know whether it can support Net::SSH or not since that would mean you could run Capistrano under it. But I’ve not had a chance to actually try it out.</p>
<p><strong>AkitaOnRails:</strong> Insisting on the Java subject, I posted a while back about some discussions about Design Patterns pointing to <a href="/2007/07/30/gof-design-patterns-sobreviveu-ao-teste-do-tempo/">flaws in languages</a>, and I just read <a href="http://weblog.jamisbuck.org/2007/7/29/net-ssh-revisited"target="_blank" rel="noopener">a post</a> you wrote about Dependency Injection and your Net::SSH library. What do you think about Design Patterns now that you’re a full-time Ruby programmer?</p>
<p><strong>Jamis Buck:</strong> Design Patterns are wonderful. They give us a language to talk more concisely about programming. It is so much easier to say “use a factory method” than to circumlocate and confuse each other. As for specific design patterns, some are more useful than others in different environments. As I wrote in my post, Dependency Injection is just not something you need in Ruby, but in environments like Java, it can be a life-saver.</p>
<p><img src="http://s3.amazonaws.com/akitaonrails/assets/2007/8/3/276456883_dd0ae43334.jpg" alt=""  loading="lazy" /></p>
<p><strong>AkitaOnRails:</strong> About 37signals again, you are very committed to Apple products. I saw that video on <a href="http://www.apple.com/education/whymac/compsci/video.html"target="_blank" rel="noopener">Apple Education</a> with Jason and David. And now we see that <a href="http://weblog.rubyonrails.org/2006/8/7/ruby-on-rails-will-ship-with-os-x-10-5-leopard"target="_blank" rel="noopener">Leopard</a> will bring not only Ruby but also Rails, Mongrel and even Capistrano. How is your relationship with Apple?</p>
<p><strong>Jamis Buck:</strong> I wouldn’t say there is any “relationship” with Apple. We’re big fans of their designs and interfaces and I think that reflects in our own work. Apple itself is including Rails, Mongrel, and Capistrano in the wake of those tools’ successes. Mac OS X really is a powerful environment for programmers and adding those tools “out of the box” makes it even more so. It makes sense to include the popular Ruby tools, too, since Ruby is going to be a <a href="http://rubycocoa.sourceforge.net/HomePage"target="_blank" rel="noopener">first class citizen</a> when it comes to developing OS X applications, thanks to the adoption by Apple of the Cocoa bindings for Ruby.</p>
<p><strong>AkitaOnRails:</strong> You obviously use a Mac for your work. What are the tools you use the most besides Textmate?</p>
<p><strong>Jamis Buck:</strong> Well, Capistrano :) Firefox w/ Firebug, Parallels (for IE testing), iTerm (though I’m looking forward to the new Terminal.app in Leopard), <a href="http://www.campfirenow.com"target="_blank" rel="noopener">Campfire</a> is indispensable, AdiumX, NetNewsWire, Knox (for simpler encrypted filesystem access). I still fall back and use vim pretty often too, for quick edits. There are probably others :) But I can’t think of them off the top of my head.</p>
<p><strong>AkitaOnRails:</strong> There are a lot of talk about <a href="http://en.wikipedia.org/wiki/Getting_Things_Done"target="_blank" rel="noopener">GTD</a> nowadays. I think I saw your profile at 43folders. Are you into this kind of organization philosophy as well? How do you maintain your routine organized? Many people (myself included) have a hard time differentiating work from hobby and keeping ourselves within the project’s deadlines and such.</p>
<p><img src="http://s3.amazonaws.com/akitaonrails/assets/2007/8/3/502950270_7175d894c8.jpg" alt=""  loading="lazy" /></p>
<p><strong>Jamis Buck:</strong> I’m not a GTD fanatic. I guess I try to be pretty pragmatic about it. I’m terrible about procrastinating, though. Mostly, from 8-5 I try to focus on the tasks that have been assigned to me and outside of that, I work on my hobbies… which happen to overlap work sometimes :) I’m really the wrong person to ask about being organized! If it weren’t for my wife, I think I’d get very little done, actually.</p>
<p><strong>AkitaOnRails:</strong> What are you most interested nowadays besides Rails, Capistrano? Not only technology-wise. Any hobbies?</p>
<p><strong>Jamis Buck:</strong> I’ve lately become very interested in military history, which has lead me to read about the American Civil War among other things. My wife and I are really enjoying Harry Potter #7 right now, too :) And I’ve become quite attached to my Nintendo Wii lately, as well! I used to be quite into role playing games, but haven’t done that for a few years. I’ve still got a ton of books for <a href="http://en.wikipedia.org/wiki/Dungeons_&amp;_Dragons"target="_blank" rel="noopener">D&amp;D</a>, though, and maybe someday I’ll dust them off :) I enjoy spending time with my kids (Nathaniel is 5, and Katie is 3). We’ll be going camping for a few days in a week or two, actually. Always something to keep me busy!</p>
<p><strong>AkitaOnRails:</strong> Haha, I see. And do you travel a lot? Outside of the US, I mean.</p>
<p><strong>Jamis Buck:</strong> Not really. Last year I went to London for the first time, for RailsConf there, and this year I went to the Czech Republic. But aside from that, I’m really a homebody. I’d kind of like to do more traveling, but I don’t like to travel alone and having kids puts a damper on the spontaneous “get up and go” aspect of travel. We do occasional road trips and stuff, though, which is fun.</p>
<p><strong>AkitaOnRails:</strong> Do you hear what’s going on the Ruby/Rails communities around the world? What do you feel about the meteoric rise of Rails in the US? 37signals, of course, was the fuel for all this and the reason we – both Rubyists now – are talking today.</p>
<p><strong>Jamis Buck:</strong> I hear some news, but I’ve trimmed my newsfeed quite a bit. I think I only subscribe to 30 or 40 feeds these days. It was really exciting to go to the Czech Republic and see the excitement there about Rails. It reminded me a lot of how things were in the US a couple of years ago.</p>
<p>The rise of Rails has been incredible. There have been growing pains, of course. With wider adoption you begin to accumulate people who aren’t passionate about the tool, and only want to learn it for the resumé, or because a client has asked for it, and I think that dilutes the waters quite a bit. But there will always be a core of passionate users and that’s what makes the community so wonderful. It’s been a really amazing experience to have been a part of that core, almost since the beginning of Rails.</p>
<p><strong>AkitaOnRails:</strong> You have the rare opportunity of seeing all this from a VIP perspective near Jason and David. And of course, yourself, being one the propellers of the community. Many people think of David as an <a href="http://www.flickr.com/photos/eugevon/130610241/"target="_blank" rel="noopener">arrogant</a> :-) I can perfectly understand his opinionated point of view. What do you think of it, from the perspective of someone who actually know the man?</p>
<p><img src="http://s3.amazonaws.com/akitaonrails/assets/2007/8/3/245186199_71f55cc3c72.png" alt=""  loading="lazy" /></p>
<p><strong>Jamis Buck:</strong> David is one of the most confident people I have ever met. My biggest frustration with him is that he’s RIGHT so often. He’s a very clear thinker and jumps right to the core of the issue, which can be very disconcerting if you aren’t used to that. All those attributes combine, I think, to make people who don’t know him consider him “arrogant”. But he’s not arrogant. He doesn’t put people down in order to raise himself up. He’s just extremely gifted, very smart, and doesn’t beat around the bush. I’m very honored to be able to know him. I’ve learned a lot from both him and Jason.</p>
<p><strong>AkitaOnRails:</strong> I don’t know if I got it correctly but Jason Fried was the one that started 37signals? Can you give us a small glimpse on the origins of the company? I know that David is not from the US, how did they get to meet each other?</p>
<p><strong>Jamis Buck:</strong> Jason co-founded 37signals, in the late 90’s. It was originally a design consultancy. In 2003, Jason started to work on a project in PHP, and asked around online whenever he got stuck. David responded to his queries several times and Jason finally just asked David if he would write the project. David came on part-time for awhile, then, since he was still finishing school. But eventually, Jason wanted to write Basecamp, and David did all the work on that, part-time, in Ruby. His experiences with that, of course, led to Rails. And eventually David become a partner in the company. That was in 2004, if I remember correctly.</p>
<p><strong>AkitaOnRails:</strong> Great. And coming back about you. Now that Capistrano 2.0 is finally released, are you involved in some new open source tool?</p>
<p><strong>Jamis Buck:</strong> With Cap2 out the door, I’m really focused on rewriting Net::SSH and Net::SFTP. Some frustrations with their limitations in writing cap2 has made this necessary :) The only “new” thing to come of this is Net::SCP, which is an implementation of an SCP client in Ruby and which I will release when I release v2 of Net::SSH and Net::SFTP.</p>
<p><strong>AkitaOnRails:</strong> Oh, and speaking of which, I remember that the former Cap 1.4.1 didn’t play very well on Windows boxes. Is that correct? Any advances in this field or do you feel that supporting Windows is just too much?</p>
<p><img src="http://s3.amazonaws.com/akitaonrails/assets/2007/8/3/506252072_642ad7ea39.jpg" alt=""  loading="lazy" /></p>
<p><strong>Jamis Buck:</strong> Well, as far as I know, cap 1.4.1 worked fine on Windows machines. The limitation was that you can not deploy TO Windows. Deploying FROM Windows works fine. And as for making cap deploy TO Windows, I have no plans. The difficulty is that Windows is not a POSIX environment, which is one of the fundamental assumptions of Capistrano, regarding the target machines.</p>
<p><strong>AkitaOnRails:</strong> Ah, yes. I never tried that, therefore my confusion. You’re right. Well, I think I already got a lot from you. Anything that you would want to say to the Brazilian community? :-)</p>
<p><strong>Jamis Buck:</strong> <em>“Stay passionate”</em> :) Keep the passion for Rails burning, it’s what makes the community worth belonging to.</p>
<p><strong>AkitaOnRails:</strong> Couldn’t be better. Any plans for coming here?</p>
<p><strong>Jamis Buck:</strong> Ha, none in the immediate future :) I’ve got a baby coming in September, which puts a damper on travel plans :) I’d love to visit Brazil someday, though.</p>
<p><strong>AkitaOnRails:</strong> Didn’t know that. Congratulations!</p>
<p><strong>Jamis Buck:</strong> Thanks.</p>
<p><strong>AkitaOnRails:</strong> Well, thanks a lot for your time. It was a very insightful conversation. I know people will enjoy this.</p>
<p><strong>Jamis Buck:</strong> Thank you very much, Fabio. I’m grateful for the opportunity!</p>
]]></content:encoded><category>interviews</category><category>devops</category><category>ruby</category></item></channel></rss>