<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Glauco Custodio</title>
    <description>Software engineering, open source, community and passion</description>
    <link>https://glaucocustodio.github.io</link>
    <atom:link href="https://glaucocustodio.github.io/feed.xml" rel="self" type="application/rss+xml" />
    
      <item>
        <title>A Neat Trick for Splitting Strings</title>
        <description>&lt;p&gt;You probably needed to split a full name into first and last name at some point.&lt;/p&gt;

&lt;p&gt;How would you split the string &lt;code&gt;Ayrton Senna da Silva&lt;/code&gt; into &lt;code&gt;Ayrton&lt;/code&gt; as the first name and &lt;code&gt;Senna da Silva&lt;/code&gt; as the last name?&lt;/p&gt;

&lt;p&gt;What if the name contains inconsistent amounts of whitespace in between the words? Eg: &lt;code style=&quot;white-space: pre;&quot;&gt;Ayrton Senna da  Silva&lt;/code&gt;.&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Ayrton Senna da  Silva&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;split&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot; &quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# =&amp;gt; [&quot;Ayrton&quot;, &quot;Senna&quot;, &quot;da&quot;, &quot;Silva&quot;]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;Splitting by space is a good start, but it doesn&apos;t finish the job.&lt;/p&gt;

&lt;h2&gt;Solution&lt;/h2&gt;

&lt;p&gt;I learned recently we can pass a second argument to &lt;code&gt;split&lt;/code&gt; specifying the maximum number of splits:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Ayrton Senna da  Silva&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;split&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot; &quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# =&amp;gt; [&quot;Ayrton&quot;, &quot;Senna da  Silva&quot;]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;That almost worked, but we still have an extra space in the last name.&lt;/p&gt;

&lt;p&gt;By combining it with &lt;code&gt;squish&lt;/code&gt; we can get the desired result in a clean way:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Ayrton Senna da  Silva&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;squish&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;split&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot; &quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# =&amp;gt; [&quot;Ayrton&quot;, &quot;Senna da Silva&quot;]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;That&apos;s it.&lt;/p&gt;</description>
        <pubDate>Thu, 15 Jan 2026 00:00:00 -0300</pubDate>
        <link>https://glaucocustodio.github.io/2026/01/15/a-neat-trick-for-splitting-strings/</link>
        <guid isPermaLink="true">https://glaucocustodio.github.io/2026/01/15/a-neat-trick-for-splitting-strings/</guid>
      </item>
    
      <item>
        <title>RSpec Satisfy Matcher</title>
        <description>&lt;p&gt;I&apos;ve recently discovered (or at least found a good use case for it) the &lt;a href=&quot;https://rspec.info/features/3-13/rspec-expectations/built-in-matchers/satisfy/&quot; target=&quot;_blank&quot; rel=&quot;external nofollow&quot;&gt;satisfy&lt;/a&gt; matcher in RSpec.&lt;/p&gt;

&lt;p&gt;My intention was to ensure an attribute contained a string and not another string (within &lt;code&gt;have_attributes&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;I tried a few variations of the code below, but none of them worked:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;n&quot;&gt;message&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;first&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;expect&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;to&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;have_attributes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;ss&quot;&gt;conversation_id: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;conversation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;ss&quot;&gt;customer: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;customer@example.com&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;ss&quot;&gt;response: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;include&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Hello, world!&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;and&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;not_to&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;include&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Subject:&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)))&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;While inspecting &lt;a href=&quot;https://rubygems.org/gems/rspec-expectations&quot; target=&quot;_blank&quot; rel=&quot;external nofollow&quot;&gt;rspec-expectations&lt;/a&gt;&apos;s source code in search of an &lt;code&gt;exclude&lt;/code&gt; matcher or something similar, the &lt;code&gt;satisfy&lt;/code&gt; matcher stood out as a good fit:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;n&quot;&gt;expect&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;to&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;have_attributes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;ss&quot;&gt;conversation_id: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;conversation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;ss&quot;&gt;customer: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;customer@example.com&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;ss&quot;&gt;response: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;satisfy&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;include?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Hello, world!&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;include?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Subject:&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;It&apos;s pretty self-explanatory, isn&apos;t it? The test will pass if the block returns &lt;code&gt;true&lt;/code&gt;.&lt;/p&gt;</description>
        <pubDate>Fri, 02 Jan 2026 00:00:00 -0300</pubDate>
        <link>https://glaucocustodio.github.io/2026/01/02/rspec-satisfy-matcher/</link>
        <guid isPermaLink="true">https://glaucocustodio.github.io/2026/01/02/rspec-satisfy-matcher/</guid>
      </item>
    
      <item>
        <title>New in Rails 8.1: Bring Your Favorite Editor to Error Pages</title>
        <description>&lt;p&gt;Rails 8.1.0 (released on October 22, 2025) brings a new feature that allows you to open the file that caused the error in your favorite editor.&lt;/p&gt;

&lt;p&gt;To enable this feature, you need to set either &lt;code&gt;EDITOR&lt;/code&gt; or &lt;code&gt;RAILS_EDITOR&lt;/code&gt; environment variable with the path to your editor, for example:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;&lt;span class=&quot;c&quot;&gt;# .bashrc / .zshrc&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;export &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;RAILS_EDITOR&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;cursor&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;Then, when you are taken to the error page, click on the pencil icon to open it in your editor:&lt;/p&gt;

&lt;img alt=&quot;Demo: opening file from error page in Rails 8.1&quot; src=&quot;/assets/opening-file-from-error-page-rails-8.1.png&quot;/&gt;

&lt;p&gt;A new &lt;code&gt;ActiveSupport::Editor&lt;/code&gt; class has been added to handle the editor functionality, check the &lt;a href=&quot;https://github.com/rails/rails/pull/55295&quot; target=&quot;_blank&quot; rel=&quot;external nofollow&quot;&gt;pull request&lt;/a&gt; for more details.&lt;/p&gt;</description>
        <pubDate>Thu, 20 Nov 2025 00:00:00 -0300</pubDate>
        <link>https://glaucocustodio.github.io/2025/11/20/new-in-rails-8.1-bring-your-favorite-editor-to-error-pages/</link>
        <guid isPermaLink="true">https://glaucocustodio.github.io/2025/11/20/new-in-rails-8.1-bring-your-favorite-editor-to-error-pages/</guid>
      </item>
    
      <item>
        <title>TIL: previously_new_record? — A Hidden Gem in ActiveRecord</title>
        <description>&lt;p&gt;
  Have you ever needed to know if a record in Rails was just created — especially after using &lt;code&gt;create_or_find_by&lt;/code&gt; or &lt;code&gt;find_or_create_by&lt;/code&gt;?
&lt;/p&gt;

&lt;p&gt;Most Rails devs reach for &lt;code&gt;new_record?&lt;/code&gt;, but it won’t help after &lt;code&gt;create_or_find_by&lt;/code&gt;, because the record is already saved. So how do you know if a record was just created?&lt;/p&gt;

&lt;p&gt;
  Say hello to &lt;code&gt;previously_new_record?&lt;/code&gt;, which for my surprise is available since &lt;a href=&quot;https://github.com/rails/rails/blob/6-1-stable/activerecord/CHANGELOG.md#rails-610-december-09-2020&quot; target=&quot;_blank&quot; rel=&quot;external nofollow&quot;&gt;Rails 6.1&lt;/a&gt;.
&lt;/p&gt;

&lt;p&gt;It tells you if the record was new right before the last save — giving you a clean way to trigger onboarding logic, log metadata, or set defaults only for new records.&lt;/p&gt;

&lt;p&gt;Here&apos;s how one can use it:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;create_or_find_by&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;email: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# only create the log if the user was just created&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;previously_new_record?&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;create_log&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;Next time you need to distinguish between a found and a created record, reach for &lt;code&gt;previously_new_record?&lt;/code&gt;. It’s already part of ActiveRecord — no extra setup needed.&lt;/p&gt;

&lt;p&gt;You can check out how it was implemented in this &lt;a href=&quot;https://github.com/rails/rails/pull/36276&quot; target=&quot;_blank&quot; rel=&quot;external nofollow&quot;&gt;pull request&lt;/a&gt;.&lt;/p&gt;</description>
        <pubDate>Thu, 03 Jul 2025 00:00:00 -0300</pubDate>
        <link>https://glaucocustodio.github.io/2025/07/03/til-previously-new-record-a-hidden-gem-in-active-record/</link>
        <guid isPermaLink="true">https://glaucocustodio.github.io/2025/07/03/til-previously-new-record-a-hidden-gem-in-active-record/</guid>
      </item>
    
      <item>
        <title>New in Rails 8: Bring Your Favorite CLI to rails dbconsole</title>
        <description>&lt;p&gt;
  Rails 8 got even better for developers who love clean CLI workflows.
&lt;/p&gt;

&lt;p&gt;You can now customize the command-line tool used by &lt;code&gt;rails dbconsole&lt;/code&gt; with the new &lt;code&gt;config.active_record.database_cli&lt;/code&gt; setting.&lt;/p&gt;

&lt;p&gt;
  Prefer &lt;a href=&quot;https://www.pgcli.com/&quot; target=&quot;_blank&quot; rel=&quot;external nofollow&quot;&gt;pgcli&lt;/a&gt; over the default &lt;code&gt;psql&lt;/code&gt; to connect to your database? Make your dev experience smoother with autocomplete and syntax highlighting:
&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;c1&quot;&gt;# config/initializers/database_cli.rb&lt;/span&gt;
&lt;span class=&quot;no&quot;&gt;Rails&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;application&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;configure&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Rails&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;env&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;local?&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;config&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;active_record&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;database_cli&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;postgresql: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;pgcli&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;Available since &lt;a href=&quot;https://github.com/rails/rails/blob/8-0-stable/activerecord/CHANGELOG.md#rails-800beta1-september-26-2024&quot; target=&quot;_blank&quot; rel=&quot;external nofollow&quot;&gt;Rails 8.0.0.beta1&lt;/a&gt; — check out the &lt;a href=&quot;https://github.com/rails/rails/pull/52656&quot; target=&quot;_blank&quot; rel=&quot;external nofollow&quot;&gt;pull request&lt;/a&gt; for full details and start customizing your database workflow today.&lt;/p&gt;</description>
        <pubDate>Wed, 21 May 2025 00:00:00 -0300</pubDate>
        <link>https://glaucocustodio.github.io/2025/05/21/rails-8-adds-config-activerecord-database-cli/</link>
        <guid isPermaLink="true">https://glaucocustodio.github.io/2025/05/21/rails-8-adds-config-activerecord-database-cli/</guid>
      </item>
    
      <item>
        <title>Migrating From Dokku to Kamal: Scheduling Cron Jobs</title>
        <description>&lt;p&gt;This is the third post of the series &quot;Migrating From Dokku to Kamal&quot; and today I am gonna show you how I&apos;ve set cron with &lt;a href=&quot;https://kamal-deploy.org/&quot; target=&quot;_blank&quot; rel=&quot;external nofollow&quot;&gt;Kamal&lt;/a&gt;, &lt;a href=&quot;/2024/01/19/migrating-from-dokku-to-kamal-setting-up-the-servers/&quot;&gt;click here&lt;/a&gt; to read the second post of the series in case you&apos;ve missed it.&lt;/p&gt;

&lt;p&gt;I needed a way run periodic tasks on Kamal to replace &lt;a target=&quot;_blank&quot; rel=&quot;external nofollow&quot; href=&quot;https://dokku.com/docs/processes/scheduled-cron-tasks/&quot;&gt;Dokku&apos;s scheduled cron tasks&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Cron seemed the way to go according to this &lt;a target=&quot;_blank&quot; rel=&quot;external nofollow&quot; href=&quot;https://github.com/basecamp/kamal/pull/57/files&quot;&gt;pull request&lt;/a&gt; on the Kamal repo, but making it work turned out to be not so simple as that.&lt;/p&gt;

&lt;p&gt;The idea is to have a new container on the &lt;code&gt;worker&lt;/code&gt; server responsible for scheduling jobs defined on a crontab file.&lt;/p&gt;

&lt;h2&gt;Defining cron jobs&lt;/h2&gt;

&lt;p&gt;First thing is to create this crontab file defining the tasks to be scheduled by cron, I&apos;ve put this file on &lt;code&gt;config/app.crontab&lt;/code&gt;:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;59 20 &lt;span class=&quot;k&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;*&lt;/span&gt; 1,2,3,4,5 /rails/bin/cron-executor.sh make whatever&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;Note that I am passing the command I want to run (&lt;code&gt;make whatever&lt;/code&gt; in this case) to a &lt;code&gt;bin/cron-executor.sh&lt;/code&gt;:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;&lt;span class=&quot;c&quot;&gt;#!/bin/bash -e&lt;/span&gt;

&lt;span class=&quot;nv&quot;&gt;PATH&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$PATH&lt;/span&gt;:/usr/local/bin

&lt;span class=&quot;nb&quot;&gt;cd&lt;/span&gt; /rails &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;exit

echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;CRON: &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;/proc/1/fd/1 2&amp;gt;/proc/1/fd/2

&lt;span class=&quot;nb&quot;&gt;exec&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;/proc/1/fd/1 2&amp;gt;/proc/1/fd/2&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;this shell script is responsible for doing a few things before running the desired command:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;adds &lt;code&gt;/usr/local/bin&lt;/code&gt; to &lt;code&gt;$PATH&lt;/code&gt;, so ruby and bundle can be loaded&lt;/li&gt;
  &lt;li&gt;enters the directory of the application (&lt;code&gt;/rails&lt;/code&gt;)&lt;/li&gt;
  &lt;li&gt;prints the command that is about to be executed&lt;/li&gt;
  &lt;li&gt;executes the command&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In case you are wondering, &lt;code&gt;&gt;/proc/1/fd/1 2&gt;/proc/1/fd/2&lt;/code&gt; is required to redirect the output of the command to the container&apos;s stdout/stderr. If you don&apos;t add this, &lt;strong&gt;you won&apos;t be able to see the logs&lt;/strong&gt; of the cron jobs when running &lt;code&gt;docker logs&lt;/code&gt; for example.&lt;/p&gt;

&lt;h2&gt;Setting up the Dockerfile&lt;/h2&gt;

&lt;p&gt;The next step is to make sure the application&apos;s &lt;code&gt;Dockerfile&lt;/code&gt; installs cron and applies the jobs from the crontab file we just defined:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-dockerfile&quot; data-lang=&quot;dockerfile&quot;&gt;&lt;span class=&quot;k&quot;&gt;ARG&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; RUBY_VERSION=3.2.2&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;FROM&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;ruby:$RUBY_VERSION-slim-bullseye&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;as&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;base&lt;/span&gt;

...

&lt;span class=&quot;k&quot;&gt;RUN &lt;/span&gt;apt-get update &lt;span class=&quot;se&quot;&gt;\
&lt;/span&gt;    &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;DEBIAN_FRONTEND&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;noninteractive apt-get &lt;span class=&quot;nt&quot;&gt;-y&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--no-install-recommends&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-y&lt;/span&gt; cron &lt;span class=&quot;se&quot;&gt;\
&lt;/span&gt;    &lt;span class=&quot;c&quot;&gt;# Remove package lists for smaller image sizes&lt;/span&gt;
    &amp;amp;&amp;amp; rm -rf /var/lib/apt/lists/* \
    &amp;amp;&amp;amp; which cron \
    &amp;amp;&amp;amp; rm -rf /etc/cron.*/*

&lt;span class=&quot;k&quot;&gt;COPY&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; config/app.crontab /etc/cron.d/cronfile&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;RUN &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;chmod &lt;/span&gt;0644 /etc/cron.d/cronfile
&lt;span class=&quot;k&quot;&gt;RUN &lt;/span&gt;crontab /etc/cron.d/cronfile

&lt;span class=&quot;k&quot;&gt;USER&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; root&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;ENTRYPOINT&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; [&quot;/rails/bin/docker-entrypoint&quot;]&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;EXPOSE&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; 3000&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;CMD&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; [&quot;./bin/rails&quot;, &quot;server&quot;]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;div class=&quot;note alert&quot;&gt;The default Dockerfile for Rails 7.1 sets up a non-root user for security reasons, but I couldn&apos;t get cron working with a non-root user, that&apos;s why I ended up using &lt;code&gt;USER root&lt;/code&gt;. Please let me know in case you managed to get that working.&lt;/div&gt;

&lt;h2&gt;Passing environment variables to cron&lt;/h2&gt;

&lt;p&gt;Cron reads environment variables from &lt;code&gt;/etc/environment&lt;/code&gt;, so we need to set them there by adding one line to &lt;code&gt;bin/docker-entrypoint&lt;/code&gt;:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;&lt;span class=&quot;c&quot;&gt;#!/bin/bash -e&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# pass env vars to cron&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;env&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&amp;gt;&lt;/span&gt; /etc/environment

&lt;span class=&quot;c&quot;&gt;# If running the rails server then create or migrate existing database&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;./bin/rails&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;server&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then&lt;/span&gt;
  ./bin/rails db:prepare
&lt;span class=&quot;k&quot;&gt;fi

&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;exec&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;h2&gt;Defining a new server&lt;/h2&gt;

&lt;p&gt;The last step is to define a new server on Kamal&apos;s &lt;code&gt;deploy.yml&lt;/code&gt; with the same ip as the &lt;code&gt;worker&lt;/code&gt;:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-yaml&quot; data-lang=&quot;yaml&quot;&gt;&lt;span class=&quot;na&quot;&gt;servers&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;web&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;hosts&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&amp;lt;%= ENV[&apos;KAMAL_WEB_IP&apos;] %&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;labels&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;traefik.http.routers.domain.rule&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Host(`*.domain.com`)&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;traefik.http.routers.domain.entrypoints&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;websecure&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;traefik.http.routers.domain.tls.certresolver&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;letsencrypt&lt;/span&gt;

  &lt;span class=&quot;na&quot;&gt;worker&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;hosts&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&amp;lt;%= ENV[&apos;KAMAL_WORKER_IP&apos;] %&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;cmd&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;bin/run-worker.sh&lt;/span&gt;

  &lt;span class=&quot;na&quot;&gt;cron&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;hosts&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&amp;lt;%= ENV[&apos;KAMAL_WORKER_IP&apos;] %&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;cmd&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;bash -c &quot;cron -f -L 2&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;Note that &lt;code&gt;bash -c &quot;cron -f -L 2&quot;&lt;/code&gt; is set as the command to run on the container, it&apos;ll make cron to run on the foreground and set the logging verbosity.&lt;/p&gt;

&lt;p&gt;That&apos;s all folks, I hope you have enjoyed the series.&lt;/p&gt;

&lt;p&gt;Ps: I&apos;d like to thank you Jason for writing &lt;a target=&quot;_blank&quot; rel=&quot;external nofollow&quot; href=&quot;https://blog.thesparktree.com/cron-in-docker&quot;&gt;this post&lt;/a&gt; on running cron on Docker, it was really useful to me!&lt;/p&gt;</description>
        <pubDate>Tue, 23 Jan 2024 00:00:00 -0300</pubDate>
        <link>https://glaucocustodio.github.io/2024/01/23/migrating-from-dokku-to-kamal-scheduling-cron-jobs/</link>
        <guid isPermaLink="true">https://glaucocustodio.github.io/2024/01/23/migrating-from-dokku-to-kamal-scheduling-cron-jobs/</guid>
      </item>
    
      <item>
        <title>Migrating From Dokku to Kamal: Setting up the Servers</title>
        <description>&lt;p&gt;This is the second post of the series &quot;Migrating From Dokku to Kamal&quot; and today I am gonna show you how I&apos;ve set up my servers with &lt;a href=&quot;https://kamal-deploy.org/&quot; target=&quot;_blank&quot; rel=&quot;external nofollow&quot;&gt;Kamal&lt;/a&gt;, &lt;a href=&quot;/2024/01/18/migrating-from-dokku-to-kamal-provisioning-with-terraform/&quot;&gt;click here&lt;/a&gt; to read the first post of the series in case you&apos;ve missed it.&lt;/p&gt;

&lt;div class=&quot;note&quot;&gt;The author assumes you&apos;ve a basic understanding of how Kamal works, you can familiarize yourself by checking the &lt;a href=&quot;https://kamal-deploy.org/docs/installation&quot; target=&quot;_blank&quot; rel=&quot;external nofollow&quot;&gt;official documentation&lt;/a&gt;.&lt;/div&gt;

&lt;p&gt;
  Below you can find part of my final &lt;code&gt;deploy.yml&lt;/code&gt;:
&lt;/p&gt;


&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-yaml&quot; data-lang=&quot;yaml&quot;&gt;&lt;span class=&quot;na&quot;&gt;servers&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;web&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;hosts&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&amp;lt;%= ENV[&apos;KAMAL_WEB_IP&apos;] %&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;labels&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;traefik.http.routers.domain.rule&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Host(`*.domain.com`)&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;traefik.http.routers.domain.entrypoints&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;websecure&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;traefik.http.routers.domain.tls.certresolver&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;letsencrypt&lt;/span&gt;

  &lt;span class=&quot;na&quot;&gt;worker&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;hosts&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&amp;lt;%= ENV[&apos;KAMAL_WORKER_IP&apos;] %&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;cmd&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;bin/run-worker.sh&lt;/span&gt;

  &lt;span class=&quot;na&quot;&gt;registry&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;server&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;ghcr.io&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;username&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;your_github_username&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;password&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;KAMAL_REGISTRY_PASSWORD&lt;/span&gt;

&lt;span class=&quot;na&quot;&gt;env&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;clear&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;RAILS_ENV&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;production&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;PIDFILE&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;/dev/null&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;secret&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;RAILS_LOG_TO_STDOUT&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;CLOUDFLARE_API_KEY&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;S3_ACCESS_KEY_ID&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;S3_SECRET_ACCESS_KEY&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;S3_ENDPOINT&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;S3_REGION&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;POSTGRES_DATABASE&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;POSTGRES_HOST&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;POSTGRES_DB&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;POSTGRES_USER&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;POSTGRES_PASSWORD&lt;/span&gt;

&lt;span class=&quot;na&quot;&gt;builder&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;multiarch&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;false&lt;/span&gt;

&lt;span class=&quot;na&quot;&gt;accessories&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;db&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;postgres:16.0&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;host&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&amp;lt;%= ENV[&apos;POSTGRES_HOST&apos;] %&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;port&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;5432&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;env&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;secret&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;POSTGRES_DB&lt;/span&gt;
        &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;POSTGRES_USER&lt;/span&gt;
        &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;POSTGRES_PASSWORD&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;directories&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;data:/var/lib/postgresql/data&lt;/span&gt;

  &lt;span class=&quot;na&quot;&gt;db_backup&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;eeshugerman/postgres-backup-s3:16&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;host&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&amp;lt;%= ENV[&apos;POSTGRES_HOST&apos;] %&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;env&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;clear&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;SCHEDULE&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;@daily&apos;&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;BACKUP_KEEP_DAYS&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;10&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;S3_BUCKET&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;whatever-bucket&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;S3_PREFIX&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;backups&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;secret&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;S3_ACCESS_KEY_ID&lt;/span&gt;
        &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;S3_SECRET_ACCESS_KEY&lt;/span&gt;
        &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;S3_ENDPOINT&lt;/span&gt;
        &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;S3_REGION&lt;/span&gt;
        &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;POSTGRES_HOST&lt;/span&gt;
        &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;POSTGRES_DATABASE&lt;/span&gt;
        &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;POSTGRES_USER&lt;/span&gt;
        &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;POSTGRES_PASSWORD&lt;/span&gt;

&lt;span class=&quot;na&quot;&gt;traefik&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;options&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;publish&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;443:443&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;volume&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;/letsencrypt/acme.json:/letsencrypt/acme.json&quot;&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;entryPoints.web.address&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;:80&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;entryPoints.websecure.address&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;:443&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;entryPoints.web.http.redirections.entryPoint.to&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;websecure&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;entryPoints.web.http.redirections.entryPoint.scheme&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;https&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;entryPoints.web.http.redirections.entrypoint.permanent&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;true&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;entrypoints.websecure.http.tls&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;true&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;entrypoints.websecure.http.tls.domains[0].main&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;domain.com&quot;&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;entrypoints.websecure.http.tls.domains[0].sans&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;*.domain.com&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;certificatesResolvers.letsencrypt.acme.email&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;user@provider.com&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;certificatesResolvers.letsencrypt.acme.storage&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;/letsencrypt/acme.json&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;certificatesresolvers.letsencrypt.acme.dnschallenge.provider&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;cloudflare&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;env&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;secret&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;CLOUDFLARE_API_KEY&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;clear&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;CLOUDFLARE_EMAIL&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;user@provider.com&apos;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;It does a couple of things:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;reads the env vars &lt;code&gt;KAMAL_WEB_IP&lt;/code&gt;, &lt;code&gt;KAMAL_WORKER_IP&lt;/code&gt; and &lt;code&gt;POSTGRES_HOST&lt;/code&gt; from the &lt;code&gt;.env&lt;/code&gt; file that were defined with Terraform (as shown on the &lt;a href=&apos;/2024/01/18/migrating-from-dokku-to-kamal-provisioning-with-terraform/&apos;&gt;other post&lt;/a&gt;)&lt;/li&gt;

  &lt;li&gt;defines three servers: &lt;code&gt;web&lt;/code&gt;, &lt;code&gt;worker&lt;/code&gt; and &lt;code&gt;db&lt;/code&gt; (which is under &lt;code&gt;accessories&lt;/code&gt;)&lt;/li&gt;

  &lt;li&gt;sets Let&apos;s Encrypt as certificate resolver for the &lt;code&gt;web&lt;/code&gt; server&lt;/li&gt;

  &lt;li&gt;overrides the command to be executed on the Docker container for the &lt;code&gt;worker&lt;/code&gt; server, it&apos;ll run &lt;code&gt;bin/run-worker.sh&lt;/code&gt; instead&lt;/li&gt;

  &lt;li&gt;sets GitHub as image registry, it&apos;s for free&lt;/li&gt;

  &lt;li&gt;sets env vars. &lt;code&gt;PIDFILE=/dev/null&lt;/code&gt; tells Rails to not save pid files as you might receive the error &lt;code&gt;A server is already running. Check /rails/tmp/pids/server.pid&lt;/code&gt; in case docker gets killed abruptly. This will be the default behaviour as of  &lt;a href=&quot;https://github.com/rails/rails/pull/50644&quot; target=&quot;_blank&quot; rel=&quot;external nofollow&quot;&gt;Rails 8&lt;/a&gt;. Check &lt;a href=&quot;https://medium.com/pragmatic-programmers/rails-tmp-pids-server-pid-not-cleaned-up-a3155093b52f&quot; target=&quot;_blank&quot; rel=&quot;external nofollow&quot;&gt;this post&lt;/a&gt; for more info.&lt;/li&gt;

  &lt;li&gt;speeds up the build time by disabling &lt;code&gt;multiarch&lt;/code&gt; since both my local machine and the servers run on the arm64 architecture&lt;/li&gt;

  &lt;li&gt;sets up a container on the &lt;code&gt;db&lt;/code&gt; server to backup the Postgres database once a day using &lt;a href=&quot;https://github.com/eeshugerman/postgres-backup-s3&quot; target=&quot;_blank&quot; rel=&quot;external nofollow&quot;&gt;postgres-backup-s3&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;Resolving Let&apos;s Encrypt ACME v2 challenge&lt;/h2&gt;

&lt;p&gt;As my domain has Cloudflare as DNS resolver and I wanted to support wildcard certificates - so I could test my app at &lt;code&gt;kamal.domain.com&lt;/code&gt; before switching from Digital Ocean to Hetzner on &lt;code&gt;domain.com&lt;/code&gt;, the web server is solving &lt;a href=&quot;https://community.letsencrypt.org/t/acme-v2-production-environment-wildcards/55578&quot; target=&quot;_blank&quot; rel=&quot;external nofollow&quot;&gt;Let&apos;s Encrypt ACME v2 challenge&lt;/a&gt; through DNS (thanks Nick for &lt;a href=&quot;https://github.com/basecamp/kamal/discussions/112&quot; target=&quot;_blank&quot; rel=&quot;external nofollow&quot;&gt;sharing it&lt;/a&gt;), that&apos;s why I had to define the extra args for &lt;code&gt;traefik&lt;/code&gt; (including &lt;code&gt;CLOUDFLARE_EMAIL&lt;/code&gt; and &lt;code&gt;CLOUDFLARE_API_KEY&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Note we are storing the certificate on the file we created with Terraform: &lt;code&gt;/letsencrypt/acme.json&lt;/code&gt;. This way we don&apos;t need to regenerate a new certificate every time a new &lt;code&gt;web&lt;/code&gt; container is run.&lt;/p&gt;

&lt;p&gt;That&apos;s it for today, in the &lt;a href=&quot;/2024/01/23/migrating-from-dokku-to-kamal-scheduling-cron-jobs/&quot;&gt;last post of the series&lt;/a&gt; I share how I&apos;ve set up cron to run scheduled tasks.&lt;/p&gt;</description>
        <pubDate>Fri, 19 Jan 2024 00:00:00 -0300</pubDate>
        <link>https://glaucocustodio.github.io/2024/01/19/migrating-from-dokku-to-kamal-setting-up-the-servers/</link>
        <guid isPermaLink="true">https://glaucocustodio.github.io/2024/01/19/migrating-from-dokku-to-kamal-setting-up-the-servers/</guid>
      </item>
    
      <item>
        <title>Migrating From Dokku to Kamal: Provisioning with Terraform</title>
        <description>&lt;p&gt;I have a
  &lt;code&gt;2 GB Memory, 1 vCPU, 50 GB Disk&lt;/code&gt; VPS on Digital Ocean which cost me 12 USD per month. Comparing the prices
  with &lt;a href=&quot;https://www.hetzner.com/&quot; target=&quot;_blank&quot; rel=&quot;external nofollow&quot;&gt;Hetzner&lt;/a&gt; I noticed I could have 3x
  &lt;code&gt;4 GB, 2 vCPU Arm64, 40 GB Disk&lt;/code&gt; for 13.53 EUR per month. That&apos;s a lot more power for almost the same
  price.
&lt;/p&gt;

&lt;p&gt;The droplet is running a Rails application with &lt;a href=&quot;https://dokku.com/&quot; target=&quot;_blank&quot; rel=&quot;external nofollow&quot;&gt;Dokku&lt;/a&gt;. I really enjoyed my time with Dokku, it makes our lives so
  much easier when setting up a VPS from scratch. The major downside of Dokku is the lack of support to multi-host
  though.&lt;/p&gt;

&lt;p&gt;After the introduction of &lt;a href=&quot;https://www.youtube.com/watch?v=yWSpjKErnco&quot; target=&quot;_blank&quot; rel=&quot;external nofollow&quot;&gt;Kamal&lt;/a&gt; I thought in giving it a try, but this time using Hetzner instead of Digital Ocean.
&lt;/p&gt;

&lt;h2&gt;Provisioning Resources on Hetzner with Terraform&lt;/h2&gt;

&lt;p&gt;Since I would be creating new servers on Hetzner, I asked ChatGPT to help me doing that with Terraform so the whole
  process could be easily repeatable.&lt;/p&gt;
&lt;/p&gt;

&lt;p&gt;After some back and forth I ended up with the following script on &lt;code&gt;terraform/main.tf&lt;/code&gt;:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-terraform&quot; data-lang=&quot;terraform&quot;&gt;&lt;span class=&quot;c1&quot;&gt;# requires TF_VAR_hetzner_api_token env var&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# eg: `TF_VAR_hetzner_api_token=foo terraform apply`&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;variable&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;hetzner_api_token&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;type&lt;/span&gt;    &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;string&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&quot;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# requires TF_VAR_hetzner_ssh_fingerprint env var&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# eg: `TF_VAR_hetzner_ssh_fingerprint=bar terraform apply`&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;variable&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;hetzner_ssh_fingerprint&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;type&lt;/span&gt;    &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;string&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&quot;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# requires TF_VAR_user_private_key_path env var&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# eg: `TF_VAR_user_private_key_path=/Users/foo/.ssh/id_ed25519 terraform apply`&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;variable&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;user_private_key_path&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;type&lt;/span&gt;    &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;string&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&quot;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;variable&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;server_names&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;web&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;worker&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;db&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;data&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;hcloud_ssh_key&quot;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;ssh_key&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;fingerprint&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;hetzner_ssh_fingerprint&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;provider&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;hcloud&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;token&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;hetzner_api_token&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;variable&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;common_provisioner_commands&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;# change default DNS servers as Hetzner&apos;s default ones are not reliable&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;s2&quot;&gt;&quot;echo 1&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;s2&quot;&gt;&quot;sed -i &apos;s/#DNS=/DNS=1.1.1.1/&apos; /etc/systemd/resolved.conf&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;s2&quot;&gt;&quot;sed -i &apos;s/#FallbackDNS=/FallbackDNS=8.8.8.8/&apos; /etc/systemd/resolved.conf&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;s2&quot;&gt;&quot;sed -i &apos;s/#DNSOverTLS=no/DNSOverTLS=opportunistic/&apos; /etc/systemd/resolved.conf&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;s2&quot;&gt;&quot;systemctl restart systemd-resolved&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;resource&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;hcloud_server&quot;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;cax11&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;count&lt;/span&gt;       &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;length&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;server_names&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;name&lt;/span&gt;        &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;server_names&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;index&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;image&lt;/span&gt;       &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;ubuntu-22.04&quot;&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;server_type&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;cax11&quot;&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;location&lt;/span&gt;    &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;nbg1&quot;&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;ssh_keys&lt;/span&gt;    &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;hcloud_ssh_key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;ssh_key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;public_net&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;ipv4_enabled&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;ipv6_enabled&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;c1&quot;&gt;# create a acme.json file only on the server &quot;web&quot;&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;# it will be used to keep the certificate on the host so it can be reused&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;# across many containers (will avoid rate limiting from letsencrypt)&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;#&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;# inspiration: https://github.com/basecamp/kamal/discussions/112#discussioncomment-6266858&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;#&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;provisioner&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;remote-exec&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;inline&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;server_names&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;index&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;web&quot;&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;concat&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;([&lt;/span&gt;
      &lt;span class=&quot;s2&quot;&gt;&quot;mkdir -p /letsencrypt &amp;amp;&amp;amp; touch /letsencrypt/acme.json &amp;amp;&amp;amp; chmod 600 /letsencrypt/acme.json&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;],&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;common_provisioner_commands&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;common_provisioner_commands&lt;/span&gt;

    &lt;span class=&quot;nx&quot;&gt;connection&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;type&lt;/span&gt;        &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;ssh&quot;&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;user&lt;/span&gt;        &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;root&quot;&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;private_key&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;file&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;user_private_key_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;host&lt;/span&gt;        &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;ipv4_address&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Print the IP addresses of the created instances&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;output&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;formatted_instance_ips&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;value&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&amp;lt;-&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;EOT&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;
    web: ${hcloud_server.cax11[0].ipv4_address}
    worker: ${hcloud_server.cax11[1].ipv4_address}
    db: ${hcloud_server.cax11[2].ipv4_address}
&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;  EOT
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;


&lt;span class=&quot;c1&quot;&gt;# update ../.env file with IPv4 addresses and&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# remove old ssh keys from the .ssh/known_hosts file&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;resource&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;null_resource&quot;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;update_env_file&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;depends_on&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;hcloud_server&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;cax11&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;provisioner&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;local-exec&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;command&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;EOT&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;
sed -i -E &apos;s/^KAMAL_WEB_IP=.*/KAMAL_WEB_IP=${hcloud_server.cax11[0].ipv4_address}/&apos; ../.env
sed -i -E &apos;s/^KAMAL_WORKER_IP=.*/KAMAL_WORKER_IP=${hcloud_server.cax11[1].ipv4_address}/&apos; ../.env
sed -i -E &apos;s/^POSTGRES_HOST=.*/POSTGRES_HOST=${hcloud_server.cax11[2].ipv4_address}/&apos; ../.env
ssh-keygen -R ${hcloud_server.cax11[0].ipv4_address}
ssh-keygen -R ${hcloud_server.cax11[1].ipv4_address}
ssh-keygen -R ${hcloud_server.cax11[2].ipv4_address}
&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;EOT
&lt;/span&gt;  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;terraform&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;required_version&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&amp;gt;= 1.4.3&quot;&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;required_providers&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;hcloud&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;s2&quot;&gt;&quot;source&quot;&lt;/span&gt;  &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;hetznercloud/hcloud&quot;&lt;/span&gt;
      &lt;span class=&quot;s2&quot;&gt;&quot;version&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&amp;gt;= 1.44.1&quot;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;The script above does a couple of things:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;define a few variables so the Terraform script doesn&apos;t hold any hard coded sensitive data, namely the Hetzner api token, the fingerprint of the ssh key associated with my account on Hetzner and my ssh private key&lt;/li&gt;

  &lt;li&gt;define the resources to be managed on the german provider: 3 VPSs of the type &lt;code&gt;CAX11&lt;/code&gt;, named &lt;code&gt;web&lt;/code&gt;, &lt;code&gt;worker&lt;/code&gt; and &lt;code&gt;db&lt;/code&gt;&lt;/li&gt;

  &lt;li&gt;create and give permission to the file &lt;code&gt;/letsencrypt/acme.json&lt;/code&gt; only on the &lt;code&gt;web&lt;/code&gt; server as it will be used ahead to hold Let&apos;s Encrypt certificate&lt;/li&gt;

  &lt;li&gt;change default DNS and enable DNSOverTLS when supported&lt;/li&gt;

  &lt;li&gt;print the ipv4 of the created resources&lt;/li&gt;

  &lt;li&gt;update the &lt;code&gt;.env&lt;/code&gt; file with the ipv4 of the created servers. This file will be used by Kamal to set up the environment variables on the servers&lt;/li&gt;

  &lt;li&gt;remove the ipv4 of the created resources from the &lt;code&gt;.ssh/known_hosts&lt;/code&gt; file, this can be left out in case you are not applying and destroying the resources on Hetzner often.&lt;/li&gt;

  &lt;li&gt;define the versions of Terraform and the provider (boilerplate code required by Terraform)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once you have uploaded your public ssh key to Hetzner, you can copy the fingerprint by visiting &lt;code&gt;Security -&gt; SSH keys&lt;/code&gt; on their website.&lt;/p&gt;

&lt;p&gt;By visiting &lt;code&gt;Security -&gt; API tokens&lt;/code&gt; you can generate an api token that will allow Terraform to manage resources on your Hetzner account.&lt;/p&gt;

&lt;p&gt;The path to your private key is required so Terraform can ssh into the servers on your behalf.&lt;/p&gt;

&lt;p&gt;Once you have everything in place and have installed Terraform on your machine you can run:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;terraform
terraform init&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;to initialize the terraform thing and:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;&lt;span class=&quot;nv&quot;&gt;TF_VAR_user_private_key_path&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;/Users/my_user/.ssh/id_ed25519 &lt;span class=&quot;nv&quot;&gt;TF_VAR_hetzner_api_token&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;abcdefghijklmnopqrstuvwxyz&quot;&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;TF_VAR_hetzner_ssh_fingerprint&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;a1:b2:c3:d4:e5:f6:g7:h8&quot;&lt;/span&gt; terraform apply&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;to create the resources on Hetzner. Notice you gotta prepend &lt;code&gt;TF_VAR_&lt;/code&gt; on the name of the variables defined in the script.&lt;/p&gt;

&lt;p&gt;In case you want to destroy the resources created you can run:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;&lt;span class=&quot;nv&quot;&gt;TF_VAR_user_private_key_path&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;/Users/my_user/.ssh/id_ed25519 &lt;span class=&quot;nv&quot;&gt;TF_VAR_hetzner_api_token&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;abcdefghijklmnopqrstuvwxyz&quot;&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;TF_VAR_hetzner_ssh_fingerprint&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;a1:b2:c3:d4:e5:f6:g7:h8&quot;&lt;/span&gt; terraform destroy&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;In the &lt;a href=&quot;/2024/01/19/migrating-from-dokku-to-kamal-setting-up-the-servers/&quot;&gt;next post of the series&lt;/a&gt; I share how Kamal has been used to set up the servers, including Let&apos;s Encrypt on the &lt;code&gt;web&lt;/code&gt;.&lt;/p&gt;</description>
        <pubDate>Thu, 18 Jan 2024 00:00:00 -0300</pubDate>
        <link>https://glaucocustodio.github.io/2024/01/18/migrating-from-dokku-to-kamal-provisioning-with-terraform/</link>
        <guid isPermaLink="true">https://glaucocustodio.github.io/2024/01/18/migrating-from-dokku-to-kamal-provisioning-with-terraform/</guid>
      </item>
    
      <item>
        <title>Turbo Drive + ECharts: Fixing History Back</title>
        <description>&lt;h2&gt;Problem&lt;/h2&gt;

&lt;p&gt;You got a Rails app with &lt;a target=&quot;_blank&quot; rel=&quot;external nofollow&quot; href=&quot;https://turbo.hotwired.dev/handbook/introduction&quot;&gt;Turbo Drive&lt;/a&gt; and some charts powered by &lt;a target=&quot;_blank&quot; rel=&quot;external nofollow&quot; href=&quot;https://echarts.apache.org/en/index.html&quot;&gt;ECharts&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It works fine until you navigate to another page and hit the back button.&lt;/p&gt;

&lt;p&gt;When navigating back in the browser history, somehow Turbo can&apos;t deal with the charts and displays an empty square instead of rendering them. Only a full page refresh can make them appear again.&lt;/p&gt;

&lt;h2&gt;The Lazy Solution&lt;/h2&gt;

&lt;p&gt;The simpler (but not so nice) solution is to disable Turbo Drive for all links on that particular page which contains the charts:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-html&quot; data-lang=&quot;html&quot;&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
  ...
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;meta&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;turbo-visit-control&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;content=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;reload&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;But that would downgrade the user experience, so it&apos;s kinda meh.&lt;/p&gt;

&lt;h2&gt;A Better Solution&lt;/h2&gt;

&lt;p&gt;Turbo Drive &lt;a target=&quot;_blank&quot; rel=&quot;external nofollow&quot; href=&quot;https://turbo.hotwired.dev/handbook/building#understanding-caching&quot;&gt;caches&lt;/a&gt; the current page before requesting the clicked link, so I thought, what if I dispose the charts before saving the cache?&lt;/p&gt;

&lt;p&gt;That worked.&lt;/p&gt;

&lt;p&gt;We can use the &lt;code&gt;turbo:before-cache&lt;/code&gt; &lt;a target=&quot;_blank&quot; rel=&quot;external nofollow&quot; href=&quot;https://turbo.hotwired.dev/reference/events&quot;&gt;callback&lt;/a&gt; to remove all chart instances upon navigation:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;kd&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;allChartIntances&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt;

&lt;span class=&quot;nb&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;addEventListener&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;turbo:before-cache&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;allChartIntances&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;forEach&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;chartInstance&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;chartInstance&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;dispose&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;

&lt;span class=&quot;nb&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;addEventListener&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;turbo:load&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;fooChart&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;echarts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;init&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;fooContainer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;allChartIntances&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;fooChart&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;kd&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;barChart&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;echarts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;init&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;barContainer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;allChartIntances&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;barChart&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;In the example above two charts are created and stored in the &lt;code&gt;allChartInstances&lt;/code&gt; array when the page first load. Before caching the page they are properly removed and when navigating back they will be re-created on &lt;code&gt;turbo:load&lt;/code&gt;.&lt;/p&gt;</description>
        <pubDate>Mon, 03 Apr 2023 00:00:00 -0300</pubDate>
        <link>https://glaucocustodio.github.io/2023/04/03/turbo-drive-plus-echarts-fixing-history-back/</link>
        <guid isPermaLink="true">https://glaucocustodio.github.io/2023/04/03/turbo-drive-plus-echarts-fixing-history-back/</guid>
      </item>
    
      <item>
        <title>How to Install Multiple Versions of Python on Apple Silicon / M1</title>
        <description>&lt;p&gt;I struggled to build and install Python on M1. Only got this error &lt;code&gt;configure: error: Unexpected output of &apos;arch&apos; on OSX&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I had Homebrew and &lt;a href=&quot;https://asdf-vm.com/&quot; target=&quot;_blank&quot; rel=&quot;external nofollow&quot;&gt;asdf&lt;/a&gt; installed for arm64 (default Apple Silicon / M1 architecture).&lt;/p&gt;

&lt;p&gt;That&apos;s what worked for me: duplicate iTerm (Finder -&gt; Applications -&gt; Right click on iTerm -&gt; Duplicate) and set it to run with Rosetta (Right click on the duplicated iTerm -&gt; Get Info -&gt; Mark &quot;Open using Rosetta&quot;).&lt;/p&gt;

&lt;p&gt;Rename the duplicate to something like iTerm Rosetta and you will end up with iTerm and iTerm Rosetta.&lt;/p&gt;

&lt;p&gt;Open iTerm Rosetta and type &lt;code&gt;arch&lt;/code&gt;, it must show &lt;code&gt;i386&lt;/code&gt; on iTerm Rosetta and &lt;code&gt;arm64&lt;/code&gt; on regular iTerm.&lt;/p&gt;

&lt;p&gt;Install Homebrew on &lt;code&gt;i386&lt;/code&gt;. Open iTerm Rosetta and run &lt;code&gt;/bin/bash -c &quot;$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)&quot;&lt;/code&gt;, then &lt;code&gt;which brew&lt;/code&gt; to confirm, you must see &lt;code&gt;/usr/local/bin/brew&lt;/code&gt;.&lt;/p&gt;

&lt;div class=&quot;note&quot;&gt;Homebrew for arm64 is installed on &lt;code&gt;/opt/homebrew/bin/brew&lt;/code&gt; instead.&lt;/div&gt;

&lt;p&gt;Now install the desired version of Python using asdf, eg: &lt;code&gt;asdf install python 3.8.9&lt;/code&gt;.&lt;/p&gt;
</description>
        <pubDate>Fri, 14 Oct 2022 00:00:00 -0300</pubDate>
        <link>https://glaucocustodio.github.io/2022/10/14/how-to-install-multiple-versions-of-python-on-apple-silicon-m1/</link>
        <guid isPermaLink="true">https://glaucocustodio.github.io/2022/10/14/how-to-install-multiple-versions-of-python-on-apple-silicon-m1/</guid>
      </item>
    
  </channel>
</rss>