<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Vladimir Dementyev</title>
    <description>The latest articles on DEV Community by Vladimir Dementyev (@palkan_tula).</description>
    <link>https://dev.to/palkan_tula</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F48217%2F7985e690-6765-41f5-ad5a-74c44393c25c.jpg</url>
      <title>DEV Community: Vladimir Dementyev</title>
      <link>https://dev.to/palkan_tula</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/palkan_tula"/>
    <language>en</language>
    <item>
      <title>Keynote tips: syntax highlighting</title>
      <dc:creator>Vladimir Dementyev</dc:creator>
      <pubDate>Mon, 04 Aug 2025 05:37:10 +0000</pubDate>
      <link>https://dev.to/palkan_tula/keynote-tips-syntax-highlighting-9nj</link>
      <guid>https://dev.to/palkan_tula/keynote-tips-syntax-highlighting-9nj</guid>
      <description>&lt;p&gt;It's been 9 years since I started my public speaking journey. Different cities, countries, and continents, presenting in front of a handful of people eating pizza or giving an opening keynote for hundreds of engineers, shaking like Elvis, and almost oversleeping my talk. The only thing that stayed constant—&lt;strong&gt;I used Apple's Keynote to craft all of my &lt;a href="https://noti.st/palkan#older" rel="noopener noreferrer"&gt;~40 slides&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Keynote is a popular presentation software (among Mac users), no doubt. Still, many software engineers prefer other, usually Markdown-driven, tools for &lt;em&gt;building&lt;/em&gt; their slides. Why so? Partially due to the &lt;em&gt;everything-should-be-code&lt;/em&gt; lifestyle, but I think most are attracted by the simplicity with which you can add beautiful code snippets to your presentation. In Markdown, you just write code and add the language specifier!&lt;/p&gt;

&lt;p&gt;How can one add highlighted code snippets to a Keynote presentation? Let's do a quick overview of options, finishing with the one I've ended up with.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option #1. Screenshots
&lt;/h2&gt;

&lt;p&gt;The first option is trivial—just take a screenshot of the code editor and paste it into your slide (that's how I started my Keynote journey). Simple? Yes. Maintainable? Not really.&lt;/p&gt;

&lt;p&gt;Still, it's an option (a last resort one). If you have to do that, consider using some specialized code-to-image tool like &lt;a href="https://carbon.now.sh/" rel="noopener noreferrer"&gt;carbon&lt;/a&gt; and not just crop an image of your editor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option #2. HTML
&lt;/h2&gt;

&lt;p&gt;Did you know that Keynote can consume HTML content? Copy some HTML and paste it onto a slide, and you'll see it being &lt;em&gt;transformed&lt;/em&gt; into a text box with styles preserved. Compared to screenshots, you can edit the text, change its characteristics (e.g., size), and so on.&lt;/p&gt;

&lt;p&gt;The question is: where to get an HTML version of the code?&lt;/p&gt;

&lt;p&gt;If you are a VS Code user, you can just copy the code from the editor. That's it. VS Code automatically saves an HTML version into the clipboard buffer.&lt;/p&gt;

&lt;p&gt;If your editor copies only the plain text (I'm looking at you, Zed), there is &lt;a href="https://psd-coder.github.io/code-to-markup" rel="noopener noreferrer"&gt;Code to Markup&lt;/a&gt; at your service!&lt;/p&gt;

&lt;p&gt;Although it is as simple as screen capturing, this approach has a couple of downsides. First, you have to switch between Keynote and an editor/website to highlight code snippets (not-so-ideal UX).&lt;/p&gt;

&lt;p&gt;Another thing that bothers me is the fact that aforementioned (maybe, all similar?) HTML generators preserve background color for code lines but not for gaps between them. That results in the following representation:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb5xsmrtj70klbfuv1lex.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb5xsmrtj70klbfuv1lex.png" alt=" " width="800" height="277"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You should either adjust your slide background or manually update it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option #3: RTF &amp;amp; Automator
&lt;/h2&gt;

&lt;p&gt;HTML is not the only format that can preserve code styling. There is also RTF (Rich Text Format). It is not as expressive as HTML, but it has everything we need to highlight code snippets. And from my experience, pasting code-as-RTF turned out to work the best with Keynote. And this experience is based on the setup described below.&lt;/p&gt;

&lt;p&gt;First, to turn a plain text code into its highlighted RTF representation, I use the tool called (surprise!)... &lt;a href="http://andre-simon.de/doku/highlight/en/highlight.php" rel="noopener noreferrer"&gt;highlight&lt;/a&gt;! It's a very old one, but it still works perfectly (people used to know how to write software that lasts).&lt;/p&gt;

&lt;p&gt;You can install it with Homebrew: &lt;code&gt;brew install highlight&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It's a command-line tool, our target code snippet &lt;em&gt;lives&lt;/em&gt; on a Keynote slide—how to connect them? Using &lt;strong&gt;Automator&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Automator is a macOS tool that allows you to customize your OS features. For example, you can create a &lt;em&gt;quick action&lt;/em&gt;—an action that can be triggered either from the context menu or via a hotkey combination. Do you see where I'm going? We will create a quick action to get selected text, turn it into RTF, and paste it back where it belongs. This way, we can highlight code in Keynote by pressing a key combination!&lt;/p&gt;

&lt;p&gt;Here is a step-by-step how-to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Open Automator, go to New -&amp;gt; Quick Action.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Select "text" in "Workflow receives current" (we will operate on the selected text). You can also limit the visibility of this action to particular applications (say, Keynote).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Open the action library and add the "Copy to Clipboard" to the workflow:&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1aq3uiefbjpdd889dggi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1aq3uiefbjpdd889dggi.png" alt=" " width="800" height="424"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Add the "Run AppleScript" action:&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzwg0ymydedka3ze3b4g8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzwg0ymydedka3ze3b4g8.png" alt=" " width="800" height="423"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Update the script as follows (feel free to tune the options to your taste):
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;on&lt;/span&gt; &lt;span class="nx"&gt;run&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;set&lt;/span&gt; &lt;span class="nx"&gt;command&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/opt/homebrew/bin/highlight --out-format rtf --syntax=ruby --style base16/chalk  --font &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;Martian Mono&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; --font-size 32&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="nx"&gt;shell&lt;/span&gt; &lt;span class="nx"&gt;script&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/bin/bash -c 'pbpaste | &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;command&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;| pbcopy'&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="nx"&gt;delay&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;
    &lt;span class="nx"&gt;tell&lt;/span&gt; &lt;span class="nx"&gt;application&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;System Events&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt; &lt;span class="nx"&gt;keystroke&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;v&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;using&lt;/span&gt; &lt;span class="nx"&gt;command&lt;/span&gt; &lt;span class="nx"&gt;down&lt;/span&gt;
&lt;span class="nx"&gt;end&lt;/span&gt; &lt;span class="nx"&gt;run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Save the workflow and go to Keynote to give it a try!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjzqp6j6mw0gahc5yuxpx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjzqp6j6mw0gahc5yuxpx.png" alt=" " width="800" height="469"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Go to System Settings -&amp;gt; Keyboard -&amp;gt; Shortcuts -&amp;gt; Services -&amp;gt; Text, find your action, and assign a keyboard combination to it. Now, you can highlight any code in Keynote by just pressing the specified keys!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F730xkfkc8exoejmi1x7b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F730xkfkc8exoejmi1x7b.png" alt=" " width="800" height="181"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This workflow is not ideal, but it has served me well for many years. You have to hardcode the theme, the font size, and the language (or have multiple actions). But you do that once per presentation (so, a few times a year max)—more than "good enough" for me.&lt;/p&gt;

&lt;p&gt;Sometimes the magic "tell application" command doesn't work, and I have to hit Cmd+V myself—again, not a big deal.&lt;/p&gt;

&lt;p&gt;I'm pretty sure the setup could be optimized further: &lt;code&gt;highlight&lt;/code&gt; replaced with some fancy and faster Rust CLI; AppleScript with JavaScript (why not?); maybe, Automator gurus know how to add options selection (language, theme) to the context menu. The core idea would stay the same—and that's what I wanted to share with you in this post.&lt;/p&gt;

&lt;p&gt;Happy conferencing!&lt;br&gt;
Share your Keynote vs. code tips!&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;P.S.&lt;/strong&gt; How to know what's been stored in the clipboard buffer after you hit Cmd+C? Run the following command in your terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;osascript &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s1"&gt;'the clipboard as record'&lt;/span&gt; | less
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>keynote</category>
      <category>speaking</category>
    </item>
    <item>
      <title>DIY multi-regional uptime monitoring with Fly.io and Uptime Kuma</title>
      <dc:creator>Vladimir Dementyev</dc:creator>
      <pubDate>Wed, 31 Aug 2022 22:03:45 +0000</pubDate>
      <link>https://dev.to/palkan_tula/diy-multi-regional-uptime-monitoring-with-flyio-and-uptime-kuma-5c53</link>
      <guid>https://dev.to/palkan_tula/diy-multi-regional-uptime-monitoring-with-flyio-and-uptime-kuma-5c53</guid>
      <description>&lt;p&gt;Two things happened recently that led to this post:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Heroku had a DNS problem (&lt;a href="https://status.heroku.com/incidents/2453" rel="noopener noreferrer"&gt;incident#2453&lt;/a&gt;, and many websites went down.&lt;/li&gt;
&lt;li&gt;Heroku announced their &lt;a href="https://blog.heroku.com/next-chapter" rel="noopener noreferrer"&gt;&lt;em&gt;next chapter&lt;/em&gt;&lt;/a&gt; (spoiler: no more free &lt;del&gt;cookies&lt;/del&gt; dynos).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I was affected by both (well, the second one has not come into effect yet). And I found this set of circumstances an excellent opportunity to try something new.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introducing Uptime Kuma
&lt;/h2&gt;

&lt;p&gt;The first event made me think of setting up an uptime monitoring (with DNS checks support), and an OSS project bubbled in my mind—&lt;a href="https://github.com/louislam/uptime-kuma" rel="noopener noreferrer"&gt;Uptime Kuma&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Uptime Kuma calls itself a "fancy monitoring tool". And I absolutely agree with that. It has both a great feature set and a slick UI. Here is my dashboard:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5dub6b4vo3gavvgqugfx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5dub6b4vo3gavvgqugfx.png" alt="Uptime Kuma dashboard"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can find the complete list of features in the repo; I was only interested in the basic HTTP and DNS checks. As for notifications, there are dozens of integrations. I chose Slack for simplicity and plan to add a Telegram integration for personal projects later.&lt;/p&gt;

&lt;p&gt;What about deployment? There is an official Docker image (&lt;code&gt;louislam/uptime-kuma&lt;/code&gt;), so all you need is a platform capable of spinning up Docker containers and taking care of them. Heroku could be a candidate, but I prefer not to pay for pet/research projects. So, I turned to the alternatives.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deploying Uptime Kuma to Fly
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://fly.io" rel="noopener noreferrer"&gt;Fly.io&lt;/a&gt; is a modern deployment platform focused on multi-regional availability ("close to your users", their main page says). By "modern", I mean providing a great developer experience: CLI-first, sensible defaults and configuration, and a generous free tier.&lt;/p&gt;

&lt;p&gt;You can deploy a Docker image to Fly with a single command:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;

flyctl launch &lt;span class="nt"&gt;--image&lt;/span&gt; louislam/uptime-kuma:1


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;That could be it for some apps, but for Uptime Kuma we need persistent storage to keep configuration and application settings (such as admin username and password). For that, we can use &lt;a href="https://fly.io/docs/reference/volumes" rel="noopener noreferrer"&gt;volumes&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Let's first create an application to generate the default &lt;code&gt;fly.toml&lt;/code&gt; file:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;

fly create
&lt;span class="c"&gt;# answer the questions, don't install PostgreSQL 🙂 &lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;In the resulting &lt;code&gt;fly.toml&lt;/code&gt; file, update the service port (Uptime Kuma uses 3001 by default):&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;

&lt;span class="nn"&gt;[[services]]&lt;/span&gt;
  &lt;span class="py"&gt;http_checks&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
  &lt;span class="py"&gt;internal_port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3001&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;And add the mounts section:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;

[mounts]
  source="kuma_data" # choose whatever name you want
  destination="/app/data"


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Now we need to create a volume manually:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;

fly volumes create kuma_data &lt;span class="nt"&gt;--region&lt;/span&gt; lhr &lt;span class="nt"&gt;--size&lt;/span&gt; 1


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;We use the smallest possible size (1 GB), which is too much for Uptime Kuma, but &lt;code&gt;fly&lt;/code&gt; doesn't allow passing floats 🤷‍♂️&lt;/p&gt;

&lt;p&gt;Now, we're ready to deploy our application!&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;

fly deploy


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;After the app has been deployed, you can open it and configure the monitors:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;

fly open


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
  
  
  Going multi-regional
&lt;/h2&gt;

&lt;p&gt;I decided to go further and turn my DIY monitoring into a high-available solution by creating a second application instance in another region.&lt;/p&gt;

&lt;p&gt;The tricky part here is dealing with volumes: a volume could only be attached to a single application instance. So it's just a &lt;em&gt;private storage&lt;/em&gt;, non-shared.&lt;/p&gt;

&lt;p&gt;If we want to deploy more instances, we must create new volumes. Let's create one in Chicago:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;

fly volumes create kuma_data &lt;span class="nt"&gt;--region&lt;/span&gt; lhr &lt;span class="nt"&gt;--size&lt;/span&gt; 0.1


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;We can check the status of our volumes by running the following command:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;

&lt;span class="nv"&gt;$ &lt;/span&gt;fly volumes list

ID        STATE    NAME      SIZE  REGION  ATTACHED VM
vol_xxxx  created  kuma_data 1GB   lhr     abc123    
vol_yyyy  created  kuma_data 1GB   yyz     


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Now, we need to add a new region to our application. First, I tried to follow &lt;a href="https://fly.io/docs/reference/scaling/#modifying-the-region-pool" rel="noopener noreferrer"&gt;the documentation&lt;/a&gt; and add a new region to the pool but failed:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;

&lt;span class="nv"&gt;$ &lt;/span&gt;fly regions add yyz

Error App &lt;span class="s1"&gt;'uptime_kuma'&lt;/span&gt; uses volumes to control regions. Add or remove volumes to change region placement.


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The error message itself wasn't really helpful (I created a volume, what's wrong?), but it led me to the &lt;a href="https://community.fly.io/t/deploying-redis-image-to-multiple-regions/6324/2" rel="noopener noreferrer"&gt;community discussion&lt;/a&gt; which explains that you don't need to add regions manually when using volumes. All you need is to scale the app, and Fly will choose a proper region based on the free volume:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;

fly scale count 2


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Awesome! Now we have two apps. But they are independent; how can we sync the configuration?&lt;/p&gt;

&lt;p&gt;To deal with configuration syncing, I decided to use the built-in Backups features of Uptime Kuma: export in one region and import into another.&lt;/p&gt;

&lt;p&gt;It turned out that there is no way to access an app deployed in the specific region, Fly takes care of geolocation-based routing itself, and you have no control over it. Solution? VPN!&lt;/p&gt;

&lt;p&gt;So, here is the step-by-step instruction on how to "sync" Uptime Kuma instances:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Open the app closest to your current location: &lt;code&gt;fly open&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Download the configuration backup.&lt;/li&gt;
&lt;li&gt;Connect to a VPN server closer to the second location and run &lt;code&gt;fly open&lt;/code&gt; again.&lt;/li&gt;
&lt;li&gt;During the first launch, a fresh Uptime Kuma instance prompts you to enter admin login details again—just use the same as for the first app.&lt;/li&gt;
&lt;li&gt;Go to Backups, choose the "Overwrite" option and upload a dump from the first app. That's it!&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;It took me about ~30 minutes to run a multi-regional monitoring system on Fly.io, and I hope it will last for years until Fly hits its &lt;em&gt;next chapter&lt;/em&gt; forcing me to look for an alternative 🙂&lt;/p&gt;

</description>
      <category>monitoring</category>
      <category>fly</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Faster RuboCop runs for Rails apps</title>
      <dc:creator>Vladimir Dementyev</dc:creator>
      <pubDate>Tue, 12 Apr 2022 18:07:08 +0000</pubDate>
      <link>https://dev.to/palkan_tula/faster-rubocop-runs-for-rails-apps-10me</link>
      <guid>https://dev.to/palkan_tula/faster-rubocop-runs-for-rails-apps-10me</guid>
      <description>&lt;p&gt;Recently, Nate Berkopec shared an interesting observation: running &lt;code&gt;bundle exec whatever&lt;/code&gt; could take seconds to boot if the Gemfile is huge (even when the executable itself requires a handful of dependencies).&lt;/p&gt;

&lt;p&gt;&lt;iframe class="tweet-embed" id="tweet-1494685271387881472-785" src="https://platform.twitter.com/embed/Tweet.html?id=1494685271387881472"&gt;
&lt;/iframe&gt;

  // Detect dark theme
  var iframe = document.getElementById('tweet-1494685271387881472-785');
  if (document.body.className.includes('dark-theme')) {
    iframe.src = "https://platform.twitter.com/embed/Tweet.html?id=1494685271387881472&amp;amp;theme=dark"
  }



&lt;/p&gt;

&lt;p&gt;That could be explained by the fact that Bundler has to verify the &lt;code&gt;Gemfile.lock&lt;/code&gt; file consistency (all the gems are installed). Thus, that's &lt;strong&gt;an expected behaviour&lt;/strong&gt; (that doesn't mean we shouldn't try to improve it; see, for example, Matthew Draper's &lt;a href="https://github.com/gel-rb/gel" rel="noopener noreferrer"&gt;Gel&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Rails developers usually put all the deps in the &lt;code&gt;Gemfile&lt;/code&gt;, including dev tools, such as &lt;a href="https://github.com/rubocop/rubocop" rel="noopener noreferrer"&gt;RuboCop&lt;/a&gt;. RuboCop is a linter, and &lt;strong&gt;linters must be fast&lt;/strong&gt;. RuboCop itself complies with this statement but running it via Bundler may not.&lt;/p&gt;

&lt;p&gt;How can we overcome this? Using a separate Gemfile!&lt;/p&gt;

&lt;p&gt;I've been using this technique for a long time for gems development—to speed up CI RuboCop runs (by installing only the linter dependencies). Here is my &lt;a href="https://github.com/anycable/anycable/blob/master/gemfiles/rubocop.gemfile" rel="noopener noreferrer"&gt;typical &lt;code&gt;rubocop.gemfile&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# gemfiles/rubocop.gemfile&lt;/span&gt;
&lt;span class="n"&gt;source&lt;/span&gt; &lt;span class="s2"&gt;"https://rubygems.org"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s2"&gt;"rubocop-md"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"~&amp;gt; 1.0"&lt;/span&gt;
  &lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s2"&gt;"rubocop-rspec"&lt;/span&gt;
  &lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s2"&gt;"standard"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"~&amp;gt; 1.0"&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To use it with Bundler, we need to specify the &lt;code&gt;BUNDLER_GEMFILE&lt;/code&gt; env variable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# first, install the deps&lt;/span&gt;
&lt;span class="nv"&gt;BUNDLE_GEMFILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;gemfiles/rubocop.gemfile bundle &lt;span class="nb"&gt;install&lt;/span&gt;

&lt;span class="c"&gt;# then, run the executable&lt;/span&gt;
&lt;span class="nv"&gt;BUNDLE_GEMFILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;gemfiles/rubocop.gemfile bundle &lt;span class="nb"&gt;exec &lt;/span&gt;rubocop
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This verbose approach works well enough for machines (CI), but not for humans: maintaining a separate lockfile and using env vars in development is far from the perfect user experience.&lt;/p&gt;

&lt;p&gt;For Rails applications development, we came up with the following trick to run commands backed by custom gemfiles—adding a simple &lt;code&gt;bin/whatever&lt;/code&gt; wrapper. Here is our &lt;code&gt;bin/rubocop&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;

&lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;dirname&lt;/span&gt; &lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;/..

&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;BUNDLE_GEMFILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./gemfiles/rubocop.gemfile
bundle check &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null &lt;span class="o"&gt;||&lt;/span&gt; bundle &lt;span class="nb"&gt;install

&lt;/span&gt;bundle &lt;span class="nb"&gt;exec &lt;/span&gt;rubocop &lt;span class="nv"&gt;$@&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;em&gt;magic&lt;/em&gt; &lt;code&gt;$@&lt;/code&gt; argument proxies everything you pass to &lt;code&gt;bin/rubocop&lt;/code&gt;, thus, making this wrapper &lt;em&gt;quack&lt;/em&gt; like RuboCop.&lt;/p&gt;

&lt;p&gt;We also do &lt;code&gt;bundle check || bundle install&lt;/code&gt; to make sure all the deps are present (so, you don't need to run &lt;code&gt;bundle install&lt;/code&gt; yourself).&lt;/p&gt;

&lt;p&gt;That's it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;P.S.&lt;/strong&gt; Why not use inline gemfiles (as &lt;a href="https://twitter.com/fxn/status/1494763178391224323" rel="noopener noreferrer"&gt;Xavier Noria suggested&lt;/a&gt;)? We could write our &lt;code&gt;bin/rubocop&lt;/code&gt; like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'bundler/inline'&lt;/span&gt;

&lt;span class="n"&gt;gemfile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;quiet: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s2"&gt;"rubocop-md"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"~&amp;gt; 1.0"&lt;/span&gt;
  &lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s2"&gt;"rubocop-rspec"&lt;/span&gt;
  &lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s2"&gt;"standard"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"~&amp;gt; 1.0"&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'rubocop'&lt;/span&gt;
&lt;span class="no"&gt;RuboCop&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;CLI&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, with this approach, there is no lockfile at all. We want to make sure everyone is using the same versions of dependencies (to avoid "works on my computer" situations). Of course, we can use the exact version in the &lt;code&gt;gemfile do ... end&lt;/code&gt; block, but, IMO, managing deps with Bundler is more convenient (e.g., you can run &lt;code&gt;bundle update&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;P.P.S.&lt;/strong&gt; One of the benefits of this approach is the ability to run linters (and other tools, e.g., &lt;a href="https://evilmartians.com/chronicles/kubing-rails-stressless-kubernetes-deployments-with-kuby#docker-for-development-vs-kuby-for-deployment" rel="noopener noreferrer"&gt;Kuby&lt;/a&gt;) locally while using &lt;a href="https://evilmartians.com/chronicles/ruby-on-whales-docker-for-ruby-rails-development" rel="noopener noreferrer"&gt;Docker for application development&lt;/a&gt;; no need to spin up containers to run RuboCop. It's especially helpful if you want to use Git hooks or editor integrations.&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>rails</category>
      <category>linters</category>
      <category>docker</category>
    </item>
    <item>
      <title>Rails-docker-box, or developing Rails within a dockerized environment</title>
      <dc:creator>Vladimir Dementyev</dc:creator>
      <pubDate>Tue, 22 Mar 2022 13:13:25 +0000</pubDate>
      <link>https://dev.to/palkan_tula/rails-docker-box-or-developing-rails-within-a-dockerized-environment-4min</link>
      <guid>https://dev.to/palkan_tula/rails-docker-box-or-developing-rails-within-a-dockerized-environment-4min</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;⚠️ Attention! This post is &lt;strong&gt;not about using Docker to develop Rails applications&lt;/strong&gt;, but about using Docker to develop the Rails framework itself. For the former one, please, visit the &lt;a href="https://evilmartians.com/chronicles/ruby-on-whales-docker-for-ruby-rails-development"&gt;Ruby on Whales article&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  From boxes to containers, or a bit of history
&lt;/h2&gt;

&lt;p&gt;I've been contributing to Rails (from time to time) since 2015. Developing such a massive framework as Rails very differs from working on a web application built with it. First of all, you need to cover all the possible configurations: databases, cache servers, etc.; many system dependencies (e.g., &lt;code&gt;libxml2&lt;/code&gt; or &lt;code&gt;ffmpeg&lt;/code&gt;) must be installed.&lt;/p&gt;

&lt;p&gt;Secondly, unlike for a private project, where each team member &lt;strong&gt;has to deal with this setup&lt;/strong&gt;, an open-source project should be...hm...&lt;em&gt;open&lt;/em&gt; for everyone willing to contribute. The more complicated it is to configure a proper environment the more likely potential contributors would give up. And we don't want this, right?&lt;/p&gt;

&lt;p&gt;Luckily, the Rails team (and &lt;a href="https://github.com/fxn"&gt;Xavier Noria&lt;/a&gt; in particular) found a way to solve this problem—&lt;a href="https://github.com/rails/rails-dev-box"&gt;rails-dev-box&lt;/a&gt;. Rails Dev Box is a &lt;a href="https://www.vagrantup.com"&gt;Vagrant&lt;/a&gt; configuration, which allows you to spin up a virtual machine with everything you need inside. Cool, right?&lt;/p&gt;

&lt;p&gt;Yeah, that was really cool. In 2015.&lt;/p&gt;

&lt;p&gt;I gave up on VM-based development in 2017, when I found that running a VM along with a couple of Electron-based apps no longer fit my laptop. I turned to containers.&lt;/p&gt;

&lt;p&gt;Since then, I started using Docker not only for &lt;a href="https://evilmartians.com/chronicles/ruby-on-whales-docker-for-ruby-rails-development"&gt;applications development&lt;/a&gt; but also for hacking around with Rails.&lt;/p&gt;

&lt;p&gt;Since I mostly dealt with Active Record and Action Cable, my Docker configuration wasn't complete. Also, back in the days, the Rails codebase wasn't &lt;em&gt;container-friendly&lt;/em&gt; (e.g., some tests relied on a Redis or PostgreSQL instance running on the same machine). Thus, I just kept my setup around (in &lt;a href="https://github.com/palkan/rails/commits/chore/dockerize-dev"&gt;a few commits&lt;/a&gt;], and haven't tried to promote to the upstream or whatever.&lt;/p&gt;

&lt;p&gt;Lately, I've been working &lt;a href="https://github.com/rails/rails/pull/44696"&gt;a new PR to Action Cable&lt;/a&gt; and had to re-visit my configuration (since many things have changed in the last year). I liked what I got in the end, so I decided to share it with the community.&lt;/p&gt;

&lt;p&gt;Below is the &lt;em&gt;compatibility table&lt;/em&gt;—which libraries are currently supported (i.e., &lt;code&gt;rake test&lt;/code&gt; passes):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;actioncable ✅&lt;/li&gt;
&lt;li&gt;actionmailbox ✅&lt;/li&gt;
&lt;li&gt;actionmailer ✅&lt;/li&gt;
&lt;li&gt;actionpack ✅&lt;/li&gt;
&lt;li&gt;actiontext ✅&lt;/li&gt;
&lt;li&gt;actionview ✅&lt;/li&gt;
&lt;li&gt;activejob ✅&lt;/li&gt;
&lt;li&gt;activemodel ✅&lt;/li&gt;
&lt;li&gt;activerecord:

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;rake test:sqlite3&lt;/code&gt; ✅ ⚠️: &lt;code&gt;26761 assertions, 2 failures, 2 errors, 27 skips&lt;/code&gt; (&lt;code&gt;sqlite3: not found&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;rake test:postgresql&lt;/code&gt; ✅ ⚠️: &lt;code&gt;28766 assertions, 0 failures, 2 errors, 18 skips&lt;/code&gt; (&lt;code&gt;couldn't connect to /var/run/postgresql/.s.PGSQL.5432&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;rake test:mysql2&lt;/code&gt; 🚫 (no &lt;code&gt;mysql&lt;/code&gt; database configured)&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;activestorage ⚠️ (&lt;em&gt;some system deps missing&lt;/em&gt;)&lt;/li&gt;
&lt;li&gt;activesupport ✅ ⚠️ (evented file checker tests fail 🤔)&lt;/li&gt;
&lt;li&gt;railties 🚫 (&lt;code&gt;No such file or directory - yarn&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; JavaScript tests are not supported (no Node/Yarn env configured).&lt;/p&gt;

&lt;h2&gt;
  
  
  Docker, Compose, and Dip walks into a bar
&lt;/h2&gt;

&lt;p&gt;It's all started with just two files: &lt;code&gt;Dockerfile&lt;/code&gt; and &lt;code&gt;docker-compose.yml&lt;/code&gt;. Although that was good enough to "build" a project and run tests, the overall developer experience was barely satisfying.&lt;/p&gt;

&lt;p&gt;So, I went the old-fashioned way and added &lt;a href="https://github.com/bibendi/dip"&gt;Dip&lt;/a&gt; to the mix.&lt;br&gt;
Now I can run all the familiar commands (&lt;code&gt;bundle&lt;/code&gt;, &lt;code&gt;rake&lt;/code&gt;, etc.) from my host system (with a &lt;code&gt;dip&lt;/code&gt; prefix) without thinking about all the &lt;code&gt;docker-compose --rm --it bla-bla&lt;/code&gt;. Moreover, I can &lt;code&gt;cd&lt;/code&gt; into a subfolder (say, &lt;code&gt;actioncable&lt;/code&gt;), and execute commands from there just like on a host machine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Installing deps at the project's root level&lt;/span&gt;
dip bundle &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;span class="c"&gt;# Run all Rails tests (I never tried 🙂)&lt;/span&gt;
dip rake &lt;span class="nb"&gt;test&lt;/span&gt;

&lt;span class="c"&gt;# That's what I usually do&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;actioncable
&lt;span class="c"&gt;# Install Action Cable dev deps&lt;/span&gt;
dip bundle
&lt;span class="c"&gt;# Run Action Cable tests&lt;/span&gt;
dip rake
&lt;span class="c"&gt;# Or run a particular test file&lt;/span&gt;
dip &lt;span class="nb"&gt;test test&lt;/span&gt;/connection/streams_test.rb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;dip test&lt;/code&gt; command is an alias for &lt;code&gt;bundle exec ruby -Ilib:test&lt;/code&gt;—and that's my favourite one ♥️&lt;/p&gt;

&lt;p&gt;Want to give this setup a try? You can grab it right from this post (or from the &lt;a href="https://gist.github.com/palkan/f745f4da3e75d4bcf5bbb3bc3f6a4661"&gt;gist&lt;/a&gt;!&lt;/p&gt;

&lt;p&gt;Here is the configuration I keep at the project's root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.dockerdev/
  Aptfile
  .bashrc
  Dockerfile
  compose.yml
  # Active Record configs
  config.yml
&amp;lt;some rails files&amp;gt;
dip.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the contents of all the files:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;.dockerdev/Dockerfile&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; RUBY_VERSION&lt;/span&gt;
&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; DISTRO_NAME=bullseye&lt;/span&gt;

&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; ruby:$RUBY_VERSION-slim-$DISTRO_NAME&lt;/span&gt;
&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; DISTRO_NAME&lt;/span&gt;

&lt;span class="c"&gt;# Common dependencies&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get update &lt;span class="nt"&gt;-qq&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;DEBIAN_FRONTEND&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;noninteractive apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-yq&lt;/span&gt; &lt;span class="nt"&gt;--no-install-recommends&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    build-essential &lt;span class="se"&gt;\
&lt;/span&gt;    gnupg2 &lt;span class="se"&gt;\
&lt;/span&gt;    curl &lt;span class="se"&gt;\
&lt;/span&gt;    less &lt;span class="se"&gt;\
&lt;/span&gt;    git &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt-get clean &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /tmp/&lt;span class="k"&gt;*&lt;/span&gt; /var/tmp/&lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;truncate&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; 0 /var/log/&lt;span class="k"&gt;*&lt;/span&gt;log

&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; PG_MAJOR&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;curl &lt;span class="nt"&gt;-sSL&lt;/span&gt; https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo &lt;/span&gt;deb http://apt.postgresql.org/pub/repos/apt/ &lt;span class="nv"&gt;$DISTRO_NAME&lt;/span&gt;&lt;span class="nt"&gt;-pgdg&lt;/span&gt; main &lt;span class="nv"&gt;$PG_MAJOR&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /etc/apt/sources.list.d/pgdg.list
&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get update &lt;span class="nt"&gt;-qq&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;DEBIAN_FRONTEND&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;noninteractive apt-get &lt;span class="nt"&gt;-yq&lt;/span&gt; dist-upgrade &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="nv"&gt;DEBIAN_FRONTEND&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;noninteractive apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-yq&lt;/span&gt; &lt;span class="nt"&gt;--no-install-recommends&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    libpq-dev &lt;span class="se"&gt;\
&lt;/span&gt;    postgresql-client-&lt;span class="nv"&gt;$PG_MAJOR&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    apt-get clean &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /tmp/&lt;span class="k"&gt;*&lt;/span&gt; /var/tmp/&lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nb"&gt;truncate&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; 0 /var/log/&lt;span class="k"&gt;*&lt;/span&gt;log

&lt;span class="c"&gt;# Application dependencies&lt;/span&gt;
&lt;span class="c"&gt;# We use an external Aptfile for this, stay tuned&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; Aptfile /tmp/Aptfile&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get update &lt;span class="nt"&gt;-qq&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;DEBIAN_FRONTEND&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;noninteractive apt-get &lt;span class="nt"&gt;-yq&lt;/span&gt; dist-upgrade &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="nv"&gt;DEBIAN_FRONTEND&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;noninteractive apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-yq&lt;/span&gt; &lt;span class="nt"&gt;--no-install-recommends&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-Ev&lt;/span&gt; &lt;span class="s1"&gt;'^\s*#'&lt;/span&gt; /tmp/Aptfile | xargs&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    apt-get clean &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /tmp/&lt;span class="k"&gt;*&lt;/span&gt; /var/tmp/&lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nb"&gt;truncate&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; 0 /var/log/&lt;span class="k"&gt;*&lt;/span&gt;log

&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; LANG C.UTF-8&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; GEM_HOME /bundle&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; BUNDLE_PATH=$GEM_HOME \&lt;/span&gt;
  BUNDLE_APP_CONFIG=$BUNDLE_PATH \
  BUNDLE_BIN=$BUNDLE_PATH/bin \
  BUNDLE_JOBS=4 \
  BUNDLE_RETRY=3
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; PATH /app/bin:$BUNDLE_BIN:$PATH&lt;/span&gt;

&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; BUNDLER_VERSION&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;gem update &lt;span class="nt"&gt;--system&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    gem &lt;span class="nb"&gt;install &lt;/span&gt;bundler

&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /app
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;

&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["/usr/bin/bash"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;.dockerdev/Aptfile&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;vim
# Build tools
autoconf
libtool
libncurses5-dev
libxml2-dev
# ActiveRecord deps
libsqlite3-dev
default-libmysqlclient-dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;.dockerdev/compose.yml&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;x-app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nl"&gt;&amp;amp;app&lt;/span&gt;
  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
    &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;RUBY_VERSION&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.0.2'&lt;/span&gt;
      &lt;span class="na"&gt;PG_MAJOR&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;14'&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rails-dev:7.1.0&lt;/span&gt;
  &lt;span class="na"&gt;tmpfs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/tmp&lt;/span&gt;

&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;runner&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;*app&lt;/span&gt;
    &lt;span class="na"&gt;stdin_open&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;tty&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;..:/app:cached&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;bundle:/bundle&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;history:/usr/local/hist&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./.psqlrc:/root/.psqlrc:ro&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./.bashrc:/root/.bashrc:ro&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;REDIS_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis://redis:6379/&lt;/span&gt;
      &lt;span class="na"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres://postgres:postgres@postgres/&lt;/span&gt;
      &lt;span class="na"&gt;HISTFILE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/usr/local/hist/.bash_history&lt;/span&gt;
      &lt;span class="na"&gt;XDG_DATA_HOME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/app/tmp/caches&lt;/span&gt;
      &lt;span class="na"&gt;EDITOR&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;vi&lt;/span&gt;
      &lt;span class="c1"&gt;# Use PostgreSQL by default for AR tests&lt;/span&gt;
      &lt;span class="na"&gt;ARCONN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${ARCONN:-postgresql}&lt;/span&gt;
    &lt;span class="na"&gt;working_dir&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${WORK_DIR:-/app}&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;postgres&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;
      &lt;span class="na"&gt;redis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;

  &lt;span class="na"&gt;postgres&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:14&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.psqlrc:/root/.psqlrc:ro&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;postgres:/var/lib/postgresql/data&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;history:/user/local/hist&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;PSQL_HISTFILE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/user/local/hist/.psql_history&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
      &lt;span class="c1"&gt;# For createdb&lt;/span&gt;
      &lt;span class="na"&gt;PGPASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="m"&gt;5432&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pg_isready -U postgres -h 127.0.0.1&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;

  &lt;span class="na"&gt;redis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis:6.2-alpine&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;redis:/data&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="m"&gt;6379&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis-cli ping&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;3s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;history&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;postgres&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;redis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;bundle&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;.dockerdev/.bashrc&lt;/code&gt; (put your favorite Bash extensions there)
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;alias be="bundle exec"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;dip.yml&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;7.1'&lt;/span&gt;

&lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;WORK_DIR&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/app/${DIP_WORK_DIR_REL_PATH}&lt;/span&gt;

&lt;span class="na"&gt;compose&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;files&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.dockerdev/compose.yml&lt;/span&gt;
  &lt;span class="na"&gt;project_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rails_dev&lt;/span&gt;

&lt;span class="na"&gt;interaction&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# This command spins up a Rails container with the required dependencies (such as databases),&lt;/span&gt;
  &lt;span class="c1"&gt;# and opens a terminal within it.&lt;/span&gt;
  &lt;span class="na"&gt;runner&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Open a Bash shell within a Rails container (with dependencies up)&lt;/span&gt;
    &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;runner&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/bin/bash&lt;/span&gt;

  &lt;span class="c1"&gt;# Run a Rails container without any dependent services (useful for non-Rails scripts)&lt;/span&gt;
  &lt;span class="na"&gt;bash&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run an arbitrary script within a container (or open a shell without deps)&lt;/span&gt;
    &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;runner&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/bin/bash&lt;/span&gt;
    &lt;span class="na"&gt;compose_run_options&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;no-deps&lt;/span&gt; &lt;span class="pi"&gt;]&lt;/span&gt;

  &lt;span class="c1"&gt;# A shortcut to run Bundler commands&lt;/span&gt;
  &lt;span class="na"&gt;bundle&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run Bundler commands&lt;/span&gt;
    &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;runner&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bundle&lt;/span&gt;
    &lt;span class="na"&gt;compose_run_options&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;no-deps&lt;/span&gt; &lt;span class="pi"&gt;]&lt;/span&gt;

  &lt;span class="na"&gt;rake&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run Rake commands&lt;/span&gt;
    &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;runner&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bundle exec rake&lt;/span&gt;

  &lt;span class="na"&gt;ruby&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run Ruby with Bundler activated&lt;/span&gt;
    &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;runner&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bundle exec ruby&lt;/span&gt;

  &lt;span class="na"&gt;rubocop&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run RuboCop&lt;/span&gt;
    &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;runner&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bundle exec rubocop&lt;/span&gt;
    &lt;span class="na"&gt;compose_run_options&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;no-deps&lt;/span&gt; &lt;span class="pi"&gt;]&lt;/span&gt;

  &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run a single test file (an alias for ruby -Ilib:test)&lt;/span&gt;
    &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;runner&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bundle exec ruby -Ilib:test&lt;/span&gt;

  &lt;span class="na"&gt;psql&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run Postgres psql console&lt;/span&gt;
    &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
    &lt;span class="na"&gt;default_args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;anycasts_dev&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;psql -h postgres -U postgres&lt;/span&gt;

  &lt;span class="na"&gt;createdb&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Create a PostgreSQL database&lt;/span&gt;
    &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;createdb -h postgres -U postgres&lt;/span&gt;

  &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;redis-cli'&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run Redis console&lt;/span&gt;
    &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis-cli -h redis&lt;/span&gt;

&lt;span class="na"&gt;provision&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;dip compose down --volumes&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;dip compose up -d postgres redis&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;dip bundle install&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;(test -f activerecord/test/config.yml) || (cp .dockerdev/config.yml activerecord/test/config.yml)&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;dip createdb activerecord_unittest&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;dip createdb activerecord_unittest2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Bonus: Git ignore without &lt;code&gt;.gitignore&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The final question: since we keep it in the project's directory, and this is not an official setup (at least, yet), we need to make sure we do not accidentally commit it to the repo. In other words, how to Git-ignore our configuration without updating the &lt;code&gt;.gitignore&lt;/code&gt; file? And the answer is—&lt;code&gt;.git/info/exclude&lt;/code&gt;. That's a specific, local, Git configuration file, which works similarly to &lt;code&gt;.gitignore&lt;/code&gt;. So, just open this file (say, &lt;code&gt;code .git/info/exclude&lt;/code&gt;) and drop the following lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# .git/info/exclude

dip.yml
.dockerdev/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it!&lt;/p&gt;

&lt;p&gt;P.S. For hacking with Ruby (MRI), I also have a dockerized environment: &lt;a href="https://github.com/palkan/ruby-dip"&gt;ruby-dip&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>rails</category>
      <category>docker</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Climbing Steep hills, or adopting Ruby 3 types</title>
      <dc:creator>Vladimir Dementyev</dc:creator>
      <pubDate>Fri, 11 Dec 2020 07:53:55 +0000</pubDate>
      <link>https://dev.to/evilmartians/climbing-steep-hills-or-adopting-ruby-3-types-181a</link>
      <guid>https://dev.to/evilmartians/climbing-steep-hills-or-adopting-ruby-3-types-181a</guid>
      <description>&lt;p&gt;With Ruby 3.0 just around the corner, let's take a look at one of the highlights of the upcoming release: &lt;a href="https://github.com/ruby/rbs"&gt;Ruby Type Signatures&lt;/a&gt;. Yes, types come to our favourite dynamic language—let's see what could work out of that!&lt;/p&gt;

&lt;p&gt;It is not the first time I'm writing about types for Ruby: more than a year ago, I &lt;em&gt;tasted&lt;/em&gt; &lt;a href="https://sorbet.org"&gt;Sorbet&lt;/a&gt; and shared my experience in the &lt;a href="https://evilmartians.com/chronicles/sorbetting-a-gem"&gt;Martian Chronicles&lt;/a&gt;. At the end of the post, I promised to give another Ruby type checker a try: &lt;a href="https://github.com/soutaro/steep"&gt;Steep&lt;/a&gt;. So, here I am, paying my debts!&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I'd highly recommend taking a look at the &lt;a href="https://evilmartians.com/chronicles/sorbetting-a-gem"&gt;"Sorbetting a gem" post&lt;/a&gt; first since I will refer to it multiple times today.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  RBS in a nutshell
&lt;/h2&gt;

&lt;p&gt;RBS is a language to describe &lt;em&gt;the structure of Ruby programs&lt;/em&gt; (from &lt;a href="https://github.com/ruby/rbs"&gt;Readme&lt;/a&gt;). The "structure" includes class and method signatures, type definitions, etc.&lt;/p&gt;

&lt;p&gt;Since it's a separate language, not Ruby, separate &lt;code&gt;.rbs&lt;/code&gt; files are used to store typings.&lt;/p&gt;

&lt;p&gt;Let's jump right into an example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# martian.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Martian&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Alien&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;evil: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="vi"&gt;@evil&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;evil&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;evil?&lt;/span&gt;
    &lt;span class="vi"&gt;@evil&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;# martian.rbs&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Alien&lt;/span&gt;
  &lt;span class="nb"&gt;attr_reader&lt;/span&gt; &lt;span class="nb"&gt;name&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;String&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="no"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;void&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Martian&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Alien&lt;/span&gt;
  &lt;span class="vi"&gt;@evil&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;bool&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="no"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="ss"&gt;evil: &lt;/span&gt;&lt;span class="n"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;void&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;evil?&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;bool&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The signature looks pretty similar to the class definition itself, except that we have types specified for arguments, methods, and instance variables. So far, looks pretty Ruby-ish. However, RBS has some entities which are missing in Ruby, for example, &lt;em&gt;interfaces&lt;/em&gt;. We're gonna see some examples later.&lt;/p&gt;

&lt;p&gt;RBS itself doesn't provide any functionality to perform type checking*; it's just a language, remember? That's where Steep comes into a stage.&lt;/p&gt;

&lt;p&gt;* Actually, that's not 100% true; there is runtime type checking mode. Continue reading to learn more.&lt;/p&gt;

&lt;p&gt;In the rest of the article, I will describe the process of adding RBS and Steep to &lt;a href="https://github.com/palkan/rubanok"&gt;Rubanok&lt;/a&gt; (the same project as I used in the Sorbet example, though the more recent version).&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting started with RBS
&lt;/h2&gt;

&lt;p&gt;It could be hard to figure out how to start adding types to an existing project. Hopefully, RBS provides a way to generate a types scaffold for your code.&lt;/p&gt;

&lt;p&gt;RBS comes with a CLI tool (&lt;code&gt;rbs&lt;/code&gt;) which has a bunch of commands, but we're interested only in the &lt;code&gt;prototype&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;rbs prototype &lt;span class="nt"&gt;-h&lt;/span&gt;
Usage: rbs prototype &lt;span class="o"&gt;[&lt;/span&gt;generator...] &lt;span class="o"&gt;[&lt;/span&gt;args...]

Generate prototype of RBS files.
Supported generators are rb, rbi, runtime.

Examples:

  &lt;span class="nv"&gt;$ &lt;/span&gt;rbs prototype rb foo.rb
  &lt;span class="nv"&gt;$ &lt;/span&gt;rbs prototype rbi foo.rbi
  &lt;span class="nv"&gt;$ &lt;/span&gt;rbs prototype runtime String
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The description is pretty self-explanatory; let's try it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;rbs prototype rb lib/&lt;span class="k"&gt;**&lt;/span&gt;/&lt;span class="k"&gt;*&lt;/span&gt;.rb

&lt;span class="c"&gt;# Rubanok provides a DSL ... (all the comments from the source file) &lt;/span&gt;
module Rubanok
  attr_accessor ignore_empty_values: untyped
  attr_accessor fail_when_no_matches: untyped
end

module Rubanok
  class Rule
    &lt;span class="c"&gt;# :nodoc:&lt;/span&gt;
    UNDEFINED: untyped

    attr_reader fields: untyped
    attr_reader activate_on: untyped
    attr_reader activate_always: untyped
    attr_reader ignore_empty_values: untyped
    attr_reader filter_with: untyped

    def initialize: &lt;span class="o"&gt;(&lt;/span&gt;untyped fields, ?activate_on: untyped activate_on, ?activate_always: bool activate_always, ?ignore_empty_values: untyped ignore_empty_values, ?filter_with: untyped? filter_with&lt;span class="o"&gt;)&lt;/span&gt; -&amp;gt; untyped
    def project: &lt;span class="o"&gt;(&lt;/span&gt;untyped params&lt;span class="o"&gt;)&lt;/span&gt; -&amp;gt; untyped
    def applicable?: &lt;span class="o"&gt;(&lt;/span&gt;untyped params&lt;span class="o"&gt;)&lt;/span&gt; -&amp;gt; &lt;span class="o"&gt;(&lt;/span&gt;::TrueClass | untyped&lt;span class="o"&gt;)&lt;/span&gt;
    def to_method_name: &lt;span class="o"&gt;()&lt;/span&gt; -&amp;gt; untyped

    private

    def build_method_name: &lt;span class="o"&gt;()&lt;/span&gt; -&amp;gt; ::String
    def fetch_value: &lt;span class="o"&gt;(&lt;/span&gt;untyped params, untyped field&lt;span class="o"&gt;)&lt;/span&gt; -&amp;gt; untyped
    def empty?: &lt;span class="o"&gt;(&lt;/span&gt;untyped val&lt;span class="o"&gt;)&lt;/span&gt; -&amp;gt; &lt;span class="o"&gt;(&lt;/span&gt;::FalseClass | untyped&lt;span class="o"&gt;)&lt;/span&gt;
  end
end

&lt;span class="c"&gt;# &amp;lt;truncated&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first option (&lt;code&gt;prototype rb&lt;/code&gt;) generates a signature for all the entities specified in the file (or files) you pass using static analysis (more precisely, via parsing the source code and analyzing ASTs).&lt;/p&gt;

&lt;p&gt;This command &lt;em&gt;streams&lt;/em&gt; to the standard output all the found typings. To save the output, one can use redirection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rbs prototype rb lib/&lt;span class="k"&gt;**&lt;/span&gt;/&lt;span class="k"&gt;*&lt;/span&gt;.rb &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; sig/rubanok.rbs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I'd prefer to mirror signature files to source files (i.e., have multiple files). We can achieve this with some knowledge of Unix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;find lib &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="se"&gt;\*&lt;/span&gt;.rb &lt;span class="nt"&gt;-print&lt;/span&gt; | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-sd&lt;/span&gt; / &lt;span class="nt"&gt;-f&lt;/span&gt; 2- | xargs &lt;span class="nt"&gt;-I&lt;/span&gt;&lt;span class="o"&gt;{}&lt;/span&gt; bash &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'export file={}; export target=sig/$file; mkdir -p ${target%/*}; rbs prototype rb lib/$file &amp;gt; sig/${file/rb/rbs}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In my opinion, it would be much better if we had the above functionality by default (or maybe that's a feature—keeping all the signatures in the same file 🤔). &lt;/p&gt;

&lt;p&gt;Also, copying comments from source files to signatures makes the latter less readable (especially if there are many comments, like in my case). Of course, we can add a bit more Unix magic to fix this...&lt;/p&gt;

&lt;p&gt;Let's try runtime mode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ RUBYOPT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"-Ilib"&lt;/span&gt; rbs prototype runtime &lt;span class="nt"&gt;-r&lt;/span&gt; rubanok Rubanok::Rule

class Rubanok::Rule
  public

  def activate_always: &lt;span class="o"&gt;()&lt;/span&gt; -&amp;gt; untyped
  def activate_on: &lt;span class="o"&gt;()&lt;/span&gt; -&amp;gt; untyped
  def applicable?: &lt;span class="o"&gt;(&lt;/span&gt;untyped params&lt;span class="o"&gt;)&lt;/span&gt; -&amp;gt; untyped
  def fields: &lt;span class="o"&gt;()&lt;/span&gt; -&amp;gt; untyped
  def filter_with: &lt;span class="o"&gt;()&lt;/span&gt; -&amp;gt; untyped
  def ignore_empty_values: &lt;span class="o"&gt;()&lt;/span&gt; -&amp;gt; untyped
  def project: &lt;span class="o"&gt;(&lt;/span&gt;untyped params&lt;span class="o"&gt;)&lt;/span&gt; -&amp;gt; untyped
  def to_method_name: &lt;span class="o"&gt;()&lt;/span&gt; -&amp;gt; untyped

  private

  def build_method_name: &lt;span class="o"&gt;()&lt;/span&gt; -&amp;gt; untyped
  def empty?: &lt;span class="o"&gt;(&lt;/span&gt;untyped val&lt;span class="o"&gt;)&lt;/span&gt; -&amp;gt; untyped
  def fetch_value: &lt;span class="o"&gt;(&lt;/span&gt;untyped params, untyped field&lt;span class="o"&gt;)&lt;/span&gt; -&amp;gt; untyped
  def initialize: &lt;span class="o"&gt;(&lt;/span&gt;untyped fields, ?activate_on: untyped, ?activate_always: untyped, ?ignore_empty_values: untyped, ?filter_with: untyped&lt;span class="o"&gt;)&lt;/span&gt; -&amp;gt; untyped
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this mode, RBS uses Ruby introspection APIs (&lt;code&gt;Class.methods&lt;/code&gt;, etc.) to generate the specified class or module signature.&lt;/p&gt;

&lt;p&gt;Let's compare signatures for the &lt;code&gt;Rubanok::Rule&lt;/code&gt; class generated with &lt;code&gt;rb&lt;/code&gt; and &lt;code&gt;runtime&lt;/code&gt; modes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;First, runtime generator does not recognize &lt;code&gt;attr_reader&lt;/code&gt; (for instance, &lt;code&gt;activate_on&lt;/code&gt; and &lt;code&gt;activate_always&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Second, runtime generator sorts methods alphabetically while static generator preserves the original layout.&lt;/li&gt;
&lt;li&gt;Finally, the first signature has a few types defined, while the latter has everything &lt;code&gt;untyped&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So, why one may find runtime generator useful? I guess there are only one reason for that: &lt;strong&gt;dynamically generated methods&lt;/strong&gt;. Like, for example, in &lt;a href="https://github.com/rails/rails/blob/5384bbc4ce937b4d539518b4e3cefb91e03d4f80/activerecord/lib/active_record/relation/query_methods.rb#L111-L132"&gt;Active Record&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Thus, both modes have their advantages and disadvantages and using them both would provide a better &lt;em&gt;signature coverage&lt;/em&gt;. Unfortunately, there is no good way to diff/merge RBS files yet; you have to that manually. Another manual work is to replace &lt;code&gt;untyped&lt;/code&gt; with the actual typing information.&lt;/p&gt;

&lt;p&gt;But wait to make your hands dirty. There is one more player in this game–&lt;a href="https://github.com/ruby/typeprof"&gt;Type Profiler&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Type Profiler infers a program type signatures dynamically during the execution. It &lt;em&gt;spies&lt;/em&gt; all the loaded classes and methods and collects the information about which types have been used as inputs and outputs, analyzes this data, and produces RBS definitions. Under the hood, it uses a custom Ruby interpreter (so, the code is not actually executed). You can find more in the &lt;a href="https://github.com/ruby/typeprof/blob/master/doc/doc.md"&gt;official docs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The main difference between TypeProf and RBS is that we need to create a sample script to be used as a profiling entry-point.&lt;/p&gt;

&lt;p&gt;Let's write one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# sig/rubanok_type_profile.rb&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"rubanok"&lt;/span&gt;

&lt;span class="n"&gt;processor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Rubanok&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Processor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;map&lt;/span&gt; &lt;span class="ss"&gt;:q&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="ss"&gt;:|&lt;/span&gt;
    &lt;span class="n"&gt;raw&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="n"&gt;match&lt;/span&gt; &lt;span class="ss"&gt;:sort_by&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:sort&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;activate_on: :sort_by&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;having&lt;/span&gt; &lt;span class="s2"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"asc"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="n"&gt;raw&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="n"&gt;default&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;sort_by&lt;/span&gt;&lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="ss"&gt;sort: &lt;/span&gt;&lt;span class="s2"&gt;"asc"&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="n"&gt;raw&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="n"&gt;processor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;project&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="ss"&gt;q: &lt;/span&gt;&lt;span class="s2"&gt;"search"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;sort_by: &lt;/span&gt;&lt;span class="s2"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="n"&gt;processor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;([],&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;q: &lt;/span&gt;&lt;span class="s2"&gt;"search"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;sort_by: &lt;/span&gt;&lt;span class="s2"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, let's run &lt;code&gt;typeprof&lt;/code&gt; command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;typeprof &lt;span class="nt"&gt;-Ilib&lt;/span&gt; sig/rubanok_type_profile.rb &lt;span class="nt"&gt;--exclude-dir&lt;/span&gt; lib/rubanok/rails &lt;span class="nt"&gt;--exclude-dir&lt;/span&gt; lib/rubanok/rspec.rb

&lt;span class="c"&gt;# Classes&lt;/span&gt;
module Rubanok
  VERSION : String

  class Rule
    UNDEFINED : Object
    @method_name : String
    attr_reader fields : untyped
    attr_reader activate_on : Array[untyped]
    attr_reader activate_always : &lt;span class="nb"&gt;false
    &lt;/span&gt;attr_reader ignore_empty_values : untyped
    attr_reader filter_with : nil
    def initialize : &lt;span class="o"&gt;(&lt;/span&gt;untyped, ?activate_on: untyped, ?activate_always: &lt;span class="nb"&gt;false&lt;/span&gt;, ?ignore_empty_values: untyped, ?filter_with: nil&lt;span class="o"&gt;)&lt;/span&gt; -&amp;gt; nil
    def project : &lt;span class="o"&gt;(&lt;/span&gt;untyped&lt;span class="o"&gt;)&lt;/span&gt; -&amp;gt; untyped
    def applicable? : &lt;span class="o"&gt;(&lt;/span&gt;untyped&lt;span class="o"&gt;)&lt;/span&gt; -&amp;gt; bool
    def to_method_name : -&amp;gt; String
    private
    def build_method_name : -&amp;gt; String
    def fetch_value : &lt;span class="o"&gt;(&lt;/span&gt;untyped, untyped&lt;span class="o"&gt;)&lt;/span&gt; -&amp;gt; Object?
    def empty? : &lt;span class="o"&gt;(&lt;/span&gt;nil&lt;span class="o"&gt;)&lt;/span&gt; -&amp;gt; &lt;span class="nb"&gt;false
  &lt;/span&gt;end

  &lt;span class="c"&gt;# ...&lt;/span&gt;
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nice, now we have some types defined (though most of them are still untyped), we can see methods visibility and even instance variables (something we haven't seen before). The order of methods stayed the same as in the original file—that's good!&lt;/p&gt;

&lt;p&gt;Unfortunately, despite being a runtime analyzer, TypeProf has not so good metaprogramming support. For example, the methods defined using iteration won't be recognized:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# a.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;A&lt;/span&gt;
  &lt;span class="sx"&gt;%w[a b]&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with_index&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;define_method&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="nb"&gt;p&lt;/span&gt; &lt;span class="no"&gt;A&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;a&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="no"&gt;A&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;b&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;typeprof a.rb

&lt;span class="c"&gt;# Classes&lt;/span&gt;
class A
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(We can handle this with &lt;code&gt;rbs prototype runtime&lt;/code&gt; 😉)&lt;/p&gt;

&lt;p&gt;So, even if you have an executable that provides 100% coverage of your APIs but uses metaprogramming, using just TypeProf is not enough to build a complete types scaffold for your program.&lt;/p&gt;

&lt;p&gt;To sum up, all three different ways to generate initial signatures have their pros and cons, but combining their results could give a very good starting point in adding types to existing code. Hopefully, we'll be able to automate this in the future.&lt;/p&gt;

&lt;p&gt;In Rubanok's case, I did the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Generating initial signatures using &lt;code&gt;rbs prototype rb&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Ran &lt;code&gt;typeprof&lt;/code&gt; and used its output to add missing instance variables and update some signatures.&lt;/li&gt;
&lt;li&gt;Finally, ran &lt;code&gt;rbs prototype runtime&lt;/code&gt; for main classes.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;While I was writing this article, a PR with &lt;code&gt;attr_reader self.foo&lt;/code&gt; support &lt;a href="https://github.com/ruby/rbs/pull/505"&gt;has been merged&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The latter one helped to find a &lt;a href="https://github.com/ruby/rbs/issues/510"&gt;bug&lt;/a&gt; in the signature generated at the first step:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt; module Rubanok
&lt;span class="gd"&gt;-  attr_accessor ignore_empty_values: untyped
-  attr_accessor fail_when_no_matches: untyped
&lt;/span&gt;&lt;span class="gi"&gt;+  def self.fail_when_no_matches: () -&amp;gt; untyped
+  def self.fail_when_no_matches=: (untyped) -&amp;gt; untyped
+  def self.ignore_empty_values: () -&amp;gt; untyped
+  def self.ignore_empty_values=: (untyped) -&amp;gt; untyped
&lt;/span&gt; end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Introducing Steep
&lt;/h2&gt;

&lt;p&gt;So far, we've only discussed how to write and generate type signatures. That would be useless if we don't add a type checker to our dev stack.&lt;/p&gt;

&lt;p&gt;As of today, the only type checker supporting RBS is &lt;a href="https://github.com/soutaro/steep"&gt;Steep&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;steep init&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Let's add the &lt;code&gt;steep&lt;/code&gt; gem to our dependencies and generate a configuration file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;steep init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That would generate a default &lt;code&gt;Steepfile&lt;/code&gt; with some configuration. For Rubanok, I updated it like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Steepfile&lt;/span&gt;
&lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="ss"&gt;:lib&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="c1"&gt;# Load signatures from sig/ folder&lt;/span&gt;
  &lt;span class="n"&gt;signature&lt;/span&gt; &lt;span class="s2"&gt;"sig"&lt;/span&gt;
  &lt;span class="c1"&gt;# Check only files from lib/ folder&lt;/span&gt;
  &lt;span class="n"&gt;check&lt;/span&gt; &lt;span class="s2"&gt;"lib"&lt;/span&gt;

  &lt;span class="c1"&gt;# We don't want to type check Rails/RSpec related code&lt;/span&gt;
  &lt;span class="c1"&gt;# (because we don't have RBS files for it)&lt;/span&gt;
  &lt;span class="n"&gt;ignore&lt;/span&gt; &lt;span class="s2"&gt;"lib/rubanok/rails/*.rb"&lt;/span&gt;
  &lt;span class="n"&gt;ignore&lt;/span&gt; &lt;span class="s2"&gt;"lib/rubanok/railtie.rb"&lt;/span&gt;
  &lt;span class="n"&gt;ignore&lt;/span&gt; &lt;span class="s2"&gt;"lib/rubanok/rspec.rb"&lt;/span&gt;

  &lt;span class="c1"&gt;# We use Set standard library; its signatures&lt;/span&gt;
  &lt;span class="c1"&gt;# come with RBS, but we need to load them explicitly&lt;/span&gt;
  &lt;span class="n"&gt;library&lt;/span&gt; &lt;span class="s2"&gt;"set"&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;steep stats&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Before drowning in a sea of types, let's think of how we can measure our signatures' efficiency. We can use &lt;code&gt;steep stats&lt;/code&gt; to see how good (or bad?) our types coverage is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;bundle &lt;span class="nb"&gt;exec &lt;/span&gt;steep stats &lt;span class="nt"&gt;--log-level&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;fatal

Target,File,Status,Typed calls,Untyped calls,All calls,Typed %
lib,lib/rubanok/dsl/mapping.rb,success,7,2,11,63.64
lib,lib/rubanok/dsl/matching.rb,success,26,18,50,52.00
lib,lib/rubanok/processor.rb,success,34,8,49,69.39
lib,lib/rubanok/rule.rb,success,24,12,36,66.67
lib,lib/rubanok/version.rb,success,0,0,0,0
lib,lib/rubanok.rb,success,8,4,12,66.67
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command outputs surprisingly outputs CSV 😯. Let's add some Unix magic and make the output more readable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;bundle &lt;span class="nb"&gt;exec &lt;/span&gt;steep stats &lt;span class="nt"&gt;--log-level&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;fatal | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="nt"&gt;-F&lt;/span&gt;&lt;span class="s1"&gt;','&lt;/span&gt; &lt;span class="s1"&gt;'{ printf "%-28s %-9s %-12s %-14s %-10s\n", $2, $3, $4, $5, $7 }'&lt;/span&gt;
File                         Status    Typed calls  Untyped calls  Typed %   
lib/rubanok/dsl/mapping.rb   success   7            2              63.64     
lib/rubanok/dsl/matching.rb  success   26           18             52.00     
lib/rubanok/processor.rb     success   34           8              69.39     
lib/rubanok/rule.rb          success   24           12             66.67     
lib/rubanok/version.rb       success   0            0              0         
lib/rubanok.rb               success   8            4              66.67  
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ideally, we would like to have everything typed. So, I opened my &lt;code&gt;.rbs&lt;/code&gt; files and started replacing &lt;code&gt;untyped&lt;/code&gt; with the actual types one by one.&lt;/p&gt;

&lt;p&gt;It took me about a dozen minutes to get rid of untyped definitions (most of them). I'm not going to describe this process in detail; it was pretty straightforward except for the one thing I'd like to pay attention to.&lt;/p&gt;

&lt;p&gt;Let's recall what Rubanok is. It provides a DSL to define &lt;em&gt;data&lt;/em&gt; (usually, user input) transformers of a form &lt;code&gt;(input, params) -&amp;gt; input&lt;/code&gt;. A typical use case is to customize an Active Record relation depending on request parameters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PagySearchyProcess&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Rubanok&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Processor&lt;/span&gt;
  &lt;span class="n"&gt;map&lt;/span&gt; &lt;span class="ss"&gt;:page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:per_page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;activate_always: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="ss"&gt;page: &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;per_page: &lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
   &lt;span class="c1"&gt;# raw is a user input&lt;/span&gt;
   &lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;page&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;per&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;per_page&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="n"&gt;map&lt;/span&gt; &lt;span class="ss"&gt;:q&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="ss"&gt;:|&lt;/span&gt;
    &lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="no"&gt;PagySearchyProcessor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;q: &lt;/span&gt;&lt;span class="s2"&gt;"rbs"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="c1"&gt;#=&amp;gt; Post.search("rbs").page(1).per(20)&lt;/span&gt;

&lt;span class="no"&gt;PagySearchyProcessor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;q: &lt;/span&gt;&lt;span class="s2"&gt;"rbs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;page: &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="c1"&gt;#=&amp;gt; Post.search("rbs").page(2).per(20)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Thus, Rubanok deals with two external types: &lt;em&gt;input&lt;/em&gt; (which could be anything) and &lt;em&gt;params&lt;/em&gt; (which is a Hash with String or Symbol keys). Also, we have a notion of &lt;em&gt;field&lt;/em&gt; internally: a params key used to activate a particular transformation. A lot of Rubanok's methods use these three entities, and to avoid duplication, I decided to use the &lt;em&gt;type aliases&lt;/em&gt; feature of RBS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;Rubanok&lt;/span&gt;
  &lt;span class="c1"&gt;# Transformation parameters&lt;/span&gt;
  &lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Hash&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="no"&gt;Symbol&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="no"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;untyped&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Symbol&lt;/span&gt;
  &lt;span class="c1"&gt;# Transformation target (we assume that input and output types are the same)&lt;/span&gt;
  &lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;

  &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Processor&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;
                 &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fields_set&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;project&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;void&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Rule&lt;/span&gt;
    &lt;span class="nb"&gt;attr_reader&lt;/span&gt; &lt;span class="ss"&gt;fields: &lt;/span&gt;&lt;span class="no"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;project&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;applicable?&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;bool&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That allowed me to avoid duplication and indicate that they are not just Hashes, Strings, or whatever passing around, but &lt;em&gt;params&lt;/em&gt;, &lt;em&gt;fields&lt;/em&gt; and &lt;em&gt;inputs&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Now, let's check our signatures!&lt;/p&gt;

&lt;h2&gt;
  
  
  Fighting with signatures, or make &lt;code&gt;steep check&lt;/code&gt; happy
&lt;/h2&gt;

&lt;p&gt;It's very unlikely that we wrote 100% correct signatures right away. I got ~30 errors:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;bundle &lt;span class="nb"&gt;exec &lt;/span&gt;steep check &lt;span class="nt"&gt;--log-level&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;fatal

lib/rubanok/dsl/mapping.rb:24:8: MethodArityMismatch: &lt;span class="nv"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;map &lt;span class="o"&gt;(&lt;/span&gt;def map&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;fields, &lt;span class="k"&gt;**&lt;/span&gt;options, &amp;amp;block&lt;span class="o"&gt;))&lt;/span&gt;
lib/rubanok/dsl/mapping.rb:38:10: NoMethodError: &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;::Object &amp;amp; ::Rubanok::DSL::Mapping::ClassMethods&lt;span class="o"&gt;)&lt;/span&gt;, &lt;span class="nv"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;define_method &lt;span class="o"&gt;(&lt;/span&gt;define_method&lt;span class="o"&gt;(&lt;/span&gt;rule.to_method_name, &amp;amp;block&lt;span class="o"&gt;))&lt;/span&gt;
lib/rubanok/dsl/mapping.rb:40:10: NoMethodError: &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;::Object &amp;amp; ::Rubanok::DSL::Mapping::ClassMethods&lt;span class="o"&gt;)&lt;/span&gt;, &lt;span class="nv"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;add_rule &lt;span class="o"&gt;(&lt;/span&gt;add_rule rule&lt;span class="o"&gt;)&lt;/span&gt;
lib/rubanok/dsl/matching.rb:25:10: MethodArityMismatch: &lt;span class="nv"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;initialize &lt;span class="o"&gt;(&lt;/span&gt;def initialize&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;, fields, values &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;[]&lt;/span&gt;, &lt;span class="k"&gt;**&lt;/span&gt;options, &amp;amp;block&lt;span class="o"&gt;))&lt;/span&gt;
lib/rubanok/dsl/matching.rb:26:26: UnexpectedSplat: &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;**&lt;/span&gt;options&lt;span class="o"&gt;)&lt;/span&gt;
lib/rubanok/dsl/matching.rb:29:12: IncompatibleAssignment:  ... 
lib/rubanok/dsl/matching.rb:30:32: NoMethodError: &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;::Array[untyped], &lt;span class="nv"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;keys &lt;span class="o"&gt;(&lt;/span&gt;@values.keys&lt;span class="o"&gt;)&lt;/span&gt;
lib/rubanok/dsl/matching.rb:42:8: MethodArityMismatch: &lt;span class="nv"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;initialize &lt;span class="o"&gt;(&lt;/span&gt;def initialize&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;, &lt;span class="k"&gt;**&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
lib/rubanok/dsl/matching.rb:70:8: MethodArityMismatch: &lt;span class="nv"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;match &lt;span class="o"&gt;(&lt;/span&gt;def match&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;fields, &lt;span class="k"&gt;**&lt;/span&gt;options, &amp;amp;block&lt;span class="o"&gt;))&lt;/span&gt;
lib/rubanok/dsl/matching.rb:71:17: IncompatibleArguments: ...
lib/rubanok/dsl/matching.rb:73:10: BlockTypeMismatch: ...
lib/rubanok/dsl/matching.rb:75:10: NoMethodError: &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;::Object &amp;amp; ::Rubanok::DSL::Matching::ClassMethods&lt;span class="o"&gt;)&lt;/span&gt;, &lt;span class="nv"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;define_method &lt;span class="o"&gt;(&lt;/span&gt;define_method&lt;span class="o"&gt;(&lt;/span&gt;rule.to_method_name&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; |params &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;{}&lt;/span&gt;|&lt;span class="o"&gt;)&lt;/span&gt;
lib/rubanok/dsl/matching.rb:83:12: NoMethodError: &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;::Object &amp;amp; ::Rubanok::DSL::Matching::ClassMethods&lt;span class="o"&gt;)&lt;/span&gt;, &lt;span class="nv"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;define_method &lt;span class="o"&gt;(&lt;/span&gt;define_method&lt;span class="o"&gt;(&lt;/span&gt;clause.to_method_name, &amp;amp;clause.block&lt;span class="o"&gt;))&lt;/span&gt;
lib/rubanok/dsl/matching.rb:86:10: NoMethodError: &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;::Object &amp;amp; ::Rubanok::DSL::Matching::ClassMethods&lt;span class="o"&gt;)&lt;/span&gt;, &lt;span class="nv"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;add_rule &lt;span class="o"&gt;(&lt;/span&gt;add_rule rule&lt;span class="o"&gt;)&lt;/span&gt;
lib/rubanok/dsl/matching.rb:96:15: NoMethodError: &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;::Object &amp;amp; ::Rubanok::DSL::Matching&lt;span class="o"&gt;)&lt;/span&gt;, &lt;span class="nv"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;raw &lt;span class="o"&gt;(&lt;/span&gt;raw&lt;span class="o"&gt;)&lt;/span&gt;
lib/rubanok/processor.rb:36:6: MethodArityMismatch: &lt;span class="nv"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;call &lt;span class="o"&gt;(&lt;/span&gt;def call&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;args&lt;span class="o"&gt;))&lt;/span&gt;
lib/rubanok/processor.rb:56:13: NoMethodError: &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;::Class | nil&lt;span class="o"&gt;)&lt;/span&gt;, &lt;span class="nv"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;superclass &amp;lt;&lt;span class="o"&gt;=&lt;/span&gt; Processor&lt;span class="o"&gt;)&lt;/span&gt;
lib/rubanok/processor.rb:57:12: NoMethodError: &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;::Class | nil&lt;span class="o"&gt;)&lt;/span&gt;, &lt;span class="nv"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;rules &lt;span class="o"&gt;(&lt;/span&gt;superclass.rules&lt;span class="o"&gt;)&lt;/span&gt;
lib/rubanok/processor.rb:67:13: NoMethodError: &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;::Class | nil&lt;span class="o"&gt;)&lt;/span&gt;, &lt;span class="nv"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;superclass &amp;lt;&lt;span class="o"&gt;=&lt;/span&gt; Processor&lt;span class="o"&gt;)&lt;/span&gt;
lib/rubanok/processor.rb:68:12: NoMethodError: &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;::Class | nil&lt;span class="o"&gt;)&lt;/span&gt;, &lt;span class="nv"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;fields_set &lt;span class="o"&gt;(&lt;/span&gt;superclass.fields_set&lt;span class="o"&gt;)&lt;/span&gt;
lib/rubanok/processor.rb:78:21: ArgumentTypeMismatch: &lt;span class="nv"&gt;receiver&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;::Hash[::Symbol, untyped], &lt;span class="nv"&gt;expected&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;::Array[::Symbol], &lt;span class="nv"&gt;actual&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;::Set[::Rubanok::field] &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;fields_set&lt;span class="o"&gt;)&lt;/span&gt;
lib/rubanok/processor.rb:116:6: NoMethodError: &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;::Rubanok::Processor, &lt;span class="nv"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;self.input &lt;span class="o"&gt;=)&lt;/span&gt;
lib/rubanok/processor.rb:134:6: NoMethodError: &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;::Rubanok::Processor, &lt;span class="nv"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;self.input &lt;span class="o"&gt;=&lt;/span&gt; prepared_input&lt;span class="o"&gt;)&lt;/span&gt;
lib/rubanok/rule.rb:11:6: IncompatibleAssignment: ...
lib/rubanok/rule.rb:20:8: UnexpectedJumpValue &lt;span class="o"&gt;(&lt;/span&gt;next acc&lt;span class="o"&gt;)&lt;/span&gt;
lib/rubanok/rule.rb:48:12: NoMethodError: &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;::Method | nil&lt;span class="o"&gt;)&lt;/span&gt;, &lt;span class="nv"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;call &lt;span class="o"&gt;(&lt;/span&gt;filter_with.call&lt;span class="o"&gt;(&lt;/span&gt;val&lt;span class="o"&gt;))&lt;/span&gt;
lib/rubanok/rule.rb:57:8: MethodArityMismatch: &lt;span class="nv"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;empty? &lt;span class="o"&gt;(&lt;/span&gt;def empty?&lt;span class="o"&gt;)&lt;/span&gt;
lib/rubanok/rule.rb:63:8: MethodArityMismatch: &lt;span class="nv"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;empty? &lt;span class="o"&gt;(&lt;/span&gt;def empty?&lt;span class="o"&gt;)&lt;/span&gt;
lib/rubanok/rule.rb:69:4: MethodArityMismatch: &lt;span class="nv"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;empty? &lt;span class="o"&gt;(&lt;/span&gt;def empty?&lt;span class="o"&gt;(&lt;/span&gt;val&lt;span class="o"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's take a closer look at these errors and try to fix them.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Refinements always break things.
&lt;/h3&gt;

&lt;p&gt;Let's start with the last three reported errors:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;lib/rubanok/rule.rb:57:8: MethodArityMismatch: &lt;span class="nv"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;empty? &lt;span class="o"&gt;(&lt;/span&gt;def empty?&lt;span class="o"&gt;)&lt;/span&gt;
lib/rubanok/rule.rb:63:8: MethodArityMismatch: &lt;span class="nv"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;empty? &lt;span class="o"&gt;(&lt;/span&gt;def empty?&lt;span class="o"&gt;)&lt;/span&gt;
lib/rubanok/rule.rb:69:4: MethodArityMismatch: &lt;span class="nv"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;empty? &lt;span class="o"&gt;(&lt;/span&gt;def empty?&lt;span class="o"&gt;(&lt;/span&gt;val&lt;span class="o"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why Steep detected three &lt;code&gt;#empty?&lt;/code&gt; methods in the Rule class? It turned out that it considers an &lt;em&gt;anonymous&lt;/em&gt; refinement body to be a part of the class body:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;using&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;refine&lt;/span&gt; &lt;span class="no"&gt;NilClass&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;empty?&lt;/span&gt;
      &lt;span class="kp"&gt;true&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="n"&gt;refine&lt;/span&gt; &lt;span class="no"&gt;Object&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;empty?&lt;/span&gt;
      &lt;span class="kp"&gt;false&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;empty?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kp"&gt;false&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="n"&gt;ignore_empty_values&lt;/span&gt;

  &lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;empty?&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I submitted an &lt;a href="https://github.com/soutaro/steep/issues/265"&gt;issue&lt;/a&gt; and moved refinements to the top of the file to fix the errors.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Superclass don't cry 😢
&lt;/h3&gt;

&lt;p&gt;Another interesting issue relates to &lt;code&gt;superclass&lt;/code&gt; usage:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;lib/rubanok/processor.rb:56:13: NoMethodError: &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;::Class | nil&lt;span class="o"&gt;)&lt;/span&gt;, &lt;span class="nv"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;superclass &amp;lt;&lt;span class="o"&gt;=&lt;/span&gt; Processor&lt;span class="o"&gt;)&lt;/span&gt;
lib/rubanok/processor.rb:57:12: NoMethodError: &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;::Class | nil&lt;span class="o"&gt;)&lt;/span&gt;, &lt;span class="nv"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;rules &lt;span class="o"&gt;(&lt;/span&gt;superclass.rules&lt;span class="o"&gt;)&lt;/span&gt;
lib/rubanok/processor.rb:67:13: NoMethodError: &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;::Class | nil&lt;span class="o"&gt;)&lt;/span&gt;, &lt;span class="nv"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;superclass &amp;lt;&lt;span class="o"&gt;=&lt;/span&gt; Processor&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The corresponding source code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="vi"&gt;@rules&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;superclass&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="no"&gt;Processor&lt;/span&gt;
    &lt;span class="n"&gt;superclass&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dup&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt;
    &lt;span class="p"&gt;[]&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's a very common pattern to inherit class &lt;em&gt;properties&lt;/em&gt;. Why doesn't it work? First, the &lt;code&gt;superclass&lt;/code&gt; signature says the result is either Class or &lt;code&gt;nil&lt;/code&gt; (though it could be nil only for the BaseObject class, as far as I know). Thus, we cannot use &lt;code&gt;&amp;lt;=&lt;/code&gt; right away (because it's not defined on &lt;code&gt;NilClass&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Even if we &lt;em&gt;unwrap&lt;/em&gt; &lt;code&gt;superclass&lt;/code&gt;, the problem with &lt;code&gt;.rules&lt;/code&gt; would still be there—Steep's flow sensitivity analysis currently doesn't recognize the &lt;code&gt;&amp;lt;=&lt;/code&gt; operator. So, I decided to hack the system and explicitly define the &lt;code&gt;.superclass&lt;/code&gt; signature for the Processor class:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# processor.rbs&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Processor&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;superclass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;singleton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Processor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This way, my code stays the same; only the types suffer 😈.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Explicit over implicit: handling splats.
&lt;/h3&gt;

&lt;p&gt;So far, we've seen pretty much the same problems as I had with &lt;a href="https://evilmartians.com/chronicles/sorbetting-a-gem"&gt;Sorbet&lt;/a&gt;. Let's take a look at something new.&lt;/p&gt;

&lt;p&gt;Consider this code snippet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;project&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transform_keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="ss"&gt;:to_sym&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="c1"&gt;# params is a Hash, fields_set is a Set&lt;/span&gt;
  &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;fields_set&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It produces the following type error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;lib/rubanok/processor.rb:78:21: ArgumentTypeMismatch: &lt;span class="nv"&gt;receiver&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;::Hash[::Symbol, untyped], &lt;span class="nv"&gt;expected&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;::Array[::Symbol], &lt;span class="nv"&gt;actual&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;::Set[::Rubanok::field]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;Hash#slice&lt;/code&gt; method expects an Array, but we pass a Set. However, we also use a splat (&lt;code&gt;*&lt;/code&gt;) operator, which implicitly tries to convert an object to an array—seems legit, right? Unfortunately, Steep is not so smart yet: we have to add an explicit &lt;code&gt;#to_a&lt;/code&gt; call.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Explicit over implicit, pt. 2: forwarding arguments.
&lt;/h3&gt;

&lt;p&gt;I used the following pattern in a few places:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;rule&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A DSL method accepts some options as keyword arguments and then pass them to the Rule class initializer. The possible options are strictly defined and enforced in the &lt;code&gt;Rule#initialize,&lt;/code&gt; but we would like to avoid declaring them explicitly just to forward down. Unfortunately, that's only possible if we declare &lt;code&gt;**options&lt;/code&gt; as &lt;code&gt;untyped&lt;/code&gt;—that would make signatures kinda useless.&lt;/p&gt;

&lt;p&gt;So, we have to become more explicit once again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;-        def map(*fields, **options, &amp;amp;block)
-          filter = options[:filter_with]
-          rule = Rule.new(fields, **options)
&lt;/span&gt;
+        def map(*fields, activate_on: fields, activate_always: false, ignore_empty_values: Rubanok.ignore_empty_values, filter_with: nil, &amp;amp;block)
&lt;span class="gi"&gt;+          filter = filter_with
+          rule = Rule.new(fields, activate_on: activate_on, activate_always: activate_always, ignore_empty_values: ignore_empty_values, filter_with: filter_with)
&lt;/span&gt;
# and more...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I guess it's time to add &lt;a href="https://github.com/ruby-next/ruby-next#proposed-and-edge-features"&gt;Ruby Next&lt;/a&gt; and use shorthand Hash notation 🙂&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Variadic arguments: annotations to the rescue!
&lt;/h3&gt;

&lt;p&gt;In the recent Rubanok release, I added an ability to skip input for transformations and only use params as the only &lt;code&gt;#call&lt;/code&gt; method argument. That led to the following code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;size&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kp"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt;
    &lt;span class="n"&gt;args&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As in the previous case, we needed to make our signature more explicit and specify the actual arguments instead of the &lt;code&gt;*args&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# This is our signature&lt;/span&gt;
&lt;span class="c1"&gt;# (Note that we can define multiple signatures for a method)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;
             &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;

&lt;span class="c1"&gt;# And this is our code (first attempt)&lt;/span&gt;
&lt;span class="no"&gt;UNDEFINED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;UNDEFINED&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;UNDEFINED&lt;/span&gt;

  &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="no"&gt;ArgumentError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Params could not be nil"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;nil?&lt;/span&gt;

  &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This refactoring doesn't pass the type check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;bundle &lt;span class="nb"&gt;exec &lt;/span&gt;steep lib/rubanok/processor.rb

lib/rubanok/processor.rb:43:24: ArgumentTypeMismatch: &lt;span class="nv"&gt;receiver&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;::Rubanok::Processor, &lt;span class="nv"&gt;expected&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;::Rubanok::params, &lt;span class="nv"&gt;actual&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;::Rubanok::input | ::Rubanok::params | ::Object&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;params&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So, according to Steep, &lt;code&gt;param&lt;/code&gt; could be pretty match anything :( We need to help Steep to make the right decision. I couldn't find a way to do that via RBS, so my last resort was to use &lt;a href="https://github.com/soutaro/steep/blob/master/manual/annotations.md"&gt;annotations&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Yes, even though RBS itself is designed not to pollute your source code, Steep allows you to do that. And in some cases, that's the necessary evil.&lt;/p&gt;

&lt;p&gt;I came up with the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;UNDEFINED&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;UNDEFINED&lt;/span&gt;

  &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="no"&gt;ArgumentError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Params could not be nil"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;nil?&lt;/span&gt;

  &lt;span class="c1"&gt;# @type var params: untyped&lt;/span&gt;
  &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We declare &lt;code&gt;params&lt;/code&gt; as &lt;code&gt;untyped&lt;/code&gt; to silence the error. The &lt;code&gt;#call&lt;/code&gt; method signature guarantees that the &lt;code&gt;params&lt;/code&gt; variable satisfies the &lt;code&gt;params&lt;/code&gt; type requirements, so we should be safe here.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Deal with metaprogramming: interfaces.
&lt;/h3&gt;

&lt;p&gt;Since Rubanok provides a DSL, it heavily uses metaprogramming.&lt;br&gt;
For example, we use &lt;code&gt;#define_method&lt;/code&gt; to dynamically generate transformation methods:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;activate_on: &lt;/span&gt;&lt;span class="n"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;activate_always: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;ignore_empty_values: &lt;/span&gt;&lt;span class="no"&gt;Rubanok&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ignore_empty_values&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;filter_with: &lt;/span&gt;&lt;span class="kp"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;
  &lt;span class="n"&gt;rule&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;activate_on: &lt;/span&gt;&lt;span class="n"&gt;activate_on&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;activate_always: &lt;/span&gt;&lt;span class="n"&gt;activate_always&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;ignore_empty_values: &lt;/span&gt;&lt;span class="n"&gt;ignore_empty_values&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;filter_with: &lt;/span&gt;&lt;span class="n"&gt;filter_with&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="n"&gt;define_method&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_method_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="n"&gt;add_rule&lt;/span&gt; &lt;span class="n"&gt;rule&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And that's the error we see when running &lt;code&gt;steep check&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;lib/rubanok/dsl/mapping.rb:38:10: NoMethodError: &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;::Object &amp;amp; ::Rubanok::DSL::Mapping::ClassMethods&lt;span class="o"&gt;)&lt;/span&gt;, &lt;span class="nv"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;define_method &lt;span class="o"&gt;(&lt;/span&gt;define_method&lt;span class="o"&gt;(&lt;/span&gt;rule.to_method_name, &amp;amp;block&lt;span class="o"&gt;))&lt;/span&gt;
lib/rubanok/dsl/mapping.rb:40:10: NoMethodError: &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;::Object &amp;amp; ::Module &amp;amp; ::Rubanok::DSL::Mapping::ClassMethods&lt;span class="o"&gt;)&lt;/span&gt;, &lt;span class="nv"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;add_rule &lt;span class="o"&gt;(&lt;/span&gt;add_rule rule&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hmm, looks like our type checker doesn't know that we're calling the &lt;code&gt;.map&lt;/code&gt; method in the context of the Processor class (we call &lt;code&gt;Processor.extend DSL::Mapping&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;RBS has a concept of a &lt;em&gt;self type&lt;/em&gt; for module: a self type adds requirements to the classes/modules, which include/prepend/extend this module. For example, we can state that we only allow using &lt;code&gt;Mapping::ClassMethods&lt;/code&gt; to extend modules (and not objects, for example):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Module here is a self type&lt;/span&gt;
&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;ClassMethods&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;Module&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That fixes &lt;code&gt;NoMethodError&lt;/code&gt; for &lt;code&gt;#define_method,&lt;/code&gt; but we still have it for &lt;code&gt;#add_rule&lt;/code&gt;—this is a Processor self method. How can we add this restriction using module self types? It's not allowed to use &lt;code&gt;singleton(SomeClass)&lt;/code&gt; as a self type; only classes and &lt;em&gt;interfaces&lt;/em&gt; are allowed. Yes, RBS has interfaces! Let's give them a try!&lt;/p&gt;

&lt;p&gt;We only use the &lt;code&gt;#add_rule&lt;/code&gt; method in the modules, so we can define an interface as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;interface&lt;/span&gt; &lt;span class="n"&gt;_RulesAdding&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;add_rule&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Rule&lt;/span&gt; &lt;span class="n"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;void&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;# Then we can use this interface in the Processor class itself&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Processor&lt;/span&gt;
  &lt;span class="kp"&gt;extend&lt;/span&gt; &lt;span class="n"&gt;_RulesAdding&lt;/span&gt;

  &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;# And in our modules&lt;/span&gt;
&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;Mapping&lt;/span&gt;
  &lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;ClassMethods&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;Module&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_RulesAdding&lt;/span&gt;
    &lt;span class="c1"&gt;# ...&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  7. Making Steep happy.
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;Other problems I faced with Steep which I converted into issues:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/soutaro/steep/issues/266"&gt;Method visibility false negatives&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/soutaro/steep/issues/261"&gt;Inability to declare block return values in some cases&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;I added a few more changes to the signatures and the source code to finally make a &lt;code&gt;steep check&lt;/code&gt; pass. The journey was a bit longer than I expected, but in the end, I'm pretty happy with the result—I will continue using RBS and Steep.&lt;/p&gt;

&lt;p&gt;Here is the final stats for Rubanok:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;File                         Status    Typed calls  Untyped calls  Typed %   
lib/rubanok/dsl/mapping.rb   success   11           0              100.00    
lib/rubanok/dsl/matching.rb  success   54           2              94.74     
lib/rubanok/processor.rb     success   52           2              96.30     
lib/rubanok/rule.rb          success   31           2              93.94     
lib/rubanok/version.rb       success   0            0              0         
lib/rubanok.rb               success   12           0              100.00  
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Runtime type checking with RBS
&lt;/h2&gt;

&lt;p&gt;Although RBS doesn't provide static type checking capabilities, it comes with &lt;a href="https://github.com/ruby/rbs/blob/master/docs/sigs.md#setting-up-the-test"&gt;runtime testing utils&lt;/a&gt;. By loading a specific file (&lt;code&gt;rbs/test/setup&lt;/code&gt;), you can ask RBS to watch the execution and check that method calls inputs and outputs satisfy signatures.&lt;/p&gt;

&lt;p&gt;Under the hood, &lt;a href="https://github.com/ruby/rbs/blob/71b8e534e0adcc78aa76ffbd0326ffe01594d520/lib/rbs/test/setup.rb#L62-L73"&gt;TracePoint API is used&lt;/a&gt; along with the &lt;a href="https://github.com/ruby/rbs/blob/71b8e534e0adcc78aa76ffbd0326ffe01594d520/lib/rbs/test/hook.rb#L50"&gt;alias method chain trick&lt;/a&gt; to hijack observed methods. Thus, it's meant for use in tests, not in production.&lt;/p&gt;

&lt;p&gt;Let's try to run our RSpec tests with runtime checking enabled:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ RBS_TEST_TARGET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'Rubanok::*'&lt;/span&gt; &lt;span class="nv"&gt;RUBYOPT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'-rrbs/test/setup'&lt;/span&gt; bundle &lt;span class="nb"&gt;exec &lt;/span&gt;rspec &lt;span class="nt"&gt;--fail-fast&lt;/span&gt;

I, &lt;span class="o"&gt;[&lt;/span&gt;2020-12-07T21:07:57.221200 &lt;span class="c"&gt;#285]  INFO -- : Setting up hooks for ::Rubanok&lt;/span&gt;
I, &lt;span class="o"&gt;[&lt;/span&gt;2020-12-07T21:07:57.221302 &lt;span class="c"&gt;#285]  INFO -- rbs: Installing runtime type checker in Rubanok...&lt;/span&gt;
...

Failures:

  1&lt;span class="o"&gt;)&lt;/span&gt; Rails controllers integration PostsApiController#planish implicit rubanok with matching
     Failure/Error: prepare! unless prepared?

     RBS::Test::Tester::TypeError:
       TypeError: &lt;span class="o"&gt;[&lt;/span&gt;Rubanok::Processor#prepared?] ReturnTypeError: expected &lt;span class="sb"&gt;`&lt;/span&gt;bool&lt;span class="sb"&gt;`&lt;/span&gt; but returns &lt;span class="sb"&gt;`&lt;/span&gt;nil&lt;span class="sb"&gt;`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Oh, we forgot to initialize the &lt;code&gt;@prepared&lt;/code&gt; instance variable with the boolean value! Nice!&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;When I tried to use RBS runtime tests for the first time, I encountered &lt;a href="https://github.com/ruby/rbs/issues?q=is%3Aissue+author%3Apalkan+is%3Aclosed"&gt;a few severe problems&lt;/a&gt;. Many thanks to &lt;a href="https://github.com/soutaro"&gt;Soutaro Matsumoto&lt;/a&gt; for fixing all of them faster than I finished working on this article!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I found a couple of more issues by using &lt;code&gt;rbs/test/setup&lt;/code&gt;, including the one I wasn't able to resolve:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Failure/Error: super&lt;span class="o"&gt;(&lt;/span&gt;fields, activate_on: activate_on, activate_always: activate_always&lt;span class="o"&gt;)&lt;/span&gt;

RBS::Test::Tester::TypeError:
  TypeError: &lt;span class="o"&gt;[&lt;/span&gt;Rubanok::Rule#initialize] UnexpectedBlockError: unexpected block is given &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;::Array[::Rubanok::field] fields, ?filter_with: ::Method?, ?ignore_empty_values: bool, ?activate_always: bool, ?activate_on: ::Rubanok::field | ::Array[::Rubanok::field]&lt;span class="o"&gt;)&lt;/span&gt; -&amp;gt; void&lt;span class="sb"&gt;`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here is the reason:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Clause&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Rubanok&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Rule&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# The block is passed to super implicitly,&lt;/span&gt;
    &lt;span class="c1"&gt;# but is not acceptable by Rule#initialize&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I tried to use &lt;code&gt;&amp;amp;nil&lt;/code&gt; to disable block propagation, but that broke &lt;code&gt;steep check&lt;/code&gt; 😞. I submitted &lt;a href="https://github.com/soutaro/steep/issues/268"&gt;an issue&lt;/a&gt; and excluded &lt;code&gt;Rule#initialize&lt;/code&gt; from the runtime checking for now using a special comment in the &lt;code&gt;.rbs&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# rule.rbs&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Rule&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;
  &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;rbs&lt;/span&gt;&lt;span class="ss"&gt;:test:skip&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="no"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="ss"&gt;activate_on: &lt;/span&gt;&lt;span class="n"&gt;field&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="no"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="ss"&gt;activate_always: &lt;/span&gt;&lt;span class="n"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="ss"&gt;ignore_empty_values: &lt;/span&gt;&lt;span class="n"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="ss"&gt;filter_with: &lt;/span&gt;&lt;span class="no"&gt;Method&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;void&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Bonus: Steep meets Rake
&lt;/h2&gt;

&lt;p&gt;I usually run &lt;code&gt;be rake&lt;/code&gt; pretty often during development to make sure that everything is correct. The default task usually includes RuboCop and tests.&lt;/p&gt;

&lt;p&gt;Let's add Steep to the party:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Rakefile&lt;/span&gt;

&lt;span class="c1"&gt;# other tasks&lt;/span&gt;

&lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="ss"&gt;:steep&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="c1"&gt;# Steep doesn't provide Rake integration yet,&lt;/span&gt;
  &lt;span class="c1"&gt;# but can do that ourselves &lt;/span&gt;
  &lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"steep"&lt;/span&gt;
  &lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"steep/cli"&lt;/span&gt;

  &lt;span class="no"&gt;Steep&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;CLI&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;argv: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"check"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="ss"&gt;stdout: &lt;/span&gt;&lt;span class="vg"&gt;$stdout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;stderr: &lt;/span&gt;&lt;span class="vg"&gt;$stderr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;stdin: &lt;/span&gt;&lt;span class="vg"&gt;$stdin&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="n"&gt;namespace&lt;/span&gt; &lt;span class="ss"&gt;:steep&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="c1"&gt;# Let's add a user-friendly shortcut&lt;/span&gt;
  &lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="ss"&gt;:stats&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="sx"&gt;%q(bundle exec steep stats --log-level=fatal | awk -F',' '{ printf "%-28s %-9s %-12s %-14s %-10s\n", $2, $3, $4, $5, $7 }')&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;# Run steep before everything else to fail-fast&lt;/span&gt;
&lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="ss"&gt;default: &lt;/span&gt;&lt;span class="sx"&gt;%w[steep rubocop rubocop:md spec]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Bonus 2: Type Checking meets GitHub Actions
&lt;/h2&gt;

&lt;p&gt;As the final step, I configure GitHub Actions to run both static and runtime type checks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# lint.yml&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;steep&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v2&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ruby/setup-ruby@v1&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;ruby-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2.7&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run Steep check&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;gem install steep&lt;/span&gt;
        &lt;span class="s"&gt;steep check&lt;/span&gt;

&lt;span class="c1"&gt;# rspec.yml&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;rspec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# ...&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="c1"&gt;# ...&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run RSpec with RBS&lt;/span&gt;
      &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;matrix.ruby == '2.7'&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;gem install rbs&lt;/span&gt;
        &lt;span class="s"&gt;RBS_TEST_TARGET="Rubanok::*" RUBYOPT="-rrbs/test/setup" bundle exec rspec --force-color&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run RSpec without RBS&lt;/span&gt;
      &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;matrix.ruby != '2.7'&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;bundle exec rspec --force-color&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;Although there are still enough rough edges, I enjoyed using RBS/Steep a bit more than "eating" Sorbet (mostly because I'm not a big fan of type annotations in the source code). I will continue adopting Ruby 3 types in my OSS projects and reporting as many issues to RBS/Steep as possible 🙂.&lt;/p&gt;

&lt;p&gt;P.S. You can find the source code in &lt;a href="https://github.com/palkan/rubanok/pull/15/files"&gt;this PR&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>types</category>
      <category>steep</category>
    </item>
    <item>
      <title>Reusable Docker development environment</title>
      <dc:creator>Vladimir Dementyev</dc:creator>
      <pubDate>Thu, 19 Nov 2020 08:38:27 +0000</pubDate>
      <link>https://dev.to/palkan_tula/reusable-docker-development-environment-81g</link>
      <guid>https://dev.to/palkan_tula/reusable-docker-development-environment-81g</guid>
      <description>&lt;p&gt;So far, I've been only talking about Docker for development in the context of web applications, i.e., something involving multiple services and specific system dependencies. Today, I'd like to look at the other side and discuss how containerization could help me work on libraries.&lt;/p&gt;

&lt;h2&gt;
  
  
  Once upon a time...
&lt;/h2&gt;

&lt;p&gt;A recent discussion in our corporate (&lt;a href="https://evilmartians.com" rel="noopener noreferrer"&gt;Evil Martians&lt;/a&gt;) Slack triggered me to write this post finally: my colleagues discussed a new tool to manage multiple Node.js versions on the same machine (like nvm but &lt;a href="https://docs.volta.sh" rel="noopener noreferrer"&gt;written in Rust&lt;/a&gt;—you know, things get cooler when rewritten in Rust 😉).&lt;/p&gt;

&lt;p&gt;My first thought was: "Why on Earth in 2020, we still need all these version managers, such as rbenv, nvm, asdf, whatever?" I almost forgot the &lt;em&gt;pleasure&lt;/em&gt; of using them: my computer breathes easy without all the &lt;em&gt;environmental&lt;/em&gt; pollution these tools bring.&lt;/p&gt;

&lt;p&gt;Let's do a twist and talk about my &lt;em&gt;monolithic personal computer&lt;/em&gt; (or laptop).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Flk353f02665frbdkqae0.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Flk353f02665frbdkqae0.gif" alt="Mono vs. Compo"&gt;&lt;/a&gt;&lt;/p&gt;
Slides from my &lt;a href="https://noti.st/palkan/VWPOSd/between-monoliths-and-microservices" rel="noopener noreferrer"&gt;Between monoliths and microservices&lt;/a&gt; RailsConf talk



&lt;p&gt;About a year ago, I switched to a new laptop. Since I'm not a big fan of backups and other &lt;em&gt;time machines&lt;/em&gt;, I had to craft a comfortable working environment from scratch. Instead of installing all the runtimes I usually use (Ruby, Golang, Erlang, Node), I decided to experiment and go with Docker for everything: applications and libraries, commercial and open-source projects. In other words, I only installed Git, Docker, and &lt;a href="https://github.com/bibendi/dip" rel="noopener noreferrer"&gt;Dip&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I decided to use Docker for everything: applications and libraries, commercial and open-source projects.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Phase 0: Universal &lt;code&gt;docker-compose.yml&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;You may think that keeping a Docker4Dev configuration (like the one described in the &lt;a href="https://evilmartians.com/chronicles/ruby-on-whales-docker-for-ruby-rails-development" rel="noopener noreferrer"&gt;Ruby on Whales&lt;/a&gt; post) for every tiny library is a bunch of overhead. Yep, that's true. So, I started with a shared &lt;code&gt;docker-compose.yml&lt;/code&gt; configuration, containing a service per project and sharing volumes between them (thus, I didn't have to install all the dependencies multiple times).&lt;/p&gt;

&lt;p&gt;Launching a container looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker-compose &lt;span class="nt"&gt;-f&lt;/span&gt; ~/dev/docker-compose.yml &lt;span class="se"&gt;\&lt;/span&gt;
  run &lt;span class="nt"&gt;--rm&lt;/span&gt; some_service

&lt;span class="c"&gt;# we can omit -f if our project is somewhere inside the ~/dev folder: docker-compose tries to find the first docker-compose.yml up the tree&lt;/span&gt;
docker-compose run &lt;span class="nt"&gt;--rm&lt;/span&gt; some_service

&lt;span class="c"&gt;# finally, using an alias&lt;/span&gt;
dcr some_service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not so bad. The only problem is that I had to define a service every time I wanted to run a new project within Docker. I continued investigating and came up with the &lt;em&gt;Reusable Docker Environment (RDE)&lt;/em&gt; concept.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 1 (discarded): RDE
&lt;/h3&gt;

&lt;p&gt;The idea of RDE was to completely eliminate the need for &lt;code&gt;Dockerfile&lt;/code&gt; and &lt;code&gt;docker-compose.yml&lt;/code&gt; files and generate them on-the-fly using predefined templates (or &lt;em&gt;executors&lt;/em&gt;).&lt;/p&gt;

&lt;p&gt;This is how I imagined it to work:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Open the current folder within a Ruby executor&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;rde &lt;span class="nt"&gt;-e&lt;/span&gt; ruby
root@1123:/app#

&lt;span class="c"&gt;# Execute a command within a JRuby executor&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;rde run &lt;span class="nt"&gt;-e&lt;/span&gt; jruby &lt;span class="nt"&gt;--&lt;/span&gt; ruby &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"puts RUBY_PLATFORM"&lt;/span&gt;
java
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This idea left in a &lt;a href="https://gist.github.com/palkan/a009c23915e2ea663cc0ba3fac69c7be" rel="noopener noreferrer"&gt;gist&lt;/a&gt; to gather dust.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 2 (current): Back to &lt;code&gt;docker-compose.yml&lt;/code&gt; with Dip
&lt;/h3&gt;

&lt;p&gt;It turned out that solving the duplication problem could be done without building a yet-another tool (even in Rust 😉). After discussing the RDE concept with &lt;a href="https://github.com/bibendi" rel="noopener noreferrer"&gt;Mikhail Merkushin&lt;/a&gt;, we realized that a similar functionality could be achieved with &lt;a href="https://github.com/bibendi/dip" rel="noopener noreferrer"&gt;Dip&lt;/a&gt; if we add a couple of features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Lookup configurations in parent directories (so, we can use a single &lt;code&gt;~/dip.yml&lt;/code&gt; for all projects).&lt;/li&gt;
&lt;li&gt;Provide an environment variable containing the relative path to the current directory from the configuration directory (so we can use it as a dynamic &lt;code&gt;working_dir&lt;/code&gt;). &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These features have been added in &lt;a href="https://github.com/bibendi/dip/releases/tag/v5.0.0" rel="noopener noreferrer"&gt;v5.0.0&lt;/a&gt; (thanks to Misha), and I started exploring the new possibilities.&lt;/p&gt;

&lt;p&gt;Let's skip all the intermediate states and finally take a look at the final configuration.&lt;/p&gt;

&lt;p&gt;Currently, my &lt;code&gt;~/dip.yml&lt;/code&gt; only contains different Rubies and databases:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;5.0'&lt;/span&gt;

&lt;span class="na"&gt;compose&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;files&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./.dip/docker-compose.yml&lt;/span&gt;
  &lt;span class="na"&gt;project_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;shared_dip_env&lt;/span&gt;

&lt;span class="na"&gt;interaction&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;ruby&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nl"&gt;&amp;amp;ruby&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Open Ruby service terminal&lt;/span&gt;
    &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ruby&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/bin/bash&lt;/span&gt;
  &lt;span class="na"&gt;jruby&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;*ruby&lt;/span&gt;
    &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;jruby&lt;/span&gt;
  &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ruby:latest'&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;*ruby&lt;/span&gt;
    &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ruby-latest&lt;/span&gt;
  &lt;span class="na"&gt;psql&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run psql console&lt;/span&gt;
    &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;psql -h postgres -U postgres&lt;/span&gt;
  &lt;span class="na"&gt;createdb&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run PostgreSQL createdb command&lt;/span&gt;
    &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;createdb -h postgres -U postgres&lt;/span&gt;
  &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;redis-cli'&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run Redis console&lt;/span&gt;
    &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis-cli -h redis&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Whenever I want to work on a Ruby gem, I just launched &lt;code&gt;dip ruby&lt;/code&gt; from the project's directory and run all the commands (e.g., &lt;code&gt;bundle install&lt;/code&gt;, &lt;code&gt;rake&lt;/code&gt;) within a container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;~ &lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ~/my_ruby_project
~/my_ruby_project &lt;span class="nv"&gt;$ &lt;/span&gt;dip ruby:latest

&lt;span class="o"&gt;[&lt;/span&gt;../my_ruby_project] ruby &lt;span class="nt"&gt;-v&lt;/span&gt;
ruby 3.0.0dev &lt;span class="o"&gt;(&lt;/span&gt;2020-10-20T12:46:54Z master 451836f582&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;x86_64-linux]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;See, I can run Ruby 3 without any hassle 🙂&lt;/p&gt;

&lt;p&gt;There is only one &lt;em&gt;special&lt;/em&gt; trick I have in the &lt;code&gt;docker-compose.yml&lt;/code&gt; which allows me to re-use the same container for all projects without manual volumes mounting—&lt;code&gt;PWD&lt;/code&gt;! Yes, all you need is &lt;code&gt;PWD&lt;/code&gt;, the absolute path to the current working directory on the host machine. Here is how I use this &lt;em&gt;sacred knowledge&lt;/em&gt; in my configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;2.4'&lt;/span&gt;

&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;ruby&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nl"&gt;&amp;amp;ruby&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bash&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ruby:2.7&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="c1"&gt;# That's all the magic!&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;${PWD}:/${PWD}:cached&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;bundler_data:/usr/local/bundle&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;history:/usr/local/hist&lt;/span&gt;
      &lt;span class="c1"&gt;# I also mount different configuration files&lt;/span&gt;
      &lt;span class="c1"&gt;# for better DX&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./.bashrc:/root/.bashrc:ro&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./.irbrc:/root/.irbrc:ro&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./.pryrc:/root/.pryrc:ro&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres://postgres:postgres@postgres:5432&lt;/span&gt;
      &lt;span class="na"&gt;REDIS_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis://redis:6379/&lt;/span&gt;
      &lt;span class="na"&gt;HISTFILE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/usr/local/hist/.bash_history&lt;/span&gt;
      &lt;span class="na"&gt;LANG&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;C.UTF-8&lt;/span&gt;
      &lt;span class="na"&gt;PROMPT_DIRTRIM&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
      &lt;span class="na"&gt;PS1&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;[\W]\!&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;
      &lt;span class="c1"&gt;# Plays nice with gemfiles/*.gemfile files for CI&lt;/span&gt;
      &lt;span class="na"&gt;BUNDLE_GEMFILE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${BUNDLE_GEMFILE:-Gemfile}&lt;/span&gt;
    &lt;span class="c1"&gt;# And that's the second part of the spell&lt;/span&gt;
    &lt;span class="na"&gt;working_dir&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${PWD}&lt;/span&gt;
    &lt;span class="na"&gt;tmpfs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/tmp&lt;/span&gt;
  &lt;span class="na"&gt;jruby&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;*ruby&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;jruby:latest&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;${PWD}:/${PWD}:cached&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;bundler_jruby:/usr/local/bundle&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;history:/usr/local/hist&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./.bashrc:/root/.bashrc:ro&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./.irbrc:/root/.irbrc:ro&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./.pryrc:/root/.pryrc:ro&lt;/span&gt;
  &lt;span class="na"&gt;ruby-latest&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;*ruby&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rubocophq/ruby-snapshot:latest&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;${PWD}:/${PWD}:cached&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;bundler_data_edge:/usr/local/bundle&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;history:/usr/local/hist&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./.bashrc:/root/.bashrc:ro&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./.irbrc:/root/.irbrc:ro&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./.pryrc:/root/.pryrc:ro&lt;/span&gt;
  &lt;span class="na"&gt;postgres&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:11.7&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;history:/usr/local/hist&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./.psqlrc:/root/.psqlrc:ro&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;postgres:/var/lib/postgresql/data&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;PSQL_HISTFILE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/usr/local/hist/.psql_history&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
      &lt;span class="na"&gt;PGPASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="m"&gt;5432&lt;/span&gt;
  &lt;span class="na"&gt;redis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis:5-alpine&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;redis:/data&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="m"&gt;6379&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis-cli ping&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;3s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;postgres&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;redis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;bundler_data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;bundler_jruby&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;bundler_data_edge&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;history&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Whenever I need PostgreSQL or Redis to build the library, I do the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Launch PostgreSQL in the background&lt;/span&gt;
dip up &lt;span class="nt"&gt;-d&lt;/span&gt; postgres
&lt;span class="c"&gt;# Create a database&lt;/span&gt;
dip createdb my_library_db
&lt;span class="c"&gt;# Run psql&lt;/span&gt;
dip psql
&lt;span class="c"&gt;# And, for example, run tests&lt;/span&gt;
dip ruby &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"bundle exec rspec"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Databases "live" within the same Docker network as other containers (since we're using the same &lt;code&gt;docker-compose.yml&lt;/code&gt;) and accessible via their names (&lt;code&gt;postgres&lt;/code&gt; and &lt;code&gt;redis&lt;/code&gt;). My code should only recognize the &lt;code&gt;DATABASE_URL&lt;/code&gt; and &lt;code&gt;REDIS_URL&lt;/code&gt;, respectively.&lt;/p&gt;

&lt;p&gt;Let's consider a few more examples.&lt;/p&gt;

&lt;h4&gt;
  
  
  Using with VS Code
&lt;/h4&gt;

&lt;p&gt;If you're a VC Code user and want to use the power of IntelliSense, you can combine this approach with &lt;a href="https://code.visualstudio.com/docs/remote/attach-container" rel="noopener noreferrer"&gt;Remote Containers&lt;/a&gt;: just run &lt;code&gt;dip up -d ruby&lt;/code&gt; and attach to a running container!&lt;/p&gt;

&lt;h4&gt;
  
  
  Node.js example: Docsify
&lt;/h4&gt;

&lt;p&gt;Let's take a look at beyond-Ruby example: running &lt;a href="https://docsify.js.org/" rel="noopener noreferrer"&gt;Docsify&lt;/a&gt; documentation servers.&lt;/p&gt;

&lt;p&gt;Docsify is a JavaScript / Node.js documentation site generator. I'm &lt;a href="https://evilmartians.com/chronicles/keeping-oss-documentation-in-check-with-docsify-lefthook-and-friends" rel="noopener noreferrer"&gt;using it&lt;/a&gt; for all my open-source projects. It requires Node.js and the &lt;code&gt;docsify-cli&lt;/code&gt; package to be installed. But we don't to install anything, remember? Let's pack it into Docker!&lt;/p&gt;

&lt;p&gt;First, we declare a base Node service in our &lt;code&gt;docker-compose.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;
  &lt;span class="na"&gt;node&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nl"&gt;&amp;amp;node&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;node:14&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;${PWD}:/${PWD}:cached&lt;/span&gt;
      &lt;span class="c1"&gt;# Where to store global packages&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;npm_data:${NPM_CONFIG_PREFIX}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;history:/usr/local/hist&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./.bashrc:/root/.bashrc:ro&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;NPM_CONFIG_PREFIX&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${NPM_CONFIG_PREFIX}&lt;/span&gt;
      &lt;span class="na"&gt;HISTFILE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/usr/local/hist/.bash_history&lt;/span&gt;
      &lt;span class="na"&gt;PROMPT_DIRTRIM&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
      &lt;span class="na"&gt;PS1&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;[\W]\!&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;
    &lt;span class="na"&gt;working_dir&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${PWD}&lt;/span&gt;
    &lt;span class="na"&gt;tmpfs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/tmp&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's &lt;a href="https://github.com/nodejs/docker-node/blob/master/docs/BestPractices.md#global-npm-dependencies" rel="noopener noreferrer"&gt;recommended&lt;/a&gt; to keep global dependencies in a non-root user directory. Also, we want to make sure we "cache" these packages by putting them into a volume.&lt;/p&gt;

&lt;p&gt;We can define the env var (&lt;code&gt;NPM_CONFIG_PREFIX&lt;/code&gt;) in the Dip config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# dip.yml&lt;/span&gt;
&lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;NPM_CONFIG_PREFIX&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/home/node/.npm-global&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since we want to run a Docsify server to access a documentation website, we need to expose ports. Let's define a separate service for that and also define a command to run a server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;
  &lt;span class="na"&gt;node&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nl"&gt;&amp;amp;node&lt;/span&gt;
    &lt;span class="c1"&gt;# ...&lt;/span&gt;

  &lt;span class="na"&gt;docsify&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;*node&lt;/span&gt;
    &lt;span class="na"&gt;working_dir&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${NPM_CONFIG_PREFIX}/bin&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docsify serve ${PWD}/docs -p 5000 --livereload-port &lt;/span&gt;&lt;span class="m"&gt;55729&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;5000:5000&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;55729:55729&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To install the &lt;code&gt;docsify-cli&lt;/code&gt; package globally, we should run the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dip compose run node npm i docsify-cli &lt;span class="nt"&gt;-g&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can simplify the command a bit if we define the &lt;code&gt;node&lt;/code&gt; command in the &lt;code&gt;dip.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;interaction&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;
  &lt;span class="na"&gt;node&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Open Node service terminal&lt;/span&gt;
    &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;node&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we can type fewer characters: &lt;code&gt;dip node npm i docsify-cli -g&lt;/code&gt; 🙂&lt;/p&gt;

&lt;p&gt;Now to run a Docsify server we just need to invoke &lt;code&gt;dip up docsify&lt;/code&gt; in the project's folder.&lt;/p&gt;

&lt;h4&gt;
  
  
  Erlang example: keeping build artifacts
&lt;/h4&gt;

&lt;p&gt;The final example I'd like to share is from the world of compiled languages—let's talk some Erlang!&lt;/p&gt;

&lt;p&gt;As before, we define a service in the &lt;code&gt;docker-compose.yml&lt;/code&gt; and the corresponding shortcut in the &lt;code&gt;dip.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# docker-compose.yml&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;
  &lt;span class="na"&gt;erlang&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nl"&gt;&amp;amp;erlang&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;erlang:23&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;${PWD}:/${PWD}:cached&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;rebar_cache:/rebar_data&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;history:/usr/local/hist&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./.bashrc:/root/.bashrc:ro&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;REBAR_CACHE_DIR&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/rebar_data/.cache&lt;/span&gt;
      &lt;span class="na"&gt;REBAR_GLOBAL_CONFIG_DIR&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/rebar_data/.config&lt;/span&gt;
      &lt;span class="na"&gt;REBAR_BASE_DIR&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/rebar_data/.project-cache${PWD}&lt;/span&gt;
      &lt;span class="na"&gt;HISTFILE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/usr/local/hist/.bash_history&lt;/span&gt;
      &lt;span class="na"&gt;PROMPT_DIRTRIM&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
      &lt;span class="na"&gt;PS1&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;[\W]\!&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;
    &lt;span class="na"&gt;working_dir&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${PWD}&lt;/span&gt;
    &lt;span class="na"&gt;tmpfs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/tmp&lt;/span&gt;

&lt;span class="c1"&gt;# dip.yml&lt;/span&gt;
&lt;span class="na"&gt;interactions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;
  &lt;span class="na"&gt;erl&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Open Erlang service terminal&lt;/span&gt;
    &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;erlang&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/bin/bash&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What differs this configuration from the Ruby one is that we the same &lt;code&gt;pwd&lt;/code&gt; trick to store dependencies and build files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;REBAR_BASE_DIR&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/rebar_data/.project-cache${PWD}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That change the default &lt;code&gt;_build&lt;/code&gt; location to the one within the mounted volume (and &lt;code&gt;${PWD}&lt;/code&gt; ensures we have no collisions with other projects).&lt;/p&gt;

&lt;p&gt;This helps us to speed up the compilation by not writing to the host (which is especially useful for MacOS users).&lt;/p&gt;

&lt;h4&gt;
  
  
  Bonus: multiple compose files
&lt;/h4&gt;

&lt;p&gt;One benefit of using Dip is the ability to specify multiple compose files to load services from. That allows us to group services by their &lt;em&gt;nature&lt;/em&gt; and avoid putting everything into the same &lt;code&gt;docker-compose.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# dip.yml&lt;/span&gt;
&lt;span class="na"&gt;compose&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;files&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./.dip/docker-compose.base.yml&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./.dip/docker-compose.databases.yml&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./.dip/docker-compose.ruby.yml&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./.dip/docker-compose.node.yml&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./.dip/docker-compose.erlang.yml&lt;/span&gt;
  &lt;span class="na"&gt;project_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;shared_dip_env&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;That's it! The example setup could be found in a &lt;a href="https://gist.github.com/palkan/e0befcf3bcf3393f4db2090b2288a408" rel="noopener noreferrer"&gt;gist&lt;/a&gt;. Feel free to use and share your feedback!&lt;/p&gt;




&lt;p&gt;P.S. I should admit that my initial plan of not installing anything on a local machine failed: I gave up and ran &lt;code&gt;brew install ruby&lt;/code&gt; (though that was long before the Phase 2).&lt;/p&gt;




&lt;p&gt;P.P.S. Recently, I got access to &lt;a href="https://github.com/features/codespaces" rel="noopener noreferrer"&gt;GitHub Codespaces&lt;/a&gt;. I still haven't figured out all the details, but it looks like it could become my first choice for library development in the future (and the hacks described in this post will no longer be needed 🙂).&lt;/p&gt;

</description>
      <category>docker</category>
      <category>dip</category>
    </item>
    <item>
      <title>Smooth PostgreSQL upgrades in DockerDev environments with Lefthook</title>
      <dc:creator>Vladimir Dementyev</dc:creator>
      <pubDate>Wed, 30 Sep 2020 16:38:25 +0000</pubDate>
      <link>https://dev.to/palkan_tula/smooth-postgresql-upgrades-in-dockerdev-environments-with-lefthook-203k</link>
      <guid>https://dev.to/palkan_tula/smooth-postgresql-upgrades-in-dockerdev-environments-with-lefthook-203k</guid>
      <description>&lt;p&gt;It's not a secret that I'm a big fan of Docker for local development: I'm building large &lt;a href="https://evilmartians.com/chronicles/ruby-on-whales-docker-for-ruby-rails-development" rel="noopener noreferrer"&gt;Rails apps&lt;/a&gt; with it and hacking &lt;a href="https://github.com/palkan/ruby-dip" rel="noopener noreferrer"&gt;Ruby itself&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The main reason why I love Dockerized environments is the simplicity of synchronizing configuration between developers/machines: all you need is to update a &lt;code&gt;Dockerfile&lt;/code&gt; or &lt;code&gt;docker-compose.yml&lt;/code&gt; and bump an image version.&lt;/p&gt;

&lt;p&gt;Unfortunately, that's not always the case: imaging a situation of upgrading a database (PostgreSQL in our case) to a new major version. Simply changing &lt;code&gt;postgres:12&lt;/code&gt; to &lt;code&gt;postgres:13&lt;/code&gt; would break our dev env:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FATAL: database files are incompatible with server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Let's see how we can make such upgrades painless with the help of &lt;a href="https://github.com/Arkweid/lefthook" rel="noopener noreferrer"&gt;Lefthook&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Upgrading PostgreSQL in Docker
&lt;/h2&gt;

&lt;p&gt;Although PostgreSQL has an official upgrade tool (&lt;a href="https://www.postgresql.org/docs/current/pgupgrade.html" rel="noopener noreferrer"&gt;&lt;code&gt;pg_upgrade&lt;/code&gt;&lt;/a&gt;), it's hardly possible to use it with Docker since it requires both versions of PostgreSQL to be installed on the same &lt;em&gt;machine&lt;/em&gt;:&lt;/p&gt;


&lt;div class="ltag_github-liquid-tag"&gt;
  &lt;h1&gt;
    &lt;a href="https://github.com/docker-library/postgres/issues/37" rel="noopener noreferrer"&gt;
      &lt;img class="github-logo" alt="GitHub logo" src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev.to%2Fassets%2Fgithub-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg"&gt;
      &lt;span class="issue-title"&gt;
        Upgrading between major versions?
      &lt;/span&gt;
      &lt;span class="issue-number"&gt;#37&lt;/span&gt;
    &lt;/a&gt;
  &lt;/h1&gt;
  &lt;div class="github-thread"&gt;
    &lt;div class="timeline-comment-header"&gt;
      &lt;a href="https://github.com/roosmaa" rel="noopener noreferrer"&gt;
        &lt;img class="github-liquid-tag-img" src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Favatars3.githubusercontent.com%2Fu%2F65596%3Fv%3D4" alt="roosmaa avatar"&gt;
      &lt;/a&gt;
      &lt;div class="timeline-comment-header-text"&gt;
        &lt;strong&gt;
          &lt;a href="https://github.com/roosmaa" rel="noopener noreferrer"&gt;roosmaa&lt;/a&gt;
        &lt;/strong&gt; posted on &lt;a href="https://github.com/docker-library/postgres/issues/37" rel="noopener noreferrer"&gt;&lt;time&gt;Nov 23, 2014&lt;/time&gt;&lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="ltag-github-body"&gt;
      &lt;p&gt;There doesn't seem to be a good way to upgrade between major versions of postgres. When sharing the volume with a new container with a newer version of postgres it won't run as the data directory hasn't been upgraded. &lt;code&gt;pg_upgrade&lt;/code&gt; on the other hand requires (?) old installation binary files, so upgrading the data files from new server container is also difficult.&lt;/p&gt;
&lt;p&gt;It would be nice if there was some suggested way of doing this in the readme. Maybe even some meta container which does the upgrading from version to version?&lt;/p&gt;

    &lt;/div&gt;
    &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/docker-library/postgres/issues/37" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;



&lt;p&gt;You can find a straightforward workaround in the linked thread: create a SQL dump with the previous version and &lt;em&gt;restore&lt;/em&gt; it into the new one. Let's do that!&lt;/p&gt;

&lt;p&gt;First, we need to update our &lt;code&gt;docker-compose.yml&lt;/code&gt; to define two PostgreSQL services (see &lt;a href="https://evilmartians.com/chronicles/ruby-on-whales-docker-for-ruby-rails-development" rel="noopener noreferrer"&gt;Ruby on Whales&lt;/a&gt; for the complete configuration):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;
  &lt;span class="c1"&gt;# That's the old version of a service&lt;/span&gt;
  &lt;span class="na"&gt;postgres-old&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:12&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.psqlrc:/root/.psqlrc:ro&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;postgres:/var/lib/postgresql/data&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./log:/root/log:cached&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
       &lt;span class="na"&gt;PSQL_HISTFILE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/root/log/.psql_history&lt;/span&gt;
       &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pg_isready -U postgres -h 127.0.0.1&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;

  &lt;span class="na"&gt;postgres&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:13&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
       &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./.psqlrc:/root/.psqlrc:ro&lt;/span&gt;
       &lt;span class="c1"&gt;# NOTE: we have a version suffix in a new volume name&lt;/span&gt;
       &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;postgres12:/var/lib/postgresql/data&lt;/span&gt;
       &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;../log:/root/log:cached&lt;/span&gt;
     &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
       &lt;span class="na"&gt;PSQL_HISTFILE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/root/log/.psql_history&lt;/span&gt;
       &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
     &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
       &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="m"&gt;5432&lt;/span&gt;
     &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pg_isready -U postgres -h 127.0.0.1&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;postgres12&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; If you using PostgreSQL tools in your application container, you also need to upgrade them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;  app: &amp;amp;app
    build:
      context: .dockerdev
      dockerfile: Dockerfile
      args:
        RUBY_VERSION: '2.6.3'
&lt;span class="gd"&gt;-       PG_MAJOR: '12'
&lt;/span&gt;&lt;span class="gi"&gt;+       PG_MAJOR: '13' 
&lt;/span&gt;        NODE_MAJOR: '11'
        YARN_VERSION: '1.13.0'
        BUNDLER_VERSION: '2.0.2'
    # IMPORTANT: Bump a minor version of a dev image
    # to re-build it for everyone
&lt;span class="gd"&gt;-   image: my-app-dev:1.0.0
&lt;/span&gt;&lt;span class="gi"&gt;+   image: my-app-dev:1.1.0
&lt;/span&gt;    tmpfs:
      - /tmp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, let's start our postgres services and "migrate" the data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# make sure containers are not running
docker-compose stop

# then, start them
docker-compose up -d postgres-old postgres

# make sure that both containers are up and running (e.g., via `docker ps`)
# that could take some time, because we need to download a new image

# finally, run the migration command
docker exec my-app-dev_postgres-old_1 pg_dumpall -U postgres | \
  docker exec -i my-app-dev_postgres_1 psql -U postgres
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; Replace &lt;code&gt;my-app&lt;/code&gt; with your Docker Compose project name.&lt;/p&gt;

&lt;p&gt;The last command looks a bit complicated and requires some &lt;em&gt;hidden&lt;/em&gt; knowledge: the names of the PostgreSQL containers.&lt;/p&gt;

&lt;p&gt;If you're using &lt;a href="https://github.com/bibendi/dip" rel="noopener noreferrer"&gt;Dip&lt;/a&gt; (like I do), you can define custom commands instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# dip.yml&lt;/span&gt;

&lt;span class="na"&gt;interaction&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;
  &lt;span class="na"&gt;psql&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run psql console&lt;/span&gt;
    &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
    &lt;span class="na"&gt;default_args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;zipline_light_development&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;env PGPASSWORD=postgres psql -h postgres -U postgres -d&lt;/span&gt;
  &lt;span class="na"&gt;pg_dump_old&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Dump a database for a previous PostgreSQL version&lt;/span&gt;
    &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres-old&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pg_dumpall -U postgres -h postgres-old&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And now the migration command transforms into:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dip pg_dump_old | dip psql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hopefully, the process above succeeds, and you get all your &lt;br&gt;
local data successfully migrated.&lt;/p&gt;

&lt;p&gt;Now, let's talk about how to automate this process and help your co-workers to avoid this not-so-pleasant experience.&lt;/p&gt;
&lt;h2&gt;
  
  
  Automating upgrades with Lefthook
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/Arkweid/lefthook" rel="noopener noreferrer"&gt;Lefthook&lt;/a&gt; is my top choice for managing Git hooks.&lt;br&gt;
Which hook could help us solve the problem? I think the &lt;code&gt;post-checkout&lt;/code&gt; hook is a good fit here: whenever a user switches to a branch with the upgraded PostgreSQL, we can automatically perform the required migration actions.&lt;/p&gt;

&lt;p&gt;Let's create a new script under &lt;code&gt;.lefthook/post-checkout&lt;/code&gt; called &lt;code&gt;postgres-upgrade&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;

&lt;span class="c"&gt;# Do not run for irrelevant checkout (i.e., when we do `git checkout` but do not switch branches)&lt;/span&gt;
&lt;span class="nv"&gt;BRANCH_CHANGE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$3&lt;/span&gt;
&lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nv"&gt;$BRANCH_CHANGE&lt;/span&gt; &lt;span class="nt"&gt;-eq&lt;/span&gt; 0 &lt;span class="o"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;exit
&lt;/span&gt;&lt;span class="nv"&gt;PREV_HEAD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;
&lt;span class="nv"&gt;CURR_HEAD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;$PREV_HEAD&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nv"&gt;$CURR_HEAD&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;exit&lt;/span&gt;

&lt;span class="c"&gt;# Only run when Dip is installed: users without Dip are likely not using Docker dev env at all.&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; which dip &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="c"&gt;# Only run if Docker is running&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;docker info &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null 2&amp;gt;&amp;amp;1&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Docker is not running. Skipping postgres-upgrade hook"&lt;/span&gt;
  &lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="c"&gt;# If there is no new postgres volume, then we haven't used a branch with the upgrade before&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;docker volume &lt;span class="nb"&gt;ls&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;my-app-dev_postgres13 &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
  &lt;span class="c"&gt;# Already created&lt;/span&gt;
  &lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="c"&gt;# Drop the old container if any (otherwise, there could be a conflict when we mount a folder with a new version)&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;docker container &lt;span class="nb"&gt;ls&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;my-app-dev_postgres_1 &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;dip compose &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; postgres
&lt;span class="k"&gt;fi

&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Preparing a new PostgreSQL container to upgrade to version 13..."&lt;/span&gt;

dip up &lt;span class="nt"&gt;-d&lt;/span&gt; postgres-old postgres

&lt;span class="c"&gt;# Wait for a new container to become healthy&lt;/span&gt;
&lt;span class="k"&gt;until &lt;/span&gt;docker container &lt;span class="nb"&gt;ls&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;my-app-dev_postgres_1 | &lt;span class="nb"&gt;grep &lt;/span&gt;healthy&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;do
    &lt;/span&gt;&lt;span class="nb"&gt;sleep &lt;/span&gt;1
  &lt;span class="k"&gt;done

&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Migrating data from the old PostgreSQL instance to a new one..."&lt;/span&gt;

dip pg_dump_old | dip psql

&lt;span class="c"&gt;# Stop the old container, we don't need it anymore&lt;/span&gt;
dip stop postgres-old

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Done ✅"&lt;/span&gt;

&lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, let's add our script to the Lefthook configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;post-checkout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;scripts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;postgres-upgrade&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;runner&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bash"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; If that's the first &lt;code&gt;post-checkout&lt;/code&gt; hook, you also need to run &lt;code&gt;lefthook install&lt;/code&gt; to activate it.&lt;/p&gt;

&lt;p&gt;That's it!&lt;/p&gt;

</description>
      <category>docker</category>
      <category>postgres</category>
      <category>git</category>
    </item>
    <item>
      <title>Cables profiling returns: GC.compact &amp; jemalloc</title>
      <dc:creator>Vladimir Dementyev</dc:creator>
      <pubDate>Fri, 28 Aug 2020 10:49:03 +0000</pubDate>
      <link>https://dev.to/palkan_tula/cables-profiling-returns-gc-compact-jemalloc-33c9</link>
      <guid>https://dev.to/palkan_tula/cables-profiling-returns-gc-compact-jemalloc-33c9</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;This post introduces a new series—&lt;em&gt;Unfinished plays&lt;/em&gt;. I have many drafts and incomplete posts I do not plan to finish in the nearest future. Since they still could be interesting/helpful to the community, I decided to release them almost as-is. This is the first one.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In the &lt;a href="https://dev.to/evilmartians/cables-vs-malloctrim-or-yet-another-ruby-memory-usage-benchmark-3emo"&gt;first part&lt;/a&gt;, we compared Action Cable memory usage with different VM configurations: &lt;code&gt;MALLOC_ARENA_MAX&lt;/code&gt;, &lt;code&gt;malloc_trim&lt;/code&gt;. I promised to continue researching in this direction and evaluate &lt;code&gt;GC.compact&lt;/code&gt; and jemalloc (via &lt;a href="https://fullstaqruby.org" rel="noopener noreferrer"&gt;Fullstaq Ruby&lt;/a&gt;) as well. So, here we are.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Benchmark
&lt;/h2&gt;

&lt;p&gt;This time I decided to analyze a little bit different scenario: "A sudden attack during a constant pressure".&lt;/p&gt;

&lt;p&gt;"Constant pressure" means a uniform load: clients are connecting, communicating, and disconnecting during the benchmark, but the total number of concurrent users stays about the same.&lt;/p&gt;

&lt;p&gt;I have a specific tool for writing such scenarios called &lt;a href="https://github.com/palkan/wsdirector" rel="noopener noreferrer"&gt;wsdirector&lt;/a&gt;. It allows you to define a scenario in YML format and run it using a different scale factor.&lt;/p&gt;

&lt;p&gt;"A sudden attack" emulates a situation when the number of concurrent connections unexpectedly spikes and then returns to normal. We do that by performing the "WebSocket shootout" scenario from the &lt;a href="https://dev.to/evilmartians/cables-vs-malloctrim-or-yet-another-ruby-memory-usage-benchmark-3emo"&gt;first part&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The source code of the benchmark is available here: &lt;a href="https://github.com/anycable/simple-cable-app" rel="noopener noreferrer"&gt;https://github.com/anycable/simple-cable-app&lt;/a&gt; (see &lt;a href="https://github.com/anycable/simple-cable-app/blob/master/features/simulate.yml" rel="noopener noreferrer"&gt;simiulate.yml&lt;/a&gt; and &lt;a href="https://github.com/anycable/simple-cable-app/blob/master/benchmarks/simulate.rb" rel="noopener noreferrer"&gt;simulate.rb&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;The server is running within a Docker container. During the benchmark, we capture the container's memory usage and generate a chart in the end (with the help of the awesome &lt;a href="https://github.com/red-data-tools/unicode_plot.rb" rel="noopener noreferrer"&gt;unicode_plot&lt;/a&gt; library, see &lt;a href="https://github.com/anycable/simple-cable-app/blob/master/benchmarks/monitor_docker.rb" rel="noopener noreferrer"&gt;monitor_docker.rb&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;The exact command I used for the benchmarks below is as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# first, start the app&lt;/span&gt;
dip up rails

&lt;span class="c"&gt;# or for Fullstaq&lt;/span&gt;
dip up rails-fs

&lt;span class="c"&gt;# or for Fullstaq with malloc_trim&lt;/span&gt;
dip up rails-fs-trim

&lt;span class="c"&gt;# pressure&lt;/span&gt;
&lt;span class="nv"&gt;TOTAL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;20 &lt;span class="nv"&gt;SAMPLE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;50 &lt;span class="nv"&gt;N&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;4 &lt;span class="nv"&gt;SCALE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;200 ruby benchmarks/simulate.rb

&lt;span class="c"&gt;# spike (runs twice during the pressure)&lt;/span&gt;
&lt;span class="nb"&gt;cat &lt;/span&gt;benchmarks/broadcast.opts | xargs websocket-bench broadcast
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Below you can find the results I got on my machine (Windows 10 + WSL2, AMD Ryzen 3200G 3.6GHz, 16GB RAM). But first, let's talk about the heap compaction for a bit.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;GC.compact&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;GC compaction has been finally released in Ruby 2.7 (thanks to &lt;a href="https://twitter.com/tenderlove" rel="noopener noreferrer"&gt;Aaron Patterson&lt;/a&gt;). To learn more about this feature, watch one of the latest Aaron's talks, for example, this one from RubyConf 2019:&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/1F3gXYhQsAY"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;First, I tried to visualize the effect of the &lt;code&gt;GC.compact&lt;/code&gt; after running a simulation with 1k connected clients:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fxnv4sihbiym80jlb73a0.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fxnv4sihbiym80jlb73a0.jpg" alt="No compaction"&gt;&lt;/a&gt;&lt;/p&gt;
Without compaction



&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F6r9j8jpihpiz5q1nrw7v.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F6r9j8jpihpiz5q1nrw7v.jpg" alt="After compaction"&gt;&lt;/a&gt;&lt;/p&gt;
After GC.compact



&lt;p&gt;Awesome! Compaction works! Or does it 🤔&lt;/p&gt;

&lt;p&gt;I tried to run the &lt;em&gt;pressure&lt;/em&gt; scenario with added &lt;code&gt;GC.compact&lt;/code&gt; calls after each &lt;em&gt;wave&lt;/em&gt; (while other &lt;em&gt;waves&lt;/em&gt; are active, i.e., we continue accepting connections, broadcasting messages, etc.) and, unfortunately, found myself in the &lt;a href="https://github.com/anycable/simple-cable-app/issues/6" rel="noopener noreferrer"&gt;segmentation fault&lt;/a&gt; situation. Likely, the problem is with C extensions (we have at least Puma and nio4r).&lt;/p&gt;

&lt;p&gt;Calling &lt;code&gt;GC.compact&lt;/code&gt; while there are no active Action Cable clients works fine. So, I had to update the scenario a bit and add a "stop-the-world"-like feature to perform compaction in isolation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fullstaq Ruby &amp;amp; jemalloc
&lt;/h2&gt;

&lt;p&gt;&lt;a href="http://jemalloc.net" rel="noopener noreferrer"&gt;Jemalloc&lt;/a&gt; is an alternative memory allocator which could be used instead of &lt;code&gt;malloc&lt;/code&gt; (used by default in MRI).&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Here are a couple of articles to learn more about jemalloc for Rails applications: &lt;a href="https://www.mikeperham.com/2018/04/25/taming-rails-memory-bloat/" rel="noopener noreferrer"&gt;one&lt;/a&gt; and &lt;a href="https://www.speedshop.co/2017/12/04/malloc-doubles-ruby-memory.html" rel="noopener noreferrer"&gt;two&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We're going to use a &lt;a href="https://fullstaqruby.org" rel="noopener noreferrer"&gt;Fullstaq Ruby&lt;/a&gt; distribution with jemalloc built-in (via &lt;a href="https://github.com/evilmartians/fullstaq-ruby-docker" rel="noopener noreferrer"&gt;Docker images provided by Evil Martians&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Benchmark results
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; Since this is an unfinished play, I'm not providing any explanations/bikeshedding here—just plain results. Feel free to start a discussion in the comments!&lt;/p&gt;

&lt;p&gt;We have 6 different setups:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;baseline&lt;/strong&gt;: MRI 2.7.1&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;compact&lt;/strong&gt;: MRI 2.7.1 w/ compaction&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;jemalloc&lt;/strong&gt;: Fullstaq Ruby 2.7.1 w/ jemalloc&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;jemalloc_compact&lt;/strong&gt;: Fullstaq Ruby 2.7.1 w/ jemalloc and compaction&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;trim&lt;/strong&gt;: Fullstaq Ruby 2.7.1 w/ malloc_trim&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;trim_compact&lt;/strong&gt;: Fullstaq Ruby 2.7.1 w/ malloc_trim and compaction.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Below you can find the results of the benchmarks in different combinations (to better demonstrate the difference).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fhoua6t8t16i7xhxl3fg7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fhoua6t8t16i7xhxl3fg7.png" alt="w/o compaction"&gt;&lt;/a&gt;&lt;/p&gt;
Without compaction



&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fxngte18a40zpvwzralpz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fxngte18a40zpvwzralpz.png" alt="w/ compaction"&gt;&lt;/a&gt;&lt;/p&gt;
With compaction



&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fjh3rqyt86sbjbz0h3ngb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fjh3rqyt86sbjbz0h3ngb.png" alt="Ruby 2.7 w/ and w/o compaction"&gt;&lt;/a&gt;&lt;/p&gt;
Ruby 2.7.1



&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fbt5pduf9cro3gu6cwtrs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fbt5pduf9cro3gu6cwtrs.png" alt="Fullstaq jemalloc w/ and w/o compaction"&gt;&lt;/a&gt;&lt;/p&gt;
FullstaqRuby 2.7.1 with jemalloc



&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fx6u7vt385bsh7ay6lwzs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fx6u7vt385bsh7ay6lwzs.png" alt="Fullstaq Ruby 2.7.1 + malloc_trim w/ and w/o compaction"&gt;&lt;/a&gt;&lt;/p&gt;
FullstaqRuby 2.7.1 with malloc_trim



&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F3h8u15h97ev58rps6dcj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F3h8u15h97ev58rps6dcj.png" alt="All together"&gt;&lt;/a&gt;&lt;/p&gt;
All results combined



</description>
      <category>ruby</category>
      <category>profiling</category>
      <category>benchmarks</category>
    </item>
    <item>
      <title>RuboCoping with legacy</title>
      <dc:creator>Vladimir Dementyev</dc:creator>
      <pubDate>Tue, 24 Mar 2020 21:38:35 +0000</pubDate>
      <link>https://dev.to/evilmartians/rubocoping-with-legacy-gme</link>
      <guid>https://dev.to/evilmartians/rubocoping-with-legacy-gme</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally posted in &lt;a href="https://evilmartians.com/chronicles/rubocoping-with-legacy-bring-your-ruby-code-up-to-standard"&gt;Martian Chronicles&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You will hardly find a Ruby developer who hasn't heard about &lt;a href="https://docs.rubocop.org"&gt;RuboCop&lt;/a&gt;, &lt;em&gt;the&lt;/em&gt; Ruby linter and formatter. And still, it is not that hard to find a project where code style is not enforced. Usually, these are large, mature codebases, often successful ones. Fixing linting and formatting can be a challenge if it wasn't set up correctly from the get-go. So, your RuboCop sees red! Here's how to fix it.&lt;/p&gt;

&lt;p&gt;In this post, I will show you how we at Evil Martians touch up codebases of our customers in 2020: from quick and dirty hacks to proper &lt;a href="https://github.com/testdouble/standard"&gt;Standard&lt;/a&gt;-enforced style guides, and our own &lt;del&gt;patented&lt;/del&gt; way to use Standard and RuboCop configs together.&lt;/p&gt;

&lt;h2&gt;
  
  
  Style matters
&lt;/h2&gt;

&lt;p&gt;Let's pretend I have to convince you to follow code style guidelines (I know, I know I don't have to!)&lt;/p&gt;

&lt;p&gt;Here are the arguments I would use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Developers understand each other much better when they &lt;del&gt;speak&lt;/del&gt; write the same language.&lt;/li&gt;
&lt;li&gt;Onboarding new engineers becomes much easier when the code style is standardized.&lt;/li&gt;
&lt;li&gt;Linters help to detect and squash bugs in time.&lt;/li&gt;
&lt;li&gt;No more "single vs. double quotes" holy wars (double FTW)!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That was all the theory for today. Time for practice!&lt;/p&gt;

&lt;h2&gt;
  
  
  TODO or not TODO
&lt;/h2&gt;

&lt;p&gt;So, you have joined a project with no style guide or with a &lt;code&gt;.rubocop.yml&lt;/code&gt; that was added years ago. You run RuboCop, and you see something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;bundle &lt;span class="nb"&gt;exec &lt;/span&gt;rubocop

3306 files inspected, 12418 offenses detected
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Flocks of noble &lt;del&gt;knights&lt;/del&gt; developers tried to &lt;del&gt;slay the beast&lt;/del&gt; fix the offenses but gave up. But that doesn't stop you—you know the magic spell:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;bundle &lt;span class="nb"&gt;exec &lt;/span&gt;rubocop &lt;span class="nt"&gt;--auto-gen-config&lt;/span&gt;
Added inheritance from &lt;span class="sb"&gt;`&lt;/span&gt;.rubocop_todo.yml&lt;span class="sb"&gt;`&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="sb"&gt;`&lt;/span&gt;.rubocop.yml&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
Created .rubocop_todo.yml.

&lt;span class="nv"&gt;$ &lt;/span&gt;bundle &lt;span class="nb"&gt;exec &lt;/span&gt;rubocop
3306 files inspected, no offenses detected
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;That was simple! &lt;em&gt;Toss the coin to your...&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Let's take a closer look at what &lt;code&gt;--auto-gen-config&lt;/code&gt; flag does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;First, it collects all the offenses and their counts;&lt;/li&gt;
&lt;li&gt;then, it generates a &lt;code&gt;.rubocop_todo.yml&lt;/code&gt; where all the current offenses are ignored;&lt;/li&gt;
&lt;li&gt;and finally, it makes &lt;code&gt;.rubocop.yml&lt;/code&gt; inherit from &lt;code&gt;.rubocop_todo.yml&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is the way to set the status quo and only enforce style checks for new code. Sounds smart, right? Not exactly.&lt;/p&gt;

&lt;p&gt;The way &lt;code&gt;.rubocop_todo.yml&lt;/code&gt; handles "ignores" depends on the cop types and the total number of current offenses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;For metrics cops (such as &lt;code&gt;Layout/LineLength&lt;/code&gt;), the limit (&lt;code&gt;Max&lt;/code&gt;) is set to the maximum value for the current codebase.&lt;/li&gt;
&lt;li&gt;All cops could be disabled if the total number of offenses hits the threshold (only 15 by default).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So, you end up with &lt;em&gt;anything goes&lt;/em&gt; situation, and that defeats the purpose.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://medium.com/@scottm/rubocop-in-legacy-projects-part-1-todos-and-todonts-877ace9f23b7"&gt;This article&lt;/a&gt; covers this problem in more detail.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;What does it mean for a typical legacy codebase? Most of the new code would be ignored by RuboCop, too. We made the tool happy, but are &lt;em&gt;we&lt;/em&gt; happy with it?&lt;/p&gt;

&lt;p&gt;Hopefully, there is a way to generate a better TODO config by adding more options to the command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;bundle &lt;span class="nb"&gt;exec &lt;/span&gt;rubocop –-auto-gen-config &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--auto-gen-only-exclude&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--exclude-limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;10000
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Where &lt;code&gt;--auto-gen-only-exclude&lt;/code&gt; force-excludes metrics cops instead of changing their &lt;code&gt;Max&lt;/code&gt; value, and &lt;code&gt;--exclude-limit&lt;/code&gt; sets the threshold for the exclusion (set to some large enough number to avoid disabling cops completely).&lt;/p&gt;

&lt;p&gt;Now your &lt;code&gt;.rubocop_todo.yml&lt;/code&gt; won't affect your new files or entirely new offenses in the old ones.&lt;/p&gt;

&lt;p&gt;RuboCop doesn't only help with style—it also saves you from common mistakes that can break your code in production. What if you had some bugs and ignored them in your TODO config? What are the cops that should never be ignored? Let me introduce the &lt;em&gt;RuboCop strict configuration&lt;/em&gt; pattern.&lt;/p&gt;

&lt;h2&gt;
  
  
  You shall not pass: introducing &lt;code&gt;.rubocop_strict.yml&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;There are a handful of cops that must be enabled for all the files independently of the &lt;code&gt;.rubocop_todo.yml&lt;/code&gt;. For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Lint/Debugger&lt;/code&gt;—don't leave debugging calls (e.g., &lt;code&gt;binding.pry&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;RSpec/Focus&lt;/code&gt; (from &lt;a href="https://github.com/rubocop-hq/rubocop-rspec"&gt;&lt;code&gt;rubocop-rspec&lt;/code&gt;&lt;/a&gt;)—don't forget to clear focused tests (to make sure CI runs the whole test suite).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We put such cops into a &lt;a href="https://gist.github.com/palkan/ee1d0247be2076e38020a9a6fbae68d5"&gt;&lt;code&gt;.rubocop_strict.yml&lt;/code&gt;&lt;/a&gt; configuration file like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;inherit_from&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.rubocop_todo.yml&lt;/span&gt;

&lt;span class="s"&gt;Lint/Debugger&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;# don't leave binding.pry&lt;/span&gt;
  &lt;span class="na"&gt;Enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;Exclude&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[]&lt;/span&gt;

&lt;span class="s"&gt;RSpec/Focus&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;# run ALL tests on CI&lt;/span&gt;
  &lt;span class="na"&gt;Enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;Exclude&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[]&lt;/span&gt;

&lt;span class="s"&gt;Rails/Output&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;# Don't leave puts-debugging&lt;/span&gt;
  &lt;span class="na"&gt;Enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;Exclude&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[]&lt;/span&gt;

&lt;span class="s"&gt;Rails/FindEach&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;# each could severely affect the performance, use find_each&lt;/span&gt;
  &lt;span class="na"&gt;Enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;Exclude&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[]&lt;/span&gt;

&lt;span class="s"&gt;Rails/UniqBeforePluck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;# uniq.pluck and not pluck.uniq&lt;/span&gt;
  &lt;span class="na"&gt;Enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;Exclude&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Then, we replace the TODO config with the Strict config in our base &lt;code&gt;.rubocop.yml&lt;/code&gt; configuration file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight diff"&gt;&lt;code&gt; inherit_from:
&lt;span class="gd"&gt;-  - .rubocop_todo.yml
&lt;/span&gt;&lt;span class="gi"&gt;+  - .rubocop_strict.yml
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;Exclude: []&lt;/code&gt; is crucial here: even if our &lt;code&gt;.rubocop_todo.yml&lt;/code&gt; contained exclusions for &lt;em&gt;strict&lt;/em&gt; cops, we &lt;em&gt;nullify&lt;/em&gt; them here, thus, re-activating these cops for all the files.&lt;/p&gt;

&lt;h2&gt;
  
  
  One Standard to rule them all
&lt;/h2&gt;

&lt;p&gt;One of the biggest problems in adopting a code style is to convince everyone on the team to always use double-quotes for strings, or to add trailing commas to multiline arrays, or ro &amp;lt;choose-your-own-controversal-style-rule&amp;gt;? We are all well familiar with bikeshedding.&lt;/p&gt;

&lt;p&gt;RuboCop provides a default configuration based on the &lt;a href="https://github.com/rubocop-hq/ruby-style-guide"&gt;Ruby Style Guide&lt;/a&gt;. And you know what? It's hard to find a project which follows all of the default rules, there are always reconfigured or disabled cops in the &lt;code&gt;.rubocop.yml&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That's okay. RuboCop's default configuration is not a &lt;em&gt;golden standard&lt;/em&gt;; it was never meant to be the one style to fit them all.&lt;/p&gt;

&lt;p&gt;Should Ruby community have &lt;em&gt;the only style&lt;/em&gt; at all? It seems that yes, we need it.&lt;/p&gt;

&lt;p&gt;I think the main reason for that is the popularity of auto-formatters in other programming languages: JavaScript, Go, Rust, Elixir. Auto-formatters are usually very strict and allow almost none or zero configuration. And developers got used to that! People like writing code without worrying about indentation, brackets, and spaces; &lt;em&gt;robots&lt;/em&gt; would sort it all out!&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Checkout out the &lt;a href="https://www.youtube.com/watch?v=uLyV5hOqGQ8"&gt;lightning talk&lt;/a&gt; and let Justin Searls convince you to switch to &lt;a href="https://github.com/testdouble/standard"&gt;Standard&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Thankfully, Ruby's ecosystem has got you covered: there is a project called &lt;a href="https://github.com/testdouble/standard"&gt;Standard&lt;/a&gt;, which claims to be &lt;em&gt;the one and only Ruby style guide&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;From the technical point of view, Standard is a wrapper over RuboCop with its custom configuration and CLI (&lt;code&gt;standard&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Unfortunately, Standard lacks some RuboCop features that are essential at the early stages of a style guide's adoption: it is not possible to use &lt;code&gt;.rubocop_todo.yml&lt;/code&gt; or any other local configuration. It also &lt;a href="https://github.com/testdouble/standard/issues/135"&gt;doesn't support&lt;/a&gt; RuboCop plugins or custom cops.&lt;/p&gt;

&lt;p&gt;But we can still use Standard as a style guide while continuing to use RuboCop as a linter and formatter!&lt;/p&gt;

&lt;p&gt;For that, we can use RuboCop's &lt;code&gt;inherit_gem&lt;/code&gt; directive:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .rubocop.yml&lt;/span&gt;

&lt;span class="c1"&gt;# We want Exclude directives from different&lt;/span&gt;
&lt;span class="c1"&gt;# config files to get merged, not overwritten&lt;/span&gt;
&lt;span class="na"&gt;inherit_mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;merge&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Exclude&lt;/span&gt;

&lt;span class="na"&gt;require&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# Performance cops are bundled with Standard&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;rubocop-performance&lt;/span&gt;
  &lt;span class="c1"&gt;# Standard's config uses this custom cop,&lt;/span&gt;
  &lt;span class="c1"&gt;# so it must be loaded&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;standard/cop/semantic_blocks&lt;/span&gt;

&lt;span class="na"&gt;inherit_gem&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;standard&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;config/base.yml&lt;/span&gt;

&lt;span class="na"&gt;inherit_from&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.rubocop_strict.yml&lt;/span&gt;

&lt;span class="c1"&gt;# Sometimes we enable metrics cops&lt;/span&gt;
&lt;span class="c1"&gt;# (which are disabled in Standard by default)&lt;/span&gt;
&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;span class="c1"&gt;# Metrics:&lt;/span&gt;
&lt;span class="c1"&gt;#   Enabled: true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;That is the configuration I use in most of my OSS and commercial projects. I can't say I agree with all the rules, but I definitely like it more than the RuboCop's default. That is a tiny trade-off if you think about the benefit of not arguing over the style anymore.&lt;/p&gt;

&lt;p&gt;Don't forget to add &lt;code&gt;standard&lt;/code&gt; to your Gemfile and freeze its minor version to avoid unexpected failures during upgrades:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s2"&gt;"standard"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"~&amp;gt; 0.2.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;require: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Although the approach above allows you to tinker with the Standard configuration, I would not recommend doing that. Use this flexibility to extend the default behavior, not change it!&lt;/p&gt;

&lt;h2&gt;
  
  
  Beyond the Standard
&lt;/h2&gt;

&lt;p&gt;RuboCop has a lot of plugins distributed as separate gems: &lt;a href="https://github.com/rubocop-hq/rubocop-rspec"&gt;&lt;code&gt;rubocop-rspec&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://github.com/rubocop-hq/rubocop-performance"&gt;&lt;code&gt;rubocop-performance&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://github.com/rubocop-hq/rubocop-rails"&gt;&lt;code&gt;rubocop-rails&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://github.com/rubocop-hq/rubocop-md"&gt;&lt;code&gt;rubocop-md&lt;/code&gt;&lt;/a&gt;, to name a few.&lt;/p&gt;

&lt;p&gt;Standard only includes the &lt;code&gt;rubocop-performance&lt;/code&gt; plugin. We usually add &lt;code&gt;rubocop-rails&lt;/code&gt; and &lt;code&gt;rubocop-rspec&lt;/code&gt; to our configuration.&lt;/p&gt;

&lt;p&gt;For each plugin, we keep a separate YAML file: &lt;code&gt;.rubocop_rails.yml&lt;/code&gt;, &lt;code&gt;.rubocop_rspec.yml&lt;/code&gt;, etc.&lt;/p&gt;

&lt;p&gt;Inside the base config we add these files to &lt;code&gt;inherit_from&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;inherit_from&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.rubocop_rails.yml&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.rubocop_rspec.yml&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.rubocop_strict.yml&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Our &lt;a href="https://gist.github.com/palkan/24869b835c45e89116b9727b534e579e"&gt;&lt;code&gt;.rubocop_rails.yml&lt;/code&gt;&lt;/a&gt; is based on the &lt;a href="https://github.com/testdouble/standard/commit/94d133f477a5694084ac974d5ee01e8a66ce777e#diff-65478e10d5b2ef41c7293a110c0e6b7c"&gt;configuration that existed in Standard&lt;/a&gt; before they dropped Rails support.&lt;/p&gt;

&lt;p&gt;There is no standard RSpec configuration, so we had to figure out our own: &lt;a href="https://gist.github.com/palkan/623c0816b05ed246bfe0cb406050990a"&gt;&lt;code&gt;.rubocop_rspec.yml&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;We also usually enable a select few &lt;em&gt;custom&lt;/em&gt; cops, for example, &lt;a href="https://github.com/evilmartians/terraforming-rails/tree/master/tools/lint_env"&gt;&lt;code&gt;Lint/Env&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In the end, our typical RuboCop configuration for Rails projects looks like this👇&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .rubocop.yml&lt;/span&gt;
&lt;span class="na"&gt;inherit_mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;merge&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Exclude&lt;/span&gt;

&lt;span class="na"&gt;require&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;rubocop-performance&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;standard/cop/semantic_blocks&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./lib/cops/lint/env.rb&lt;/span&gt;

&lt;span class="na"&gt;inherit_gem&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;standard&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;config/base.yml&lt;/span&gt;

&lt;span class="na"&gt;inherit_from&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.rubocop_rails.yml&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.rubocop_rspec.yml&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.rubocop_strict.yml&lt;/span&gt;

&lt;span class="s"&gt;Lint/Env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;Enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;Include&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;**/*.rb'&lt;/span&gt;
  &lt;span class="na"&gt;Exclude&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;**/config/environments/**/*'&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;**/config/application.rb'&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;**/config/environment.rb'&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;**/config/puma.rb'&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;**/config/boot.rb'&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;**/spec/*_helper.rb'&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;**/spec/**/support/**/*'&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;lib/generators/**/*'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Feel free to use it as an inspiration for your projects that could use some RuboCop's tough love.&lt;/p&gt;




&lt;p&gt;RuboCop plays a vital role in the Ruby world and will stay TOP-1 for linting and formatting code for quite a long time (though competing formatters are evolving, for example, &lt;a href="https://github.com/penelopezone/rubyfmt/"&gt;rubyfmt&lt;/a&gt; and &lt;a href="https://github.com/prettier/plugin-ruby"&gt;prettier-ruby&lt;/a&gt;). Don't ignore RuboCop; write code in style 😎&lt;/p&gt;




&lt;p&gt;Read more dev articles on &lt;a href="https://evilmartians.com/chronicles"&gt;https://evilmartians.com/chronicles&lt;/a&gt;!&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>linters</category>
      <category>formatters</category>
      <category>rubocop</category>
    </item>
    <item>
      <title>GitHub Actions: First impressions</title>
      <dc:creator>Vladimir Dementyev</dc:creator>
      <pubDate>Fri, 06 Sep 2019 10:16:33 +0000</pubDate>
      <link>https://dev.to/evilmartians/github-actions-first-impressions-4ce8</link>
      <guid>https://dev.to/evilmartians/github-actions-first-impressions-4ce8</guid>
      <description>&lt;p&gt;GitHub Actions are coming on strong—in my team, almost everyone who has applied for a beta program, me included, had recently got access to GitHub's latest "killer feature" that threatens to make life harder for Travis CI and CircleCI: Continuous Integration with GitHub &lt;a href="https://github.com/features/actions" rel="noopener noreferrer"&gt;Actions&lt;/a&gt;. Here are my first impressions.&lt;/p&gt;

&lt;p&gt;&lt;iframe class="tweet-embed" id="tweet-1164301670429409281-5" src="https://platform.twitter.com/embed/Tweet.html?id=1164301670429409281"&gt;
&lt;/iframe&gt;

  // Detect dark theme
  var iframe = document.getElementById('tweet-1164301670429409281-5');
  if (document.body.className.includes('dark-theme')) {
    iframe.src = "https://platform.twitter.com/embed/Tweet.html?id=1164301670429409281&amp;amp;theme=dark"
  }



&lt;/p&gt;

&lt;p&gt;For the ultimate test, I decided to reuse the documentation generator setup from my previous article: &lt;a href="https://dev.to/evilmartians/keeping-oss-documentation-with-docsify-lefthook-and-friends-11e5"&gt;"Keeping OSS documentation with Docsify, Lefthook, and friends"&lt;/a&gt;). To lint a documentation website for &lt;a href="https://anycable.io/" rel="noopener noreferrer"&gt;AnyCable&lt;/a&gt;, I used &lt;a href="https://evilmartians.com/chronicles/lefthook-knock-your-teams-code-back-into-shape" rel="noopener noreferrer"&gt;Lefthook&lt;/a&gt; locally and CircleCI for production. For my next documentation project, the one for &lt;a href="https://test-prof.evilmartians.io/" rel="noopener noreferrer"&gt;TestProf&lt;/a&gt;, I decided to move all the CI functionality to GitHub Actions (sorry, Travis and CircleCI, &lt;a href="https://en.wikipedia.org/wiki/Autonomous_spaceport_drone_shiphttps://en.wikipedia.org/wiki/Autonomous_spaceport_drone_ship" rel="noopener noreferrer"&gt;of course I still love you&lt;/a&gt;).&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Check the PR with the final implementation: &lt;a href="https://github.com/palkan/test-prof/pull/156" rel="noopener noreferrer"&gt;palkan/test-prof#156&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Warming up: dealing with the stale issues
&lt;/h2&gt;

&lt;p&gt;The first thing I found impressive about GitHub Actions is that they could be used not only to deal with code pushes and pull requests but also react on other &lt;a href="https://help.github.com/en/articles/events-that-trigger-workflows" rel="noopener noreferrer"&gt;GitHub events&lt;/a&gt; or run on schedule!&lt;/p&gt;

&lt;p&gt;One of the &lt;em&gt;actions&lt;/em&gt; that GitHub offers you when you first open the "Actions" tab of your project is &lt;a href="https://github.com/actions/stale" rel="noopener noreferrer"&gt;Stale&lt;/a&gt;: it allows you to mark issues and pull requests with a "stale" label and close them. That's what I usually did by hand (and was hoping to automate with the help of the &lt;a href="https://github.com/apps/stale" rel="noopener noreferrer"&gt;Stale&lt;/a&gt; GitHub App).&lt;/p&gt;

&lt;p&gt;It took me a few minutes to add this action:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/stale.yml&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Mark stale issues&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;cron&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*"&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stale&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/stale@v1&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;repo-token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GITHUB_TOKEN }}&lt;/span&gt;
        &lt;span class="na"&gt;stale-issue-message&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="s"&gt;⚠ Marking this issue as stale since there has been no activity in the last 30 days.&lt;/span&gt;
          &lt;span class="s"&gt;Remove stale label or comment or this issue will be closed in 15 days ⌛️&lt;/span&gt;
        &lt;span class="na"&gt;stale-issue-label&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;stale&lt;/span&gt;
        &lt;span class="na"&gt;days-before-stale&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;
        &lt;span class="na"&gt;days-before-close&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;15&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After some time, I received a lot of notifications—the action took action:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fqlr9sbagvo4r9pdf4x1j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fqlr9sbagvo4r9pdf4x1j.png" alt="Stale Comment"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I quickly realized that it wasn't a good idea: GitHub marked all the issues intentionally kept open (for discussion) as stale and spammed all the participants with the comment notification. I'm sorry, guys 😿.&lt;/p&gt;

&lt;p&gt;It turned out that there is no ignore mechanism (e.g., by labels). &lt;a href="https://github.com/actions/stale/pull/11" rel="noopener noreferrer"&gt;There is one&lt;/a&gt; now, but still be careful: it is easy to get burned.&lt;/p&gt;

&lt;h2&gt;
  
  
  Migrating Markdown linters
&lt;/h2&gt;

&lt;p&gt;Reimplementing the Markdown &lt;a href="https://github.com/palkan/docs-example/blob/master/.circleci/config.yml" rel="noopener noreferrer"&gt;linting configuration&lt;/a&gt; I've already had for CircleCI was a pretty straightforward task:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Lint Docs&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;master&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;**/*.md"&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;**/*.md"&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;markdownlint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v1&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Set up Ruby &lt;/span&gt;&lt;span class="m"&gt;2.6&lt;/span&gt;
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-ruby@v1&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;ruby-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2.6.x&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run Markdown linter&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;gem install mdl&lt;/span&gt;
        &lt;span class="s"&gt;mdl docs&lt;/span&gt;
  &lt;span class="na"&gt;rubocop&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v1&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Set up Ruby &lt;/span&gt;&lt;span class="m"&gt;2.6&lt;/span&gt;
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-ruby@v1&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;ruby-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2.6.x&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Lint Markdown files with RuboCop&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;gem install bundler&lt;/span&gt;
        &lt;span class="s"&gt;bundle install --gemfile gemfiles/rubocop.gemfile --jobs 4 --retry 3&lt;/span&gt;
        &lt;span class="s"&gt;bundle exec --gemfile gemfiles/rubocop.gemfile rubocop -c .rubocop-md.yml&lt;/span&gt;
  &lt;span class="na"&gt;forspell&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v1&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install Hunspell&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;sudo apt-get install hunspell&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Set up Ruby &lt;/span&gt;&lt;span class="m"&gt;2.6&lt;/span&gt;
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-ruby@v1&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;ruby-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2.6.x&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run Forspell&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;gem install forspell&lt;/span&gt;
        &lt;span class="s"&gt;forspell docs/&lt;/span&gt;
  &lt;span class="na"&gt;liche&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v1&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Set up Go&lt;/span&gt;
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-go@v1&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;go-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1.12.x&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run liche&lt;/span&gt;
      &lt;span class="c1"&gt;# see https://github.com/actions/setup-go/issues/14&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;export PATH=$PATH:$(go env GOPATH)/bin&lt;/span&gt;
        &lt;span class="s"&gt;go get -u github.com/raviqqe/liche&lt;/span&gt;
        &lt;span class="s"&gt;liche -r docs -d docs&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few noticeable differences compared to a &lt;a href="https://github.com/palkan/docs-example/blob/master/.circleci/config.yml" rel="noopener noreferrer"&gt;CircleCI config&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ability to trigger actions &lt;em&gt;only when matching files have changed&lt;/em&gt; (See the &lt;code&gt;paths: ["**/*.md"]&lt;/code&gt; declaration).&lt;/li&gt;
&lt;li&gt;Inability to share the setup (checkout, dependency installation) between the jobs from the same action; &lt;strong&gt;there is &lt;a href="https://stackoverflow.com/questions/55110729/how-do-i-cache-steps-in-github-action" rel="noopener noreferrer"&gt;no cache&lt;/a&gt;&lt;/strong&gt;, or anything akin to CircleCI "workspaces".&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Adding Neo and Trinity for RSpec
&lt;/h2&gt;

&lt;p&gt;Adding the "Lint Docs" action went smoothly, so I decided to continue with migrating RSpec tests.&lt;/p&gt;

&lt;p&gt;I'm running gems tests against multiple Ruby and frameworks versions to make sure most of the users are covered. I've been doing this successfully for years with Travis' &lt;a href="https://docs.travis-ci.com/user/build-matrix/" rel="noopener noreferrer"&gt;Build Matrix&lt;/a&gt; feature:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;matrix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;fast_finish&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;include&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;rvm&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2.6.3&lt;/span&gt;
      &lt;span class="na"&gt;gemfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gemfiles/railsmaster.gemfile&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;rvm&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;jruby-9.2.7.0&lt;/span&gt;
      &lt;span class="na"&gt;gemfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gemfiles/jruby.gemfile&lt;/span&gt; 
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;rvm&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2.6.3&lt;/span&gt;
      &lt;span class="na"&gt;gemfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gemfiles/activerecord6.gemfile&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;rvm&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2.6.3&lt;/span&gt;
      &lt;span class="na"&gt;gemfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Gemfile&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;rvm&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2.5.3&lt;/span&gt;
      &lt;span class="na"&gt;gemfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Gemfile&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;rvm&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2.4.2&lt;/span&gt;
      &lt;span class="na"&gt;gemfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gemfiles/default_factory_girl.gemfile&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;rvm&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2.4.2&lt;/span&gt;
      &lt;span class="na"&gt;gemfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gemfiles/rspec35.gemfile&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;rvm&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2.4.2&lt;/span&gt;
      &lt;span class="na"&gt;gemfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gemfiles/activerecord42.gemfile&lt;/span&gt;
   &lt;span class="na"&gt;allow_failures&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;rvm&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2.6.2&lt;/span&gt;
      &lt;span class="na"&gt;gemfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gemfiles/railsmaster.gemfile&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;rvm&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;jruby-9.2.7.0&lt;/span&gt;
      &lt;span class="na"&gt;gemfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gemfiles/jruby.gemfile&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GitHub Actions have a similar feature—&lt;a href="https://help.github.com/en/articles/workflow-syntax-for-github-actions#jobsjob_idstrategy" rel="noopener noreferrer"&gt;&lt;code&gt;strategy.matrix&lt;/code&gt;&lt;/a&gt;. And it's even stated in the documentation that the &lt;code&gt;include&lt;/code&gt;-only variant works the same way as in Travis. However, that's &lt;a href="https://github.community/t5/How-to-use-Git-and-GitHub/GitHub-Actions-Matrix-options-dont-work-as-documented/td-p/29558" rel="noopener noreferrer"&gt;not exactly true yet&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;So, I had to take a detour and use the &lt;code&gt;exclude&lt;/code&gt; option:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;matrix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;ruby&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2.5.x"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2.6.x"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2.4.x"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;gemfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;
       &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemfiles/railsmaster.gemfile"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;
       &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemfiles/activerecord6.gemfile"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;
       &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemfiles/activerecord42.gemfile"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;
       &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemfiles/default_factory_girl.gemfile"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;
       &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemfiles/rspec35.gemfile"&lt;/span&gt;
    &lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;exclude&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;ruby&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2.6.x"&lt;/span&gt;
      &lt;span class="na"&gt;gemfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemfiles/activerecord42.gemfile"&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;ruby&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2.6.x"&lt;/span&gt;
      &lt;span class="na"&gt;gemfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemfiles/rspec35.gemfile"&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;ruby&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2.6.x"&lt;/span&gt;
      &lt;span class="na"&gt;gemfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemfiles/default_factory_girl.gemfile"&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;ruby&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2.5.x"&lt;/span&gt;
      &lt;span class="na"&gt;gemfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemfiles/railsmaster.gemfile"&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;ruby&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2.5.x"&lt;/span&gt;
      &lt;span class="na"&gt;gemfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemfiles/activerecord42.gemfile"&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;ruby&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2.5.x"&lt;/span&gt;
      &lt;span class="na"&gt;gemfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemfiles/rspec35.gemfile"&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;ruby&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2.5.x"&lt;/span&gt;
      &lt;span class="na"&gt;gemfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemfiles/default_factory_girl.gemfile"&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;ruby&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2.4.x"&lt;/span&gt;
      &lt;span class="na"&gt;gemfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemfiles/railsmaster.gemfile"&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;ruby&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2.4.x"&lt;/span&gt;
      &lt;span class="na"&gt;gemfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemfiles/activerecord6.gemfile"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This configuration works exactly the same as the one from Travis (except the missing JRuby, we'll discuss it later), but unfortunately is much less readable.&lt;/p&gt;

&lt;p&gt;Another thing I'd like to point out is that with GitHub Actions (compared to Travis) you have to deal with setting up a correct Gemfile yourself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Configure Gemfile&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;bundle config --global gemfile ${{ matrix.gemfile }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also, there is no &lt;code&gt;allow_failures&lt;/code&gt; option. There is a &lt;a href="https://help.github.com/en/articles/workflow-syntax-for-github-actions#jobsjob_idstepscontinue-on-error" rel="noopener noreferrer"&gt;&lt;code&gt;steps.continue-on-failure&lt;/code&gt;&lt;/a&gt; toggle which could be used to achieve something similar, but with the strategy matrix.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dealing with JRuby
&lt;/h3&gt;

&lt;p&gt;You cannot use JRuby with the &lt;code&gt;actions/setup-ruby&lt;/code&gt; action (or you can, but I couldn't find how?). It requires special treatment.&lt;/p&gt;

&lt;p&gt;Here I applied another GitHub Actions feature—ability to use Docker containers to perform actions. That makes it works very similar to CircleCI.&lt;/p&gt;

&lt;p&gt;I took the &lt;a href="https://hub.docker.com/_/jruby" rel="noopener noreferrer"&gt;official JRuby image&lt;/a&gt; and added a separate job:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;rspec-jruby&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
  &lt;span class="na"&gt;container&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;jruby:9.2.8&lt;/span&gt;
    &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;BUNDLE_GEMFILE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gemfiles/jruby.gemfile&lt;/span&gt;
  &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v1&lt;/span&gt;
  &lt;span class="c1"&gt;# I need git 'cause I use `git ls-files` in my .gemspec&lt;/span&gt;
  &lt;span class="c1"&gt;# to generate the list of the gem's files&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install git&lt;/span&gt;
    &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;apt-get update&lt;/span&gt;
      &lt;span class="s"&gt;apt-get install -y --no-install-recommends git&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install deps and run RSpec&lt;/span&gt;
    &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;gem install bundler&lt;/span&gt;
      &lt;span class="s"&gt;bundle install --jobs 4 --retry 3&lt;/span&gt;
      &lt;span class="s"&gt;bundle exec rspec&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And it just works!&lt;/p&gt;

&lt;h2&gt;
  
  
  Bonus: multiple badges
&lt;/h2&gt;

&lt;p&gt;After merging &lt;a href="https://github.com/palkan/test-prof/pull/156" rel="noopener noreferrer"&gt;the PR&lt;/a&gt; I've started looking for a way to add a build status badge to Readme (since I remove the old one from Travis).&lt;/p&gt;

&lt;p&gt;The answer was found on &lt;a href="https://www.reddit.com/r/github/comments/csehoc/github_actions_official_status_badges/" rel="noopener noreferrer"&gt;Reddit&lt;/a&gt; pretty soon: &lt;code&gt;https://github.com/{github_id}/{repository}/workflows/{workflow_name}/badge.svg&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;What's interesting here is that you have a separate badge for each workflow. That means that, for example, you shouldn't be afraid of a "red" status due to the yet-another minor RuboCop release.&lt;/p&gt;

&lt;p&gt;Another useful application of this "feature" is an ability to show that your library supports specific runtimes. For example, I split &lt;code&gt;rspec&lt;/code&gt; workflow into two parts: one for different MRI version and another for JRuby. Now it's clear from the README that TestProf has been tested on JRuby and it's (hopefully) green!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Frljeqyj1f935jkd54ro7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Frljeqyj1f935jkd54ro7.png" alt="Badges"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;GitHub Actions look very promising, especially for open source projects. The lack of some features (e.g., caching) would stop from using it as the primary CI/CD tool for commercial projects for now.&lt;/p&gt;

&lt;p&gt;But that is just the beginning; we will see what's coming in the final release!&lt;/p&gt;




&lt;p&gt;Read more dev articles on &lt;a href="https://evilmartians.com/chronicles" rel="noopener noreferrer"&gt;https://evilmartians.com/chronicles&lt;/a&gt;!&lt;/p&gt;

</description>
      <category>github</category>
      <category>ruby</category>
      <category>ci</category>
    </item>
    <item>
      <title>Keeping OSS documentation with Docsify, Lefthook, and friends</title>
      <dc:creator>Vladimir Dementyev</dc:creator>
      <pubDate>Mon, 02 Sep 2019 18:28:51 +0000</pubDate>
      <link>https://dev.to/evilmartians/keeping-oss-documentation-with-docsify-lefthook-and-friends-11e5</link>
      <guid>https://dev.to/evilmartians/keeping-oss-documentation-with-docsify-lefthook-and-friends-11e5</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;"A program is only as good as its documentation."—&lt;a href="https://en.wikipedia.org/wiki/Joe_Armstrong_(programmer)"&gt;Joe Armstrong&lt;/a&gt;, the author of the Erlang programming language&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;What makes a good open source project? If you are into Ruby, you can check out all the best practices in one place at &lt;a href="https://gemcheck.evilmartians.io"&gt;Gem Check&lt;/a&gt; (created by yours truly). But even regardless the language or the stack—one thing is vital for all OSS projects: documentation.&lt;/p&gt;


&lt;blockquote class="ltag__twitter-tweet"&gt;
      &lt;div class="ltag__twitter-tweet__media ltag__twitter-tweet__media__video-wrapper"&gt;
        &lt;div class="ltag__twitter-tweet__media--video-preview"&gt;
          &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--k5i619Xi--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://pbs.twimg.com/tweet_video_thumb/DHy7V8WVoAE2jt8.jpg" alt="unknown tweet media content"&gt;
          &lt;img src="/assets/play-butt.svg" class="ltag__twitter-tweet__play-butt" alt="Play butt"&gt;
        &lt;/div&gt;
        &lt;div class="ltag__twitter-tweet__video"&gt;
          
            
          
        &lt;/div&gt;
      &lt;/div&gt;

  &lt;div class="ltag__twitter-tweet__main"&gt;
    &lt;div class="ltag__twitter-tweet__header"&gt;
      &lt;img class="ltag__twitter-tweet__profile-image" src="https://res.cloudinary.com/practicaldev/image/fetch/s--lVAr5QzI--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://pbs.twimg.com/profile_images/761377629056307200/AOB3guut_normal.jpg" alt="Kelsey Hightower profile image"&gt;
      &lt;div class="ltag__twitter-tweet__full-name"&gt;
        Kelsey Hightower
      &lt;/div&gt;
      &lt;div class="ltag__twitter-tweet__username"&gt;
        @kelseyhightower
      &lt;/div&gt;
      &lt;div class="ltag__twitter-tweet__twitter-logo"&gt;
        &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--P4t6ys1m--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://practicaldev-herokuapp-com.freetls.fastly.net/assets/twitter-f95605061196010f91e64806688390eb1a4dbc9e913682e043eb8b1e06ca484f.svg" alt="twitter logo"&gt;
      &lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="ltag__twitter-tweet__body"&gt;
      So you built it. Nobody's using it. Did you forget the docs? Aha! 
    &lt;/div&gt;
    &lt;div class="ltag__twitter-tweet__date"&gt;
      01:08 AM - 22 Aug 2017
    &lt;/div&gt;


    &lt;div class="ltag__twitter-tweet__actions"&gt;
      &lt;a href="https://twitter.com/intent/tweet?in_reply_to=899800333462806529" class="ltag__twitter-tweet__actions__button"&gt;
        &lt;img src="/assets/twitter-reply-action.svg" alt="Twitter reply action"&gt;
      &lt;/a&gt;
      &lt;a href="https://twitter.com/intent/retweet?tweet_id=899800333462806529" class="ltag__twitter-tweet__actions__button"&gt;
        &lt;img src="/assets/twitter-retweet-action.svg" alt="Twitter retweet action"&gt;
      &lt;/a&gt;
      130
      &lt;a href="https://twitter.com/intent/like?tweet_id=899800333462806529" class="ltag__twitter-tweet__actions__button"&gt;
        &lt;img src="/assets/twitter-like-action.svg" alt="Twitter like action"&gt;
      &lt;/a&gt;
      440
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/blockquote&gt;


&lt;p&gt;All open source projects, independently of the size, &lt;em&gt;must&lt;/em&gt; be documented (even the now-deprecated, infamous &lt;a href="https://github.com/left-pad/left-pad"&gt;left-pad&lt;/a&gt; is documented). In most cases, a well-crafted README is more than enough, but you can go further and create an &lt;a href="https://github.com/matiassingers/awesome-readme"&gt;"awesome readme"&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;However, as the project grows, README-backed documentation stops working. We might say "&lt;del&gt;Rails&lt;/del&gt; README doesn't scale!". And we will be right.&lt;/p&gt;

&lt;p&gt;This brings us to a question: &lt;em&gt;"What scales better than a single markdown page?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In this text, I am sharing my answer to that.&lt;/p&gt;

&lt;h2&gt;
  
  
  In the Beginning, There Was Chaos
&lt;/h2&gt;

&lt;p&gt;Before we start talking about &lt;a href="https://docsify.js.org/"&gt;docsify&lt;/a&gt;, let me show you some other tools I used, before settling on &lt;em&gt;the one and only&lt;/em&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  GitHub Wiki
&lt;/h3&gt;

&lt;p&gt;My first project to outgrow its README was &lt;a href="https://anycable.io"&gt;AnyCable&lt;/a&gt;. Without giving it much thought, I started moving bits of documentation into the GitHub's built-in wiki (it's still &lt;a href="https://github.com/anycable/anycable/wiki"&gt;there&lt;/a&gt;, for older versions of the gem). If we already have the tool—that's the way to go, right? Well, not necessarily.&lt;/p&gt;

&lt;p&gt;It turned out that the fact that GitHub wiki is built-in is the only advantage, while the list of cons drags on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Updating code and docs independently is likely to lead to inconsistency.&lt;/li&gt;
&lt;li&gt;Web editor has much to be desired; you cannot easily upload images, for example (technically it is possible, but it requires &lt;a href="https://gist.github.com/subfuzion/0d3f19c4f780a7d75ba2"&gt;cloning the wiki repo&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;Cross-references are easy to break just by changing a title; there is no way to have a permanent URL (or I was missing something).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;docs&lt;/code&gt; folder
&lt;/h3&gt;

&lt;p&gt;One way to resolve all the Wiki weak points (see the pun?) at once is to create a &lt;code&gt;docs&lt;/code&gt; folder in the repo and populate it with markdown files. GitHub displays &lt;code&gt;.md&lt;/code&gt; files with formatting when you open them in your browser. And you can also edit the contents right from the web UI! If that's the case, why use the wiki feature at all?&lt;/p&gt;

&lt;p&gt;So, we found a good way to store the documentation contents. But what about the UI/UX? Don't we want to make our documentation more user-friendly (for example, add searching functionality)? Yes, we do.&lt;/p&gt;

&lt;p&gt;Let's convert our GitHub-driven docs to web format.&lt;/p&gt;

&lt;h3&gt;
  
  
  Jekyll &amp;amp; GitHub Pages
&lt;/h3&gt;

&lt;p&gt;GitHub helps with setting up a documentation website backed by &lt;a href="https://jekyllrb.com"&gt;Jekyll&lt;/a&gt; in a few clicks: go to "Settings" -&amp;gt; "GitHub Pages," choose the source for the website (we use the &lt;code&gt;docs&lt;/code&gt; folder) and pick a Jekyll theme to your liking.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--nlZT9lFh--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/l0qmpr7uz1kwazegguez.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--nlZT9lFh--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/l0qmpr7uz1kwazegguez.png" alt="GitHub Pages settings"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now visit the URL from the Settings page and find your brand new documentation website there!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--pabDUmAi--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/e8bvf2ckgga42lb4glwo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--pabDUmAi--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/e8bvf2ckgga42lb4glwo.png" alt="Jekyll Website Example"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Unfortunately, the GitHub Pages Jekyll integration is limited, especially in terms of &lt;a href="https://help.github.com/en/articles/configuring-jekyll-plugins"&gt;the available plugins&lt;/a&gt;. You cannot go far with it. And, in my opinion, Jekyll is a bit too complicated if you want to customize the looks of your page or add some interactivity.&lt;/p&gt;

&lt;p&gt;Let's check out &lt;em&gt;modern&lt;/em&gt; tools.&lt;/p&gt;

&lt;h3&gt;
  
  
  Docusaurus
&lt;/h3&gt;

&lt;p&gt;The first modern documentation generator I have tried was &lt;a href="https://docusaurus.io"&gt;Docusaurus&lt;/a&gt;. We built the &lt;a href="https://clowne.evilmartians.io"&gt;Clowne&lt;/a&gt;'s gem documentation with it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--FXWJeASg--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/rcgxyj4tdcxadxtek455.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--FXWJeASg--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/rcgxyj4tdcxadxtek455.png" alt="Clowne Documentation"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But I have to admit that the experience was not so pleasant:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Given the fact that the library is built with React, I expected it to have more straightforward customization options. However, you don't really get access to internals and library authors don't want you to do the serious tweaking.&lt;/li&gt;
&lt;li&gt;At the time of my first usage, it did not have the support for live reload, which is crucial for local development (now it seems to be &lt;a href="https://github.com/facebook/Docusaurus/issues/234"&gt;fixed&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;It requires a separate building step (&lt;code&gt;yarn run build&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I started looking for other options and found &lt;a href="https://docsify.js.org/"&gt;docsify&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Docsifying documentation
&lt;/h2&gt;

&lt;p&gt;Docsify uses a different approach, compared to Jekyll or Docusaurus, to "generating" a website: it &lt;em&gt;renders markdown files on the fly&lt;/em&gt;, and does not require a build phase.&lt;/p&gt;

&lt;p&gt;There is also support for &lt;a href="https://docsify.js.org/#/ssr"&gt;Server-side rendering&lt;/a&gt; and even for &lt;a href="https://docsify.js.org/#/pwa"&gt;offline mode&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;To "docsify" your &lt;code&gt;docs&lt;/code&gt;, you need to do the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Add &lt;code&gt;docs/.nojekyll&lt;/code&gt; file to disable Jekyll.&lt;/li&gt;
&lt;li&gt;Add &lt;code&gt;index.html&lt;/code&gt; that loads and configure docsify:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;http-equiv=&lt;/span&gt;&lt;span class="s"&gt;"X-UA-Compatible"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"IE=edge,chrome=1"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"viewport"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"width=device-width,initial-scale=1"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;charset=&lt;/span&gt;&lt;span class="s"&gt;"UTF-8"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"stylesheet"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"//unpkg.com/docsify/themes/vue.css"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"app"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
    &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;$docsify&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;loadSidebar&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;subMaxLevel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;palkan/docs-example&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;basePath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/docs-example/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;auto2top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;homepage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://raw.githubusercontent.com/palkan/docs-example/master/README.md&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"//unpkg.com/docsify/lib/docsify.min.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"//unpkg.com/prismjs/components/prism-bash.min.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"//unpkg.com/prismjs/components/prism-ruby.min.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;And that is it! Now you have something like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--D4nnoErV--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/efyauumzlep7586ityac.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--D4nnoErV--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/efyauumzlep7586ityac.png" alt="Simple Docsify example"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Note that I have added some specific configuration options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;basePath: '/docs-example/&lt;/code&gt; defines the root path of your website (which is the repo name for personal projects on GitHub Pages);&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;homepage: '...'&lt;/code&gt; is set to the repo's README (by default docsify uses the &lt;code&gt;docs/README.md&lt;/code&gt; file); that allows us to keep both home pages (GitHub and web) in sync.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And that's just the beginning! One of the main advantages of docsify is the simplicity of adding useful features via &lt;a href="https://docsify.js.org/#/plugins"&gt;plugins&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Let's add a searching functionality.&lt;/p&gt;

&lt;p&gt;All we need is to add these two lines of code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight diff"&gt;&lt;code&gt; window.$docsify = {
   loadSidebar: true,
   subMaxLevel: 2,
&lt;span class="gi"&gt;+  search: 'auto',
&lt;/span&gt;   repo: 'palkan/docs-example',
   basePath: '/docs-example/',
   auto2top: true,
   homepage: 'https://raw.githubusercontent.com/palkan/docs-example/master/README.md'
   }
 &amp;lt;/script&amp;gt;
 &amp;lt;script src="//unpkg.com/docsify/lib/docsify.min.js"&amp;gt;&amp;lt;/script&amp;gt;
 &amp;lt;script src="//unpkg.com/docsify/lib/plugins/search.min.js"&amp;gt;&amp;lt;/script&amp;gt;
 &amp;lt;script src="//unpkg.com/prismjs/components/prism-bash.min.js"&amp;gt;&amp;lt;/script&amp;gt;
 &amp;lt;script src="//unpkg.com/prismjs/components/prism-ruby.min.js"&amp;gt;&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;And &lt;em&gt;voilà&lt;/em&gt;! We can search through our documentation! The searching is implemented on the client side and is backed by indexes saved to &lt;code&gt;localStorage&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--p-6fIvcZ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/jnk8cljp5uceazqzj6zq.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--p-6fIvcZ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/jnk8cljp5uceazqzj6zq.gif" alt="docsify search demo"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Another thing I like about docsify is the ease of customizing styles: the library uses &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties"&gt;CSS properties&lt;/a&gt;, thus making it possible to change colors and layouts without building your own CSS!&lt;/p&gt;

&lt;p&gt;You can change the color and font sizes right in your &lt;code&gt;index.html&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;style&amp;gt;&lt;/span&gt;
  &lt;span class="nd"&gt;:root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="py"&gt;--theme-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#ff5e5e&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="py"&gt;--theme-color-light&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#fd7373&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="py"&gt;--theme-color-dark&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#f64242&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="py"&gt;--theme-color-secondary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#ff5e5e&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="py"&gt;--theme-color-secondary-dark&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#f64242&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="py"&gt;--theme-color-secondary-light&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#fd7373&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="py"&gt;--text-color-base&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#363636&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="py"&gt;--text-color-secondary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#646473&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
  ...
&lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--nldQPl-5--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/f84i794w13iczmy8w97i.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--nldQPl-5--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/f84i794w13iczmy8w97i.png" alt="Adding red styles"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Isn't it &lt;a href="https://docsify.js.org/#/awesome"&gt;awesome&lt;/a&gt;?&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The code for the example above could be found on GitHub: &lt;a href="https://github.com/palkan/docs-example"&gt;palkan/docs-example&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Check out &lt;a href="https://docs.anycable.io"&gt;AnyCable documentation&lt;/a&gt; website and &lt;a href="https://github.com/anycable/docs.anycable.io"&gt;the corresponding repo&lt;/a&gt; for a more advanced example!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--unLB2KcL--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/xzqpu3ukix9v9vnxulku.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--unLB2KcL--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/xzqpu3ukix9v9vnxulku.png" alt="AnyCable docs"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Bonus&lt;/em&gt;*: you can find the implementation of the floating action button for "Edit on GitHub" functionality in &lt;a href="https://gist.github.com/palkan/f702908151f5822bcf8d5daeb41e2f5f"&gt;this gist&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keeping docs healthy with linters
&lt;/h2&gt;

&lt;p&gt;In the second part of this tutorial, I would like to share my approach to keeping documentation in a healthy state. And by the "healthy state," I mean:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Style consistency for source files (Markdown) and code examples (Ruby).&lt;/li&gt;
&lt;li&gt;Correct spelling.&lt;/li&gt;
&lt;li&gt;Valid code examples (from the syntax point of view).&lt;/li&gt;
&lt;li&gt;Valid links (no link should lead to 4xx/5xx).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's not surprising that for all of the above there is an open source tool (and sometimes many).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/markdownlint/markdownlint"&gt;Markdownlint&lt;/a&gt; helps me to enforce Markdown files style (there is also &lt;a href="https://github.com/DavidAnson/markdownlint"&gt;a NodeJS version&lt;/a&gt; and a &lt;a href="https://github.com/DavidAnson/vscode-markdownlint"&gt;VS Code plugin&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;I also usually disable a couple of rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Line length (&lt;code&gt;MD013&lt;/code&gt;)—modern editors (such as VS Code) could handle this by wrapping long lines.&lt;/li&gt;
&lt;li&gt;HTML fragments—sometimes Markdown is not enough.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To do that I put a &lt;code&gt;.mdlrc&lt;/code&gt; file in the project's root with the following contents:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;rules &lt;span class="s2"&gt;"~MD013"&lt;/span&gt;, &lt;span class="s2"&gt;"~MD033"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;To deal with Ruby syntax I use &lt;a href="https://github.com/rubocop-hq/rubocop"&gt;RuboCop&lt;/a&gt; along with the &lt;a href="https://github.com/rubocop-hq/rubocop-md"&gt;rubocop-md&lt;/a&gt; plugin that I wrote specifically for this task. As a default style configuration, I've recently started using &lt;a href="https://github.com/testdouble/standard"&gt;standard&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;To make this setup work you need to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Install &lt;code&gt;standard&lt;/code&gt; and &lt;code&gt;rubocop-md&lt;/code&gt; gems (&lt;code&gt;gem install standard&lt;/code&gt; and &lt;code&gt;gem install rubocop-md&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Add a &lt;code&gt;.rubocop.yml&lt;/code&gt; with the following contents:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;require&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;standard/cop/semantic_blocks&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;rubocop-md&lt;/span&gt;

&lt;span class="na"&gt;inherit_gem&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;standard&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;config/base.yml&lt;/span&gt;

&lt;span class="s"&gt;Standard/SemanticBlocks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;Enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Run RuboCop.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For spellchecking, there is yet another Ruby tool—&lt;a href="https://github.com/kkuprikov/forspell"&gt;Forspell&lt;/a&gt;. It is a wrapper over a well-known &lt;a href="https://en.wikipedia.org/wiki/Hunspell"&gt;Hunspell&lt;/a&gt; package.&lt;/p&gt;

&lt;p&gt;Due to the number of technical terms, you may see a lot of warnings from Forspell during the first run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;forspell docs/

docs/development/lefthook.md:5: lefthook &lt;span class="o"&gt;(&lt;/span&gt;suggestions: left hook, left-hook, leftmost&lt;span class="o"&gt;)&lt;/span&gt;
docs/development/lefthook.md:9: lefthook &lt;span class="o"&gt;(&lt;/span&gt;suggestions: left hook, left-hook, leftmost&lt;span class="o"&gt;)&lt;/span&gt;
docs/development/lefthook.md:11: Hombrew &lt;span class="o"&gt;(&lt;/span&gt;suggestions: Hombre, Hombres, Hombre w&lt;span class="o"&gt;)&lt;/span&gt;
docs/development/lefthook.md:17: Golang &lt;span class="o"&gt;(&lt;/span&gt;suggestions: Golan, Golan g, Angolan&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;That could be easily fixed by running Forspell with the &lt;code&gt;--gen-dictionary&lt;/code&gt; flag: it generates a &lt;code&gt;forspell.dict&lt;/code&gt; file with all the unknown words. Don't forget to scan this file with your eyes and remove the actual typos.&lt;/p&gt;

&lt;p&gt;Finally, to make sure that our documentation does not have any broken links, I use &lt;a href="https://github.com/raviqqe/liche"&gt;liche&lt;/a&gt;—a link checker for Markdown and HTML written in Go:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;liche &lt;span class="nt"&gt;-r&lt;/span&gt; docs/

 ERROR    https://githb.com/palkan/anyway_config
                Dialing to the given TCP address timed out
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Liche lacks some features I wish it had: for example, it does not warn about URLs responding with 404. Nevertheless, I found it a bit better than other existing tools.&lt;/p&gt;

&lt;p&gt;In order to manage all these linters, I use &lt;a href="https://github.com/Arkweid/lefthook"&gt;Lefthook&lt;/a&gt; for local development and CircleCI for pull requests.&lt;/p&gt;

&lt;p&gt;Here is the contents of my &lt;code&gt;lefthook.yml&lt;/code&gt;, a file that stores Lefthook's &lt;a href="https://github.com/Arkweid/lefthook/blob/master/docs/full_guide.md"&gt;configuration&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;pre-commit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;commands&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;mdl&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;glob&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;**/*.md"&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mdl {staged_files}&lt;/span&gt;
    &lt;span class="na"&gt;liche&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;glob&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;**/*.md"&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;liche -r docs&lt;/span&gt;
    &lt;span class="na"&gt;forspell&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;glob&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;**/*.md"&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;forspell {staged_files}&lt;/span&gt;
    &lt;span class="na"&gt;rubocop&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;glob&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;**/*.md"&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rubocop {staged_files}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;CirclCI configuration is a little bit more verbose, but does pretty much the same:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2.1&lt;/span&gt;

&lt;span class="na"&gt;workflows&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
  &lt;span class="na"&gt;build_and_test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;checkout&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;md_lint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;requires&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;checkout&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;links_lint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;requires&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;checkout&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;spelling&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;requires&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;checkout&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;rubocop&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;requires&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;checkout&lt;/span&gt;

&lt;span class="na"&gt;executors&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;golang&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;docker&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;circleci/golang:1.12.4-stretch&lt;/span&gt;
  &lt;span class="na"&gt;ruby&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;docker&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;circleci/ruby:2.5-stretch&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;checkout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;executor&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ruby&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;restore_cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;keys&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;project-source-v1-{{ .Branch }}-{{ .Revision }}&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;project-source-v1-{{ .Branch }}&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;project-source-v1&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;checkout&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;save_cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;project-source-v1-{{ .Branch }}-{{ .Revision }}&lt;/span&gt;
          &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.git&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;persist_to_workspace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;root&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
          &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
  &lt;span class="na"&gt;md_lint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;executor&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ruby&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;attach_workspace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;at&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install mdl&lt;/span&gt;
          &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gem install mdl&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Markdown lint&lt;/span&gt;
          &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mdl docs&lt;/span&gt;
  &lt;span class="na"&gt;links_lint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;executor&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;golang&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;attach_workspace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;at&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install liche&lt;/span&gt;
          &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;go get -u github.com/raviqqe/liche&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Check links&lt;/span&gt;
          &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;liche -r docs&lt;/span&gt;
  &lt;span class="na"&gt;spelling&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;executor&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ruby&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;attach_workspace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;at&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install hunspell&lt;/span&gt;
          &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sudo apt-get install hunspell&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install forspell&lt;/span&gt;
          &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gem install forspell&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Check spelling&lt;/span&gt;
          &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;forspell docs/&lt;/span&gt;
  &lt;span class="na"&gt;rubocop&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;executor&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ruby&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;attach_workspace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;at&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install standard&lt;/span&gt;
          &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gem install standard&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install rubocop-md&lt;/span&gt;
          &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gem install rubocop-md&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Check Ruby style&lt;/span&gt;
          &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rubocop&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;The complete example can be found in the &lt;a href="https://github.com/anycable/docs.anycable.io/"&gt;docs.anycable.io&lt;/a&gt; repo (see PRs &lt;a href="https://github.com/anycable/docs.anycable.io/pull/14"&gt;#14&lt;/a&gt; and &lt;a href="https://github.com/anycable/docs.anycable.io/pull/15"&gt;#15&lt;/a&gt;).&lt;/p&gt;




&lt;p&gt;It took me a while to come with this setup (literally, years). Now I can spin up a new documentation website in minutes. Hope you found this article useful and will consider using a similar approach next time you need web-based docs for your projects.&lt;/p&gt;

&lt;p&gt;Documentation for the win!&lt;/p&gt;




&lt;p&gt;Read more dev articles on &lt;a href="https://evilmartians.com/chronicles"&gt;https://evilmartians.com/chronicles&lt;/a&gt;!&lt;/p&gt;

</description>
      <category>tutorial</category>
      <category>documentation</category>
      <category>docsify</category>
      <category>lefthook</category>
    </item>
    <item>
      <title>Ruby on Whales: Dockerizing Ruby and Rails development</title>
      <dc:creator>Vladimir Dementyev</dc:creator>
      <pubDate>Wed, 24 Jul 2019 19:27:13 +0000</pubDate>
      <link>https://dev.to/evilmartians/ruby-on-whales-dockerizing-ruby-and-rails-development-4dm7</link>
      <guid>https://dev.to/evilmartians/ruby-on-whales-dockerizing-ruby-and-rails-development-4dm7</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally posted in &lt;a href="https://evilmartians.com/chronicles/ruby-on-whales-docker-for-ruby-rails-development"&gt;Martian Chronicles&lt;/a&gt;&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This post is a b-side of my recent RailsConf talk "Terraforming legacy Rails applications" (&lt;a href="https://www.youtube.com/watch?v=-NKpMn6XSjU"&gt;video&lt;/a&gt;, &lt;a href="https://speakerdeck.com/palkan/railsconf-2019-terraforming-legacy-rails-applications"&gt;slides&lt;/a&gt;).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In this post, I am not going to convince you to switch to Docker for application development (though you can check the &lt;a href="https://www.youtube.com/watch?v=-NKpMn6XSjU"&gt;RailsConf video&lt;/a&gt; for some arguments). My goal is to share the configuration I currently use for Rails projects, and which was born in &lt;del&gt;production&lt;/del&gt; development at &lt;a href="https://evilmartians.com/"&gt;Evil Martians&lt;/a&gt;. Feel free to use it!&lt;/p&gt;

&lt;p&gt;I've started using Docker in my development environment about three years ago (instead of Vagrant which was too heavy for my 4GB RAM laptop). It wasn't all roses since the start, of course—I spent two years trying to find a configuration that is &lt;em&gt;good enough&lt;/em&gt;, suitable not only for myself but also for my team.&lt;/p&gt;

&lt;p&gt;Let me present this config here and explain (almost) every line of it, because we've all had enough of cryptic tutorials that just assume you &lt;em&gt;know stuff&lt;/em&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The source code could be found in the &lt;a href="https://github.com/evilmartians/terraforming-rails/blob/master/examples/dockerdev"&gt;evilmartians/terraforming-rails&lt;/a&gt; repository on GitHub.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We use the following stack in this example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ruby 2.6.3&lt;/li&gt;
&lt;li&gt;PostgreSQL 11&lt;/li&gt;
&lt;li&gt;NodeJS 11 &amp;amp; Yarn (for Webpacker-backed assets compilation)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://github.com/evilmartians/terraforming-rails/blob/master/examples/dockerdev/.dockerdev/Dockerfile"&gt;&lt;code&gt;Dockerfile&lt;/code&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Dockerfile&lt;/code&gt; defines the &lt;em&gt;environment&lt;/em&gt; for our Ruby application: this is where we run servers, console (&lt;code&gt;rails c&lt;/code&gt;), tests, Rake tasks, interact with our code in any way &lt;em&gt;as developers&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; RUBY_VERSION&lt;/span&gt;
&lt;span class="c"&gt;# See explanation below&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; ruby:$RUBY_VERSION&lt;/span&gt;

&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; PG_MAJOR&lt;/span&gt;
&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; NODE_MAJOR&lt;/span&gt;
&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; BUNDLER_VERSION&lt;/span&gt;
&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; YARN_VERSION&lt;/span&gt;

&lt;span class="c"&gt;# Add PostgreSQL to sources list&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;curl &lt;span class="nt"&gt;-sSL&lt;/span&gt; https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'deb http://apt.postgresql.org/pub/repos/apt/ stretch-pgdg main'&lt;/span&gt; &lt;span class="nv"&gt;$PG_MAJOR&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /etc/apt/sources.list.d/pgdg.list

&lt;span class="c"&gt;# Add NodeJS to sources list&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;curl &lt;span class="nt"&gt;-sL&lt;/span&gt; https://deb.nodesource.com/setup_&lt;span class="nv"&gt;$NODE_MAJOR&lt;/span&gt;.x | bash -

&lt;span class="c"&gt;# Add Yarn to the sources list&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;curl &lt;span class="nt"&gt;-sS&lt;/span&gt; https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'deb http://dl.yarnpkg.com/debian/ stable main'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /etc/apt/sources.list.d/yarn.list

&lt;span class="c"&gt;# Install dependencies&lt;/span&gt;
&lt;span class="c"&gt;# We use an external Aptfile for that, stay tuned&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; .dockerdev/Aptfile /tmp/Aptfile&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get update &lt;span class="nt"&gt;-qq&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;DEBIAN_FRONTEND&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;noninteractive apt-get &lt;span class="nt"&gt;-yq&lt;/span&gt; dist-upgrade &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="nv"&gt;DEBIAN_FRONTEND&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;noninteractive apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-yq&lt;/span&gt; &lt;span class="nt"&gt;--no-install-recommends&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    build-essential &lt;span class="se"&gt;\
&lt;/span&gt;    postgresql-client-&lt;span class="nv"&gt;$PG_MAJOR&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    nodejs &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nv"&gt;yarn&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$YARN_VERSION&lt;/span&gt;&lt;span class="nt"&gt;-1&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /tmp/Aptfile | xargs&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    apt-get clean &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /var/lib/apt/lists/&lt;span class="k"&gt;*&lt;/span&gt; /tmp/&lt;span class="k"&gt;*&lt;/span&gt; /var/tmp/&lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nb"&gt;truncate&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; 0 /var/log/&lt;span class="k"&gt;*&lt;/span&gt;log

&lt;span class="c"&gt;# Configure bundler and PATH&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; LANG=C.UTF-8 \&lt;/span&gt;
  GEM_HOME=/bundle \
  BUNDLE_JOBS=4 \
  BUNDLE_RETRY=3
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; BUNDLE_PATH $GEM_HOME&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; BUNDLE_APP_CONFIG=$BUNDLE_PATH \&lt;/span&gt;
  BUNDLE_BIN=$BUNDLE_PATH/bin
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; PATH /app/bin:$BUNDLE_BIN:$PATH&lt;/span&gt;

&lt;span class="c"&gt;# Upgrade RubyGems and install required Bundler version&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;gem update &lt;span class="nt"&gt;--system&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    gem &lt;span class="nb"&gt;install &lt;/span&gt;bundler:&lt;span class="nv"&gt;$BUNDLER_VERSION&lt;/span&gt;

&lt;span class="c"&gt;# Create a directory for the app code&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /app

&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;This configuration contains the essentials only and could be used as a starting point. Let me show what we are doing here.&lt;/p&gt;

&lt;p&gt;The first two lines could look a bit strange:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; RUBY_VERSION&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; ruby:$RUBY_VERSION&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Why not just &lt;code&gt;FROM ruby:2.6.3&lt;/code&gt;, or whatever Ruby stable version du jour it is? We want to make our environment configurable from the outside using Dockerfile as a sort of a template:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the exact versions of runtime dependencies are specified in the &lt;code&gt;docker-compose.yml&lt;/code&gt; (see below);&lt;/li&gt;
&lt;li&gt;the list of &lt;code&gt;apt&lt;/code&gt;-installable dependencies is stored in a separate file (also see below).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The following three lines define arguments for PostgreSQL, NodeJS, Yarn, and Bundler versions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; PG_MAJOR&lt;/span&gt;
&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; NODE_MAJOR&lt;/span&gt;
&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; BUNDLER_VERSION&lt;/span&gt;
&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; YARN_VERSION&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Since we do not expect anyone to use this Dockerfile without &lt;a href="https://docs.docker.com/compose/"&gt;Docker Compose&lt;/a&gt;, we do not provide default values.&lt;/p&gt;

&lt;p&gt;Installing PostgreSQL, NodeJS, Yarn via &lt;code&gt;apt&lt;/code&gt; requires adding their deb packages repos to the sources list.&lt;/p&gt;

&lt;p&gt;For PostgreSQL (based in the &lt;a href="https://www.postgresql.org/download/linux/debian/"&gt;official documentation&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;RUN &lt;/span&gt;curl &lt;span class="nt"&gt;-sSL&lt;/span&gt; https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'deb http://apt.postgresql.org/pub/repos/apt/ stretch-pgdg main'&lt;/span&gt; &lt;span class="nv"&gt;$PG_MAJOR&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /etc/apt/sources.list.d/pgdg.list
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;For NodeJS (from &lt;a href="https://github.com/nodesource/distributions/blob/master/README.md#debinstall"&gt;NodeSource repo&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;RUN &lt;/span&gt;curl &lt;span class="nt"&gt;-sL&lt;/span&gt; https://deb.nodesource.com/setup_&lt;span class="nv"&gt;$NODE_MAJOR&lt;/span&gt;.x | bash -
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;For Yarn (from the &lt;a href="https://yarnpkg.com/en/docs/install#debian-stable"&gt;official website&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;RUN &lt;/span&gt;curl &lt;span class="nt"&gt;-sS&lt;/span&gt; https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'deb http://dl.yarnpkg.com/debian/ stable main'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /etc/apt/sources.list.d/yarn.list
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Now it's time to install the dependencies, i.e. run &lt;code&gt;apt-get install&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; .dockerdev/Aptfile /tmp/Aptfile&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get update &lt;span class="nt"&gt;-qq&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;DEBIAN_FRONTEND&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;noninteractive apt-get &lt;span class="nt"&gt;-yq&lt;/span&gt; dist-upgrade &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="nv"&gt;DEBIAN_FRONTEND&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;noninteractive apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-yq&lt;/span&gt; &lt;span class="nt"&gt;--no-install-recommends&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    build-essential &lt;span class="se"&gt;\
&lt;/span&gt;    postgresql-client-&lt;span class="nv"&gt;$PG_MAJOR&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    nodejs &lt;span class="se"&gt;\
&lt;/span&gt;    yarn &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /tmp/Aptfile | xargs&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    apt-get clean &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /var/lib/apt/lists/&lt;span class="k"&gt;*&lt;/span&gt; /tmp/&lt;span class="k"&gt;*&lt;/span&gt; /var/tmp/&lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nb"&gt;truncate&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; 0 /var/log/&lt;span class="k"&gt;*&lt;/span&gt;log
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;First, let's talk about the Aptfile trick:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; .dockerdev/Aptfile /tmp/Aptfile&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /tmp/Aptfile | xargs&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;I borrowed this idea from &lt;a href="https://github.com/heroku/heroku-buildpack-apt"&gt;heroku-buildpack-apt&lt;/a&gt;, which allows installing additional packages on Heroku. If you're using this buildpack, you can even re-use the same Aptfile for local and production environment (though the buildpack's one provides more functionality).&lt;/p&gt;

&lt;p&gt;Our &lt;a href="https://github.com/evilmartians/terraforming-rails/blob/master/examples/dockerdev/.dockerdev/Aptfile"&gt;default Aptfile&lt;/a&gt; contains only a single package (we use Vim to edit Rails Credentials):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;vim
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;In one of the previous project I worked on, we generated PDFs using LaTeX and &lt;a href="https://www.tug.org/texlive/"&gt;TexLive&lt;/a&gt;. Our Aptfile might look like this (those days I didn't use this trick):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;vim
texlive
texlive-latex-recommended
texlive-fonts-recommended
texlive-lang-cyrillic
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;This way, we keep the task-specific dependencies in a separate file, making our Dockerfile more universal.&lt;/p&gt;

&lt;p&gt;With regards to &lt;code&gt;DEBIAN_FRONTEND=noninteractive&lt;/code&gt;, I kindly ask you to take a look at &lt;a href="https://askubuntu.com/a/972528"&gt;answer on Ask Ubuntu&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;--no-install-recommends&lt;/code&gt; switch helps us to save some space (and make our image slimmer) by not installing recommended packages. See more &lt;a href="http://xubuntugeek.blogspot.com/2012/06/save-disk-space-with-apt-get-option-no.html"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The last part of this &lt;code&gt;RUN&lt;/code&gt; (&lt;code&gt;apt-get clean &amp;amp;&amp;amp; rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* &amp;amp;&amp;amp; truncate -s 0 /var/log/*log&lt;/code&gt;) also serves the same purpose—clears out the local repository of retrieved package files (we installed everything, we don't need them anymore) and all the temporary files and logs created during the installation. We need this cleanup to be in the same &lt;code&gt;RUN&lt;/code&gt; statement to make sure this particular &lt;a href="https://docs.docker.com/storage/storagedriver/#images-and-layers"&gt;Docker layer&lt;/a&gt; doesn't contain any garbage.&lt;/p&gt;

&lt;p&gt;The final part is mostly devoted to Bundler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; LANG=C.UTF-8 \&lt;/span&gt;
  GEM_HOME=/bundle \
  BUNDLE_JOBS=4 \
  BUNDLE_RETRY=3
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; BUNDLE_PATH $GEM_HOME&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; BUNDLE_APP_CONFIG=$BUNDLE_PATH \&lt;/span&gt;
  BUNDLE_BIN=$BUNDLE_PATH/bin
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; PATH /app/bin:$BUNDLE_BIN:$PATH&lt;/span&gt;

&lt;span class="c"&gt;# Upgrade RubyGems and install required Bundler version&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;gem update &lt;span class="nt"&gt;--system&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    gem &lt;span class="nb"&gt;install &lt;/span&gt;bundler:&lt;span class="nv"&gt;$BUNDLER_VERSION&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;LANG=C.UTF-8&lt;/code&gt; sets the default locale to UTF-8. Otherwise Ruby uses US-ASCII for strings and bye-bye those sweet sweet emojis 👋&lt;/p&gt;

&lt;p&gt;We set the path for gem installations via &lt;code&gt;GEM_HOME=/bundle&lt;/code&gt;. What is &lt;code&gt;/bundle&lt;/code&gt;? That's the path where we're going to mount as a &lt;em&gt;volume&lt;/em&gt; later to persist the dependencies on the host system, i.e., your development machine (see below in &lt;code&gt;docker-compose.yml&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;BUNDLE_PATH&lt;/code&gt; and &lt;code&gt;BUNDLE_BIN&lt;/code&gt; variables tell Bundler where to look for gems and Ruby executables.&lt;/p&gt;

&lt;p&gt;Finally, we expose Ruby and application binaries globally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; PATH /app/bin:$BUNDLE_BIN:$PATH&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;That allows us to run &lt;code&gt;rails&lt;/code&gt;, &lt;code&gt;rake&lt;/code&gt;, &lt;code&gt;rspec&lt;/code&gt; and other &lt;em&gt;binstubbed&lt;/em&gt; commands without prefixing them with &lt;code&gt;bundle exec&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://github.com/evilmartians/terraforming-rails/blob/master/examples/dockerdev/docker-compose.yml"&gt;&lt;code&gt;docker-compose.yml&lt;/code&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://docs.docker.com/compose/"&gt;Docker Compose&lt;/a&gt; is a tool to orchestrate our containerized environment. It allows us to link containers to each other, define persistent volumes and services.&lt;/p&gt;

&lt;p&gt;Below is the compose file for a typical Rails application development with PostgreSQL as a database, and Sidekiq background job processor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.4'&lt;/span&gt;

&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nl"&gt;&amp;amp;app&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
      &lt;span class="na"&gt;dockerfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./.dockerdev/Dockerfile&lt;/span&gt;
      &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;RUBY_VERSION&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;2.6.3'&lt;/span&gt;
        &lt;span class="na"&gt;PG_MAJOR&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;11'&lt;/span&gt;
        &lt;span class="na"&gt;NODE_MAJOR&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;11'&lt;/span&gt;
        &lt;span class="na"&gt;YARN_VERSION&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;1.13.0'&lt;/span&gt;
        &lt;span class="na"&gt;BUNDLER_VERSION&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;2.0.2'&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;example-dev:1.0.0&lt;/span&gt;
    &lt;span class="na"&gt;tmpfs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/tmp&lt;/span&gt;

  &lt;span class="na"&gt;backend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nl"&gt;&amp;amp;backend&lt;/span&gt;
    &lt;span class="s"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;*app&lt;/span&gt;
    &lt;span class="na"&gt;stdin_open&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;tty&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.:/app:cached&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;rails_cache:/app/tmp/cache&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;bundle:/bundle&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;node_modules:/app/node_modules&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;packs:/app/public/packs&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.dockerdev/.psqlrc:/root/.psqlrc:ro&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;NODE_ENV=development&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;RAILS_ENV=${RAILS_ENV:-development}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;REDIS_URL=redis://redis:6379/&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DATABASE_URL=postgres://postgres:postgres@postgres:5432&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;BOOTSNAP_CACHE_DIR=/bundle/bootsnap&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;WEBPACKER_DEV_SERVER_HOST=webpacker&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;WEB_CONCURRENCY=1&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;HISTFILE=/app/log/.bash_history&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;PSQL_HISTFILE=/app/log/.psql_history&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;EDITOR=vi&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;redis&lt;/span&gt;

  &lt;span class="na"&gt;runner&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="s"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;*backend&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/bin/bash&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3000:3000'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3002:3002'&lt;/span&gt;

  &lt;span class="na"&gt;rails&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="s"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;*backend&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bundle exec rails server -b 0.0.0.0&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3000:3000'&lt;/span&gt;

  &lt;span class="na"&gt;sidekiq&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="s"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;*backend&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bundle exec sidekiq -C config/sidekiq.yml&lt;/span&gt;

  &lt;span class="na"&gt;postgres&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:11.1&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.psqlrc:/root/.psqlrc:ro&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;postgres:/var/lib/postgresql/data&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./log:/root/log:cached&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;PSQL_HISTFILE=/root/log/.psql_history&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="m"&gt;5432&lt;/span&gt;

  &lt;span class="na"&gt;redis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis:3.2-alpine&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;redis:/data&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="m"&gt;6379&lt;/span&gt;

  &lt;span class="na"&gt;webpacker&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="s"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;*app&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./bin/webpack-dev-server&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3035:3035'&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.:/app:cached&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;bundle:/bundle&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;node_modules:/app/node_modules&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;packs:/app/public/packs&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;NODE_ENV=${NODE_ENV:-development}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;RAILS_ENV=${RAILS_ENV:-development}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;WEBPACKER_DEV_SERVER_HOST=0.0.0.0&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;postgres&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;redis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;bundle&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;node_modules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;rails_cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;packs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;We define &lt;strong&gt;eight&lt;/strong&gt; services. Why so many? Some of them only define shared configuration for others (&lt;em&gt;abstract&lt;/em&gt; services, e.g., &lt;code&gt;app&lt;/code&gt; and &lt;code&gt;backend&lt;/code&gt;), others are used to specific commands using the application container (e.g., &lt;code&gt;runner&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;With this approach, we do not use &lt;code&gt;docker-compose up&lt;/code&gt; command to run our application, but always specify the exact service we want to run (e.g., &lt;code&gt;docker-compose up rails&lt;/code&gt;). That makes sense: in development, you rarely need all of the services up and running (Webpacker, Sidekiq, etc.).&lt;/p&gt;

&lt;p&gt;Let's take a thorough look at each service.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;app&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The main purpose of this service is to provide all the required information to build our application container (the one defined in the &lt;code&gt;Dockerfile&lt;/code&gt; above):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
  &lt;span class="na"&gt;dockerfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./.dockerdev/Dockerfile&lt;/span&gt;
  &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;RUBY_VERSION&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;2.6.3'&lt;/span&gt;
    &lt;span class="na"&gt;PG_MAJOR&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;11'&lt;/span&gt;
    &lt;span class="na"&gt;NODE_MAJOR&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;11'&lt;/span&gt;
    &lt;span class="na"&gt;YARN_VERSION&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;1.13.0'&lt;/span&gt;
    &lt;span class="na"&gt;BUNDLER_VERSION&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;2.0.2'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;context&lt;/code&gt; directory defines the &lt;a href="https://docs.docker.com/compose/compose-file/#context"&gt;build context&lt;/a&gt; for Docker: this is something like a working directory for the build process, it's used by the &lt;code&gt;COPY&lt;/code&gt; command, for example.&lt;/p&gt;

&lt;p&gt;We explicitly specify the path to Dockerfile since we do not keep it in the project root, packing all Docker-related files inside a hidden &lt;code&gt;.dockerdev&lt;/code&gt; directory.&lt;/p&gt;

&lt;p&gt;And, as we mentioned earlier, we specify the exact version of dependencies using &lt;code&gt;args&lt;/code&gt; declared in the Dockerfile.&lt;/p&gt;

&lt;p&gt;One thing that we should pay attention to is the way we tag images:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;example-dev:1.0.0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;One of the benefits of using Docker for development is the ability to synchronize the configuration changes across the team automatically. You only need to upgrade the local image version every time you make changes to it (or to the arguments or files it relies on). The worst thing you can do is to use &lt;code&gt;example-dev:latest&lt;/code&gt; as your build tag.&lt;/p&gt;

&lt;p&gt;Keeping an image version also helps to work with two different environments without any additional hassle. For example, when you work on a long-running "chore/upgrade-to-ruby-3" branch, you can easily switch to &lt;code&gt;master&lt;/code&gt; and use the older image with the older Ruby, no need to rebuild anything.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The worst thing you can do is to use &lt;code&gt;latest&lt;/code&gt; tags for images in your &lt;code&gt;docker-compose.yml&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We also &lt;em&gt;tell&lt;/em&gt; Docker to &lt;a href="https://docs.docker.com/v17.09/engine/admin/volumes/tmpfs/#choosing-the-tmpfs-or-mount-flag"&gt;use tmpfs&lt;/a&gt; for &lt;code&gt;/tmp&lt;/code&gt; folder within a container to speed things up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;tmpfs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/tmp&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;backend&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;We reached the most interesting part of this post.&lt;/p&gt;

&lt;p&gt;This service defines the shared behavior of all  Ruby services.&lt;/p&gt;

&lt;p&gt;Let's talk about the volumes first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.:/app:cached&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;bundle:/bundle&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;rails_cache:/app/tmp/cache&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;node_modules:/app/node_modules&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;packs:/app/public/packs&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.dockerdev/.psqlrc:/root/.psqlrc:ro&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;The first item in the volumes list mounts the current working directory (the project's root) to the &lt;code&gt;/app&lt;/code&gt; folder within a container using the &lt;code&gt;cached&lt;/code&gt; strategy. This &lt;code&gt;cached&lt;/code&gt; modifier is the key to efficient Docker development on MacOS. We're not going to dig deeper in this post (we're working on a separate one on this subject 😉), but you can take a look at &lt;a href="https://docs.docker.com/docker-for-mac/osxfs-caching/"&gt;the docs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The next line &lt;em&gt;tells&lt;/em&gt; our container to use a volume named &lt;code&gt;bundle&lt;/code&gt; to store &lt;code&gt;/bundle&lt;/code&gt; contents. This way we persist our gems data across runs: all the volumes defined in the &lt;code&gt;docker-compose.yml&lt;/code&gt; stay put until we run &lt;code&gt;docker-compose down --volumes&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The following three lines are also there to get rid of the "Docker is slow on Mac" curse. We put all the generated files into Docker volumes to avoid heavy disk operations on the host machine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;rails_cache:/app/tmp/cache&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;node_modules:/app/node_modules&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;packs:/app/public/packs&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;To make Docker fast enough on MacOS follow these two rules: use &lt;code&gt;:cached&lt;/code&gt; to mount source files and use volumes for generated content (assets, bundle, etc.).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The last line adds a specific &lt;code&gt;psql&lt;/code&gt; configuration to the container. We mostly need it to persist the commands history by storing it in the app's &lt;code&gt;log/.psql_history&lt;/code&gt; file. Why &lt;code&gt;psql&lt;/code&gt; in the Ruby container? It's used internally when you run &lt;code&gt;rails dbconsole&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Our &lt;a href="https://github.com/evilmartians/terraforming-rails/blob/master/examples/dockerdev/.dockerdev/.psqlrc"&gt;&lt;code&gt;.psqlrc&lt;/code&gt;&lt;/a&gt; file contains the following trick to make it possible to specify the path to the history file via the env variable (allow specifying the path to history file via &lt;code&gt;PSQL_HISTFILE&lt;/code&gt; env variable, and fallback to the defaukt &lt;code&gt;$HOME/.psql_history&lt;/code&gt; otherwise):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="n"&gt;HISTFILE&lt;/span&gt; &lt;span class="nv"&gt;`[[ -z $PSQL_HISTFILE ]] &amp;amp;&amp;amp; echo $HOME/.psql_history || echo $PSQL_HISTFILE`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Let's talk about the environment variables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;NODE_ENV=${NODE_ENV:-development}&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;RAILS_ENV=${RAILS_ENV:-development}&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;REDIS_URL=redis://redis:6379/&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DATABASE_URL=postgres://postgres:postgres@postgres:5432&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;WEBPACKER_DEV_SERVER_HOST=webpacker&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;BOOTSNAP_CACHE_DIR=/bundle/bootsnap&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;HISTFILE=/app/log/.bash_history&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;PSQL_HISTFILE=/app/log/.psql_history&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;EDITOR=vi&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;MALLOC_ARENA_MAX=2&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;WEB_CONCURRENCY=${WEB_CONCURRENCY:-1}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;There are several things here, and I'd like to focus one.&lt;/p&gt;

&lt;p&gt;First, the &lt;code&gt;X=${X:-smth}&lt;/code&gt; syntax. It could be translated as "For X variable within the container use the host machine X env variable value if present and another value otherwise". &lt;em&gt;Thus, we make it possible to run a service in a different environment provided along with the command, e.g., &lt;code&gt;RAILS_ENV=test docker-compose up rails&lt;/code&gt;&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;DATABASE_URL&lt;/code&gt;, &lt;code&gt;REDIS_URL&lt;/code&gt;, and &lt;code&gt;WEBPACKER_DEV_SERVER_HOST&lt;/code&gt; variables &lt;em&gt;connect&lt;/em&gt; our Ruby application to other services. The &lt;code&gt;DATABASE_URL&lt;/code&gt; and &lt;code&gt;WEBPACKER_DEV_SERVER_HOST&lt;/code&gt; variables are supported by Rails (ActiveRecord and Webpacker respectively) out-of-the-box. Some libraries support &lt;code&gt;REDIS_URL&lt;/code&gt; as well (Sidekiq) but not all of them (for instance, Action Cable must be configured explicitly).&lt;/p&gt;

&lt;p&gt;We use &lt;a href="https://www.github.com/Shopify/bootsnap"&gt;bootsnap&lt;/a&gt; to speed up the application load time. We store its cache in the same volume as the Bundler data because this cache mostly contains the gems data; thus, we should drop everything altogether in case we do another Ruby version upgrade, for instance.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;HISTFILE=/app/log/.bash_history&lt;/code&gt; is the significant setting from the developer's UX point of view: it tells Bash to store its history in the specified location, thus making it persistent.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;EDITOR=vi&lt;/code&gt; is used, for example, by &lt;code&gt;rails credentials:edit&lt;/code&gt; command to manage credentials files.&lt;/p&gt;

&lt;p&gt;Finally, the last two settings, &lt;code&gt;MALLOC_ARENA_MAX&lt;/code&gt; and &lt;code&gt;WEB_CONCURRENCY&lt;/code&gt;, are there to help you keep Rails memory handling in check.&lt;/p&gt;

&lt;p&gt;The only lines in this service yet to cover are:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;stdin_open&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
&lt;span class="na"&gt;tty&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;They make this service &lt;em&gt;interactive&lt;/em&gt;, i.e., provide a TTY. We need it, for example, to run Rails console or Bash within a container.&lt;/p&gt;

&lt;p&gt;It is the same as running a Docker container with the &lt;code&gt;-it&lt;/code&gt; options.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;webpacker&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The only thing I want to mention here is the &lt;code&gt;WEBPACKER_DEV_SERVER_HOST=0.0.0.0&lt;/code&gt; setting: it makes Webpack dev server accessible from the &lt;em&gt;outside&lt;/em&gt; (by default it runs on &lt;code&gt;localhost&lt;/code&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;runner&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;To explain what is this service for, let me share the way I use Docker for development:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I start a Docker daemon running a custom &lt;code&gt;docker-start&lt;/code&gt; script:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/sh&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;docker info &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null 2&amp;gt;&amp;amp;1&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Opening Docker for Mac..."&lt;/span&gt;
  open &lt;span class="nt"&gt;-a&lt;/span&gt; /Applications/Docker.app
  &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; docker system info &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null 2&amp;gt;&amp;amp;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do &lt;/span&gt;&lt;span class="nb"&gt;sleep &lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;done
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Docker is ready to rock!"&lt;/span&gt;
&lt;span class="k"&gt;else
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Docker is up and running."&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Then I run &lt;code&gt;dcr runner&lt;/code&gt; (&lt;code&gt;dcr&lt;/code&gt; is an alias for &lt;code&gt;docker-compose run&lt;/code&gt;) in the project directory to log into the container's shell; this is an alias for:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker-compose run &lt;span class="nt"&gt;--rm&lt;/span&gt; runner
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;I run (almost) everything from within this container: tests, migrations, Rake tasks, whatever.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As you can see, I do not spin a new container every time I need to run a task, and I'm always using the same one.&lt;/p&gt;

&lt;p&gt;Thus, I'm using &lt;code&gt;dcr runner&lt;/code&gt; the same way I used &lt;code&gt;vagrant ssh&lt;/code&gt; years ago.&lt;/p&gt;

&lt;p&gt;The only reason why it's called &lt;code&gt;runner&lt;/code&gt; and not &lt;code&gt;shell&lt;/code&gt;, for example, is that it also could be used to &lt;em&gt;run&lt;/em&gt; arbitrary commands within a container.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: The &lt;code&gt;runner&lt;/code&gt; service is a matter of taste, it doesn't bring anything new comparing to the &lt;code&gt;web&lt;/code&gt; service, except from the default &lt;code&gt;command&lt;/code&gt; (&lt;code&gt;/bin/bash&lt;/code&gt;); thus, &lt;code&gt;docker-compose run runner&lt;/code&gt; is exactly the same as &lt;code&gt;docker-compose run web /bin/bash&lt;/code&gt; (but shorter 😉).&lt;/p&gt;

&lt;h2&gt;
  
  
  Bonus: &lt;a href="https://github.com/evilmartians/terraforming-rails/blob/master/examples/dockerdev/dip.yml"&gt;dip.yml&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;If you still think that the &lt;em&gt;Docker Compose&lt;/em&gt; way is too complicated, there is a tool called &lt;a href="https://github.com/bibendi/dip"&gt;Dip&lt;/a&gt; developed by one of my colleages at Evil Martians that aims to make the developer experience smoother.&lt;/p&gt;

&lt;p&gt;It is especially useful if you have multiple compose files or platform-dependent configurations because it could glue them together and provide a universal interface to manage the Docker development environment.&lt;/p&gt;

&lt;p&gt;We're going to tell you more about it in the future. Stay tuned!&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;P.S.&lt;/strong&gt; Special thanks to &lt;a href="https://github.com/sponomarev"&gt;Sergey Ponomarev&lt;/a&gt; and &lt;a href="https://github.com/bibendi"&gt;Mikhail Merkushin&lt;/a&gt; for sharing their tips on the subject. 🤘&lt;/p&gt;




&lt;p&gt;Read more dev articles on &lt;a href="https://evilmartians.com/chronicles"&gt;https://evilmartians.com/chronicles&lt;/a&gt;!&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>rails</category>
      <category>docker</category>
    </item>
  </channel>
</rss>
