<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://ericproulx.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://ericproulx.github.io/" rel="alternate" type="text/html" /><updated>2025-01-02T23:28:22+00:00</updated><id>https://ericproulx.github.io/feed.xml</id><title type="html">Rick on Rails</title><subtitle>Sharing my experience on Ruby on Rails</subtitle><entry><title type="html">Simple Docker Setup for Ruby development</title><link href="https://ericproulx.github.io/2024/12/19/simple-docker-setup-for-ruby-development.html" rel="alternate" type="text/html" title="Simple Docker Setup for Ruby development" /><published>2024-12-19T00:00:00+00:00</published><updated>2024-12-19T00:00:00+00:00</updated><id>https://ericproulx.github.io/2024/12/19/simple-docker-setup-for-ruby-development</id><content type="html" xml:base="https://ericproulx.github.io/2024/12/19/simple-docker-setup-for-ruby-development.html"><![CDATA[<p>When forking a project, I like having a simple Docker setup to quickly start.</p>

<p>First, I create a <code class="language-plaintext highlighter-rouge">docker</code> folder and I add the following files in it:</p>
<ul>
  <li><a href="#dockerfile">Dockerfile</a></li>
  <li><a href="#entrypointsh">entrypoint.sh</a></li>
</ul>

<p>Then, I add a <a href="#docker-composeyml">docker-compose.yml</a> in the root folder of the project.
Simply run:</p>

<p><code class="language-plaintext highlighter-rouge">docker compose run --rm cli &lt;your_bundle_exec_command&gt;</code></p>

<p>⚠️ The image will be built only at the first execution and subsequent calls won’t automatically rebuild the initial image.
If your making changes along the way you need to add the <code class="language-plaintext highlighter-rouge">--build</code> option:</p>

<p><code class="language-plaintext highlighter-rouge">docker compose run --build --rm cli &lt;your_bundle_exec_command&gt;</code></p>

<h1 id="dockerfile">Dockerfile</h1>

<div class="language-dockerfile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">ARG</span><span class="s"> RUBY_VERSION=3</span>
<span class="k">FROM</span><span class="s"> ruby:${RUBY_VERSION}-alpine</span>

<span class="k">ENV</span><span class="s"> BUNDLE_PATH /usr/local/bundle/gems</span>
<span class="k">ENV</span><span class="s"> LIB_PATH /var/gem</span>
<span class="c"># --enable-frozen-string-literal maybe not be suitable for all projects </span>
<span class="k">ENV</span><span class="s"> RUBYOPT --enable-frozen-string-literal --yjit</span>
<span class="c"># taken from dockerfile-rails https://github.com/fly-apps/dockerfile-rails/blob/f34fab26ae3b77a14c15b4bafb0fb91f911b8d8a/lib/generators/dockerfile_generator.rb#L981-L983</span>
<span class="k">ENV</span><span class="s"> LD_PRELOAD libjemalloc.so.2</span>
<span class="k">ENV</span><span class="s"> MALLOC_CONF dirty_decay_ms:1000,narenas:2,background_thread:true</span>

<span class="c"># these packages should cover the basics but you might need more</span>
<span class="k">RUN </span>apk add <span class="nt">--update</span> <span class="nt">--no-cache</span> make g++ gcc git libc-dev gcompat jemalloc <span class="o">&amp;&amp;</span> <span class="se">\
</span>    gem update <span class="nt">--system</span> <span class="o">&amp;&amp;</span> gem <span class="nb">install </span>bundler

<span class="k">WORKDIR</span><span class="s"> $LIB_PATH</span>

<span class="k">COPY</span><span class="s"> /docker/entrypoint.sh /usr/local/bin/docker-entrypoint.sh</span>
<span class="k">RUN </span><span class="nb">chmod</span> +x /usr/local/bin/docker-entrypoint.sh

<span class="k">ENTRYPOINT</span><span class="s"> ["docker-entrypoint.sh"]</span>
</code></pre></div></div>

<h1 id="entrypointsh">entrypoint.sh</h1>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/sh</span>

<span class="nb">set</span> <span class="nt">-e</span>

<span class="c"># Useful information</span>
<span class="nb">echo</span> <span class="nt">-e</span> <span class="s2">"</span><span class="si">$(</span>ruby <span class="nt">--version</span><span class="si">)</span><span class="se">\n</span><span class="s2">rubygems </span><span class="si">$(</span>gem <span class="nt">--version</span><span class="si">)</span><span class="se">\n</span><span class="si">$(</span>bundle version<span class="si">)</span><span class="s2">"</span>
<span class="c"># Can run different gemfiles located in `gemfiles` folder</span>
<span class="k">if</span> <span class="o">[</span> <span class="nt">-z</span> <span class="s2">"</span><span class="k">${</span><span class="nv">GEMFILE</span><span class="k">}</span><span class="s2">"</span> <span class="o">]</span>
<span class="k">then
  </span><span class="nb">echo</span> <span class="s2">"Running default Gemfile"</span>
<span class="k">else
  </span><span class="nb">export </span><span class="nv">BUNDLE_GEMFILE</span><span class="o">=</span><span class="s2">"./gemfiles/</span><span class="k">${</span><span class="nv">GEMFILE</span><span class="k">}</span><span class="s2">.gemfile"</span>
  <span class="nb">echo</span> <span class="s2">"Running gemfile: </span><span class="k">${</span><span class="nv">GEMFILE</span><span class="k">}</span><span class="s2">"</span>
<span class="k">fi</span>

<span class="c"># Keep gems in the latest possible state</span>
<span class="o">(</span>bundle check <span class="o">||</span> bundle <span class="nb">install</span><span class="o">)</span> <span class="o">&amp;&amp;</span> bundle update <span class="o">&amp;&amp;</span> <span class="nb">exec </span>bundle <span class="nb">exec</span> <span class="k">${</span><span class="p">@</span><span class="k">}</span>
</code></pre></div></div>

<h1 id="docker-composeyml">docker-compose.yml</h1>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">volumes</span><span class="pi">:</span>
  <span class="na">gems</span><span class="pi">:</span>

<span class="na">x-base</span><span class="pi">:</span> <span class="nl">&amp;base</span>
  <span class="na">build</span><span class="pi">:</span>
    <span class="na">context</span><span class="pi">:</span> <span class="s">.</span>
    <span class="na">dockerfile</span><span class="pi">:</span> <span class="s">docker/Dockerfile</span>
    <span class="na">args</span><span class="pi">:</span>
      <span class="c1"># you can set a specific version</span>
      <span class="pi">-</span> <span class="s">RUBY_VERSION=${RUBY_VERSION:-3}</span>
  <span class="na">stdin_open</span><span class="pi">:</span> <span class="no">true</span>
  <span class="na">tty</span><span class="pi">:</span> <span class="no">true</span>
  <span class="na">volumes</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">.:/var/gem</span>
    <span class="pi">-</span> <span class="s">gems:/usr/local/bundle</span>
<span class="c1"># if you have multiple gemfiles (see entrypoint.sh), </span>
<span class="c1"># simply uncomment the following lines and change &lt;gemfile_name&gt;</span>
<span class="c1"># environment:</span>
<span class="c1">#  GEMFILE: &lt;gemfile_name&gt;</span>

<span class="na">services</span><span class="pi">:</span>
  <span class="na">cli</span><span class="pi">:</span>
    <span class="na">&lt;&lt;</span><span class="pi">:</span> <span class="nv">*base</span>
</code></pre></div></div>

<p>If you need to run a server, you can add another service that will run a command and expose a port. Here’s a example to with <code class="language-plaintext highlighter-rouge">jekyll</code> with live reload</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="na">server</span><span class="pi">:</span>
    <span class="na">&lt;&lt;</span><span class="pi">:</span> <span class="nv">*base</span>
    <span class="na">ports</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">4000:4000</span>
    <span class="c1"># need to change the host since we're running in Docker</span>
    <span class="na">command</span><span class="pi">:</span> <span class="pi">[</span> <span class="s1">'</span><span class="s">jekyll'</span><span class="pi">,</span> <span class="s1">'</span><span class="s">serve'</span><span class="pi">,</span> <span class="s1">'</span><span class="s">--host'</span><span class="pi">,</span> <span class="s1">'</span><span class="s">0.0.0.0'</span><span class="pi">,</span> <span class="s1">'</span><span class="s">-l'</span> <span class="pi">]</span>
</code></pre></div></div>

<p>To start the service: <code class="language-plaintext highlighter-rouge">docker compose run --rm --service-ports server</code></p>]]></content><author><name></name></author><summary type="html"><![CDATA[When forking a project, I like having a simple Docker setup to quickly start.]]></summary></entry><entry><title type="html">GET/HEAD = readonly? 🤔</title><link href="https://ericproulx.github.io/2022/05/27/get-head-readonly.html" rel="alternate" type="text/html" title="GET/HEAD = readonly? 🤔" /><published>2022-05-27T00:00:00+00:00</published><updated>2022-05-27T00:00:00+00:00</updated><id>https://ericproulx.github.io/2022/05/27/get-head-readonly</id><content type="html" xml:base="https://ericproulx.github.io/2022/05/27/get-head-readonly.html"><![CDATA[<p>By <a href="https://restfulapi.net/http-methods/#get">definition</a>, GET/HEAD endpoints should read stuff only. In other words, no writing at all, but how can you make sure it’s the case when you work in a large application? The question needs to be asked
if you’re about to use read-only replicas and activate the <a href="https://guides.rubyonrails.org/active_record_multiple_databases.html#activating-automatic-role-switching">automatic role switching feature</a></p>

<h1 id="lets-talk-about-strategies">Let’s talk about strategies.</h1>

<p>Depending of your current situation (multiple teams , multiple endpoints 😅, etc…), you need a scalable and sustainable solution over time.
I’m talking about some kind of tool that will give some awareness first and gives hard stops (exceptions) through progression.
Being smooth at the beginning and tighten things up at some point.</p>

<p>Leveraging <code class="language-plaintext highlighter-rouge">config.active_support.deprecation</code> feels like the best choice to me since you can easily change the behavior through environments.
Warnings is also a good choice depending if you’re handling them or not. I found a <a href="https://www.bugsnag.com/blog/managing-warnings-in-ruby">great article</a> about that.</p>

<h2 id="instrumentation-is-your-friend">Instrumentation is your friend</h2>

<p>A while ago, I came up with a neat solution using <a href="https://guides.rubyonrails.org/active_support_instrumentation.html">instrumentation</a> and a <a href="https://guides.rubyonrails.org/rails_on_rack.html">middleware</a>.
Basically, it wraps all non GET/HEAD request with a subscription to <a href="https://guides.rubyonrails.org/active_support_instrumentation.html#active-record">sql.active_record</a>.
The subscription’s callback looks at the payload for writable sql statements. It will then apply the chosen strategy depending of your choice if any writable statements were found.</p>

<p>This middleware does not log SQL statements but a <code class="language-plaintext highlighter-rouge">debug</code> parameter could be added to show a complete list (might be useful)
Lastly,</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># frozen_string_literal: true</span>
<span class="c1"># lib/middlewares/read_only_middleware.rb</span>

<span class="k">module</span> <span class="nn">Middlewares</span>
  <span class="k">class</span> <span class="nc">ReadOnlyMiddleware</span>
    <span class="no">WRITABLE_SQL_STATEMENTS</span> <span class="o">=</span> <span class="sx">%w[INSERT UPDATE DELETE UPSERT MERGE]</span><span class="p">.</span><span class="nf">freeze</span>

    <span class="n">thread_mattr_accessor</span> <span class="ss">:found_sql_writable_statement</span>

    <span class="nb">attr_reader</span> <span class="ss">:app</span><span class="p">,</span> <span class="ss">:reporting_strategy</span>

    <span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">app</span><span class="p">,</span> <span class="ss">strategy: </span><span class="kp">nil</span><span class="p">)</span>
      <span class="vi">@app</span> <span class="o">=</span> <span class="n">app</span>
      <span class="vi">@reporting_strategy</span> <span class="o">=</span> <span class="n">resolve_strategy</span><span class="p">(</span><span class="n">strategy</span><span class="p">)</span>
    <span class="k">end</span>

    <span class="k">def</span> <span class="nf">call</span><span class="p">(</span><span class="n">env</span><span class="p">)</span>
      <span class="n">request</span> <span class="o">=</span> <span class="no">Rack</span><span class="o">::</span><span class="no">Request</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">env</span><span class="p">)</span>
      <span class="k">return</span> <span class="n">_call</span><span class="p">(</span><span class="n">env</span><span class="p">)</span> <span class="k">unless</span> <span class="n">request</span><span class="p">.</span><span class="nf">get?</span> <span class="o">||</span> <span class="n">request</span><span class="p">.</span><span class="nf">head?</span>

      <span class="nb">self</span><span class="p">.</span><span class="nf">found_sql_writable_statement</span> <span class="o">=</span> <span class="kp">false</span>
      <span class="n">response</span> <span class="o">=</span> <span class="kp">nil</span>
      <span class="no">ActiveSupport</span><span class="o">::</span><span class="no">Notifications</span><span class="p">.</span><span class="nf">subscribed</span><span class="p">(</span><span class="nb">method</span><span class="p">(</span><span class="ss">:notification_callback</span><span class="p">),</span> <span class="s1">'sql.active_record'</span><span class="p">)</span> <span class="k">do</span>
        <span class="n">response</span> <span class="o">=</span> <span class="n">_call</span><span class="p">(</span><span class="n">env</span><span class="p">)</span>
      <span class="k">end</span>
      <span class="n">report</span><span class="p">(</span><span class="n">request</span><span class="p">)</span>
      <span class="n">response</span>
    <span class="k">end</span>

    <span class="kp">private</span>

    <span class="k">def</span> <span class="nf">_call</span><span class="p">(</span><span class="n">env</span><span class="p">)</span>
      <span class="vi">@app</span><span class="p">.</span><span class="nf">call</span><span class="p">(</span><span class="n">env</span><span class="p">)</span>
    <span class="k">end</span>

    <span class="kp">private</span>

    <span class="k">def</span> <span class="nf">notification_callback</span><span class="p">(</span><span class="o">*</span><span class="n">args</span><span class="p">)</span>
      <span class="n">event</span> <span class="o">=</span> <span class="no">ActiveSupport</span><span class="o">::</span><span class="no">Notifications</span><span class="o">::</span><span class="no">Event</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="o">*</span><span class="n">args</span><span class="p">)</span>
      <span class="k">return</span> <span class="k">if</span> <span class="n">found_sql_writable_statement</span> <span class="o">||</span> <span class="o">!</span><span class="n">event</span><span class="p">.</span><span class="nf">payload</span><span class="p">[</span><span class="ss">:sql</span><span class="p">].</span><span class="nf">start_with?</span><span class="p">(</span><span class="o">*</span><span class="no">WRITABLE_SQL_STATEMENTS</span><span class="p">)</span>

      <span class="nb">self</span><span class="p">.</span><span class="nf">found_sql_writable_statement</span> <span class="o">=</span> <span class="kp">true</span>
    <span class="k">end</span>

    <span class="k">def</span> <span class="nf">report</span><span class="p">(</span><span class="n">request</span><span class="p">)</span>
      <span class="k">return</span> <span class="k">unless</span> <span class="n">found_sql_writable_statement</span>

      <span class="n">reporting_strategy</span><span class="p">.</span><span class="nf">warn</span><span class="p">(</span><span class="s2">"The following endpoint is not readonly: </span><span class="si">#{</span><span class="n">endpoint_info</span><span class="p">(</span><span class="n">request</span><span class="p">)</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
    <span class="k">end</span>

    <span class="k">def</span> <span class="nf">endpoint_info</span><span class="p">(</span><span class="n">request</span><span class="p">)</span>
      <span class="n">path_params</span> <span class="o">=</span> <span class="n">request</span><span class="p">.</span><span class="nf">env</span><span class="p">[</span><span class="no">ActionDispatch</span><span class="o">::</span><span class="no">Http</span><span class="o">::</span><span class="no">Parameters</span><span class="o">::</span><span class="no">PARAMETERS_KEY</span><span class="p">]</span>
      <span class="s2">"</span><span class="si">#{</span><span class="n">path_params</span><span class="p">[</span><span class="ss">:controller</span><span class="p">]</span><span class="si">}</span><span class="s2">#</span><span class="si">#{</span><span class="n">path_params</span><span class="p">[</span><span class="ss">:action</span><span class="p">]</span><span class="si">}</span><span class="s2">"</span>
    <span class="k">end</span>

    <span class="k">def</span> <span class="nf">resolve_strategy</span><span class="p">(</span><span class="n">strategy</span><span class="p">)</span>
      <span class="k">case</span> <span class="n">strategy</span>
      <span class="k">when</span> <span class="ss">:deprecation</span>
        <span class="no">ActiveSupport</span><span class="o">::</span><span class="no">Deprecation</span>
      <span class="k">else</span>
        <span class="no">Rails</span><span class="p">.</span><span class="nf">logger</span>
      <span class="k">end</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>

</code></pre></div></div>
<p>In your <code class="language-plaintext highlighter-rouge">application.rb</code></p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">require</span> <span class="s1">'middlewares/read_only_middleware'</span>
<span class="n">config</span><span class="p">.</span><span class="nf">middleware</span><span class="p">.</span><span class="nf">use</span> <span class="no">Middlewares</span><span class="o">::</span><span class="no">ReadOnlyMiddleware</span><span class="p">,</span> <span class="ss">strategy: :deprecation</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">ActiveSupport::Deprecation</code> and <code class="language-plaintext highlighter-rouge">Rails.logger</code> came naturally to my mind since you might already have a strategy(:notify, :raise, etc…) in your app based on your environment.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[By definition, GET/HEAD endpoints should read stuff only. In other words, no writing at all, but how can you make sure it’s the case when you work in a large application? The question needs to be asked if you’re about to use read-only replicas and activate the automatic role switching feature]]></summary></entry></feed>