<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Ruby &amp; Beyond</title>
    <description>Igor Aleksandrov – Ruby engineer, Open Source contributor, JetRockets CTO and co-founder, Docker Captain and a proud father.</description>
    <link>https://igor.works/</link>
    <atom:link href="https://igor.works/feed.xml" rel="self" type="application/rss+xml"/>
    <pubDate>Sat, 06 Dec 2025 03:22:04 +0400</pubDate>
    <lastBuildDate>Sat, 06 Dec 2025 03:22:04 +0400</lastBuildDate>
    <generator>Jekyll v4.4.1</generator>
    
      <item>
        <title>Where is the software development industry going?</title>
        <description>&lt;p&gt;What does the future of software development actually look like—and where does AI fit in?&lt;/p&gt;
</description>
        <pubDate>Tue, 21 Oct 2025 00:00:00 +0400</pubDate>
        <link>https://igor.works/blog/where-is-the-software-development-industry-going</link>
        <guid isPermaLink="true">https://igor.works/blog/where-is-the-software-development-industry-going</guid>
        
        
      </item>
    
      <item>
        <title>Kamal 2 – Get out of the cloud</title>
        <description>&lt;p&gt;I took the TropicalOnRails 2025 stage to challenge the status quo with the talk “Kamal 2 – Get Out of Cloud”. In this bold session, I explored the possibilities and implications of running your infrastructure without relying on traditional cloud providers. Drawing from real-world experience and practical experimentation, I unpacked the motivations behind adopting Kamal 2, the architectural decisions involved, and what this shift means for scalability, cost, and developer control. A compelling talk for anyone curious about modern deployment strategies beyond the cloud.&lt;/p&gt;
</description>
        <pubDate>Sat, 17 May 2025 00:00:00 +0400</pubDate>
        <link>https://igor.works/blog/kamal-2-get-out-of-the-cloud</link>
        <guid isPermaLink="true">https://igor.works/blog/kamal-2-get-out-of-the-cloud</guid>
        
        <category>Docker</category>
        
        <category>Rails</category>
        
        <category>DevOps</category>
        
        <category>Kamal</category>
        
        
      </item>
    
      <item>
        <title>Kamal cheat sheet</title>
        <description>&lt;p&gt;I use Kamal daily to deploy all the apps I maintain. I created a list of Kamal commands I use frequently. While this is not an exhaustive list, but it covers the most common tasks I usually perform.&lt;/p&gt;

&lt;!--more--&gt;

&lt;pre class=&quot;language-bash&quot;&gt;&lt;code&gt;
# The init command is used to create the config and secrets files once after Kamal is added to the app.
$ kamal init

# Install Docker on all target hosts
$ kamal server

# Aliases
# Kamal defines `kamal shell` out of the box, I usually add `kamal console` to quickly access the Rails console.
$ kamal shell [-d production]
# app exec -i --reuse &quot;bin/rails console&quot;
$ kamal console [-d production]

# Deploy app [to the staging environment]
$ kamal deploy [-d staging]

# Rollback app [to the VERSION] [in the staging environment]
$ kamal rollback [VERSION] [-d staging]

# Building images
# Most of the time you won&apos;t use the `kamal build` command directly,
# since it primarily used by `kamal deploy` and `kamal redeploy`.
# However, you can also use it to build the app image without pushing it, which is useful for testing.
$ kamal build dev

# Below are several commands to read the logs of the app and its accessories.
$ kamal app logs [--roles=web -n 100 -f]
$ kamal app logs [--primary -n 100 -f]
$ kamal app logs [--hosts=&amp;lt;ip_address&amp;gt; -n 100 -f]
$ kamal audit
# Show logs for the PgHero accessory [for the staging environment]
$ kamal accessory logs pghero [-d staging]
# Show logs for the proxy server [for the staging environment]
$ kamal proxy logs [-d staging]

# Show combined config (including secrets) [for the staging environment]. Can be useful to debug and in case you forgot your server ip-address.
$ kamal config [-d staging]

# Print secrets to stdout [for the staging environment]
$ kamal secrets print [-d staging]

# Show details about all containers grouped by role [for the staging environment]
$ kamal details [-d staging]

# Show details only about the app containers [for the staging environment], [for the web role]
$ kamal app details [-d staging] [--roles=web]

# Show details only about the PgHero accessory containers [for the staging environment]
$ kamal accessory details pghero -d staging
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;To see all available commands, run &lt;code&gt;kamal help&lt;/code&gt;. If you prefer to save this cheat sheet as an image, &lt;a href=&quot;/assets/images/posts/2025-04-17/kamal-cheat-sheet.png&quot;&gt;use this link&lt;/a&gt;.&lt;/p&gt;
</description>
        <pubDate>Thu, 17 Apr 2025 00:00:00 +0400</pubDate>
        <link>https://igor.works/blog/kamal-cheat-sheet</link>
        <guid isPermaLink="true">https://igor.works/blog/kamal-cheat-sheet</guid>
        
        <category>Docker</category>
        
        <category>Rails</category>
        
        <category>DevOps</category>
        
        <category>Kamal</category>
        
        
      </item>
    
      <item>
        <title>Rails Dockerfile Best Practices – Part 1: Image Slimming</title>
        <description>&lt;p&gt;This article is a part of a series of posts where I will walk through every line of the Rails default &lt;code&gt;Dockerfile&lt;/code&gt; and explain the best practices and optimizations.&lt;/p&gt;

&lt;p&gt;Docker images can be optimized in different ways that include, but are not limited to, image size reduction, build performance optimization, security, and maintainability best practices, and application-specific optimizations. In the first article, I will touch only image size reduction optimization and explain why they are important.&lt;/p&gt;

&lt;!--more--&gt;

&lt;h2 id=&quot;why-to-optimize-the-image-size&quot;&gt;Why to optimize the image size?&lt;/h2&gt;

&lt;p&gt;As in every other process of software development, each developer will list his reasons why he wants to make his Docker builds faster. I will list the reasons that are most important to me.&lt;/p&gt;

&lt;h4 id=&quot;faster-builds--deployments&quot;&gt;Faster builds &amp;amp; deployments&lt;/h4&gt;

&lt;p&gt;Smaller images are faster to build because fewer files and layers need to be processed. This improves developer productivity, especially during iterative development cycles. Smaller images take less time to push to a registry and pull from it during deployments. This is especially critical in CI/CD pipelines where containers are built and deployed frequently.&lt;/p&gt;

&lt;h4 id=&quot;reduced-storage-costs--network-bandwidth-usage&quot;&gt;Reduced storage costs &amp;amp; network bandwidth usage&lt;/h4&gt;

&lt;p&gt;Smaller images consume less storage on container registries, local development machines, and production servers. This reduces infrastructure costs, especially for large-scale deployments. Smaller images use less bandwidth when transferred between servers, especially important when you’re building images locally or in CI/CD pipelines and pushing them to a registry.&lt;/p&gt;

&lt;figure&gt;
  &lt;blockquote&gt;We spent $3.2m on cloud in 2022... We stand to save about $7m in server expenses over five years from our cloud exit.&lt;/blockquote&gt;

  &lt;figcaption&gt;
    David Heinemeier Hansson —
    &lt;cite&gt;
      &lt;a href=&quot;https://world.hey.com/dhh/we-stand-to-save-7m-over-five-years-from-our-cloud-exit-53996caa&quot;&gt; HEY World &lt;/a&gt;
    &lt;/cite&gt;
  &lt;/figcaption&gt;
&lt;/figure&gt;

&lt;h4 id=&quot;improved-performance--security&quot;&gt;Improved performance &amp;amp; security&lt;/h4&gt;

&lt;p&gt;Smaller images require fewer resources (e.g., CPU, RAM) to load and run, improving the overall performance of containerized applications. Faster startup times mean your services are ready more quickly, which is crucial for scaling and high-availability systems. Minimal base images like &lt;code&gt;alpine&lt;/code&gt; or &lt;code&gt;debian-slim&lt;/code&gt; contain fewer pre-installed packages, decreasing the risk of unpatched or unnecessary software being exploited.&lt;/p&gt;

&lt;p&gt;Besides everything mentioned above, removing unnecessary files and tools minimizes distractions when diagnosing issues and leads to better maintainability and reduced technical debt.&lt;/p&gt;

&lt;h2 id=&quot;inspecting-docker-images&quot;&gt;Inspecting Docker images&lt;/h2&gt;

&lt;p&gt;To get different parameters of the image, including the  the size, you can either look at the Docker Desktop or run the &lt;code&gt;docker images&lt;/code&gt; command in the terminal.&lt;/p&gt;

&lt;pre class=&quot;language-bash&quot;&gt;&lt;code&gt;
➜ docker images
REPOSITORY        TAG       IMAGE ID       CREATED        SIZE
kamal-dashboard   latest    673737b771cd   2 days ago     619MB
kamal-proxy       latest    5f6cd8983746   6 weeks ago    115MB
docs-server       latest    a810244e3d88   6 weeks ago    1.18GB
busybox           latest    63cd0d5fb10d   3 months ago   4.04MB
postgres          latest    6c9aa6ecd71d   3 months ago   456MB
postgres          16.4      ced3ad69d60c   3 months ago   453MB
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Knowing the size of the image does not give you the full picture. You don’t know what is inside the image, how many layers it has, or how big each layer is. A &lt;strong&gt;Docker image layer&lt;/strong&gt; is a read-only, &lt;strong&gt;immutable file system layer&lt;/strong&gt; that is a component of a Docker image. Each layer represents a set of changes made to the image’s file system, such as adding files, modifying configurations, or installing software.&lt;/p&gt;

&lt;p&gt;Docker images are built incrementally, layer by layer, and each layer corresponds to an instruction in the &lt;code&gt;Dockerfile&lt;/code&gt;. To get the layers of the image, you can run the &lt;code&gt;docker history&lt;/code&gt; command.&lt;/p&gt;

&lt;pre class=&quot;language-bash&quot;&gt;&lt;code&gt;
➜ docker history kamal-dashboard:latest
IMAGE          CREATED       CREATED BY                                      SIZE      COMMENT
673737b771cd   4 days ago    CMD [&quot;./bin/thrust&quot; &quot;./bin/rails&quot; &quot;server&quot;]     0B        buildkit.dockerfile.v0
&amp;lt;missing&amp;gt;      4 days ago    EXPOSE map[80/tcp:{}]                           0B        buildkit.dockerfile.v0
&amp;lt;missing&amp;gt;      4 days ago    ENTRYPOINT [&quot;/rails/bin/docker-entrypoint&quot;]     0B        buildkit.dockerfile.v0
&amp;lt;missing&amp;gt;      4 days ago    USER 1000:1000                                  0B        buildkit.dockerfile.v0
&amp;lt;missing&amp;gt;      4 days ago    RUN /bin/sh -c groupadd --system --gid 1000 …   54MB      buildkit.dockerfile.v0
&amp;lt;missing&amp;gt;      4 days ago    COPY /rails /rails # buildkit                   56.2MB    buildkit.dockerfile.v0
&amp;lt;missing&amp;gt;      4 days ago    COPY /usr/local/bundle /usr/local/bundle # b…   153MB     buildkit.dockerfile.v0
&amp;lt;missing&amp;gt;      4 days ago    ENV RAILS_ENV=production BUNDLE_DEPLOYMENT=1…   0B        buildkit.dockerfile.v0
&amp;lt;missing&amp;gt;      4 days ago    RUN /bin/sh -c apt-get update -qq &amp;amp;&amp;amp;     apt…   137MB     buildkit.dockerfile.v0
&amp;lt;missing&amp;gt;      4 days ago    WORKDIR /rails                                  0B        buildkit.dockerfile.v0
&amp;lt;missing&amp;gt;      3 weeks ago   CMD [&quot;irb&quot;]                                     0B        buildkit.dockerfile.v0
&amp;lt;missing&amp;gt;      3 weeks ago   RUN /bin/sh -c set -eux;  mkdir &quot;$GEM_HOME&quot;;…   0B        buildkit.dockerfile.v0
&amp;lt;missing&amp;gt;      3 weeks ago   ENV PATH=/usr/local/bundle/bin:/usr/local/sb…   0B        buildkit.dockerfile.v0
&amp;lt;missing&amp;gt;      3 weeks ago   ENV BUNDLE_SILENCE_ROOT_WARNING=1 BUNDLE_APP…   0B        buildkit.dockerfile.v0
&amp;lt;missing&amp;gt;      3 weeks ago   ENV GEM_HOME=/usr/local/bundle                  0B        buildkit.dockerfile.v0
&amp;lt;missing&amp;gt;      3 weeks ago   RUN /bin/sh -c set -eux;   savedAptMark=&quot;$(a…   78.1MB    buildkit.dockerfile.v0
&amp;lt;missing&amp;gt;      3 weeks ago   ENV RUBY_DOWNLOAD_SHA256=018d59ffb52be3c0a6d…   0B        buildkit.dockerfile.v0
&amp;lt;missing&amp;gt;      3 weeks ago   ENV RUBY_DOWNLOAD_URL=https://cache.ruby-lan…   0B        buildkit.dockerfile.v0
&amp;lt;missing&amp;gt;      3 weeks ago   ENV RUBY_VERSION=3.4.1                          0B        buildkit.dockerfile.v0
&amp;lt;missing&amp;gt;      3 weeks ago   ENV LANG=C.UTF-8                                0B        buildkit.dockerfile.v0
&amp;lt;missing&amp;gt;      3 weeks ago   RUN /bin/sh -c set -eux;  mkdir -p /usr/loca…   19B       buildkit.dockerfile.v0
&amp;lt;missing&amp;gt;      3 weeks ago   RUN /bin/sh -c set -eux;  apt-get update;  a…   43.9MB    buildkit.dockerfile.v0
&amp;lt;missing&amp;gt;      3 weeks ago   # debian.sh --arch &apos;arm64&apos; out/ &apos;bookworm&apos; &apos;…   97.2MB    debuerreotype 0.15
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Since I already provided theory about images, and layers, it is time to explore the &lt;code&gt;Dockerfile&lt;/code&gt;. Starting from Rails 7.1, the &lt;code&gt;Dockerfile&lt;/code&gt; is generated with the new Rails application. Below is an example of what it may look like.&lt;/p&gt;

&lt;pre class=&quot;language-dockerfile&quot;&gt;&lt;code&gt;
# syntax=docker/dockerfile:1
# check=error=true

# Make sure RUBY_VERSION matches the Ruby version in .ruby-version
ARG RUBY_VERSION=3.4.1
FROM docker.io/library/ruby:$RUBY_VERSION-slim AS base

# Rails app lives here
WORKDIR /rails

# Install base packages
# Replace libpq-dev with sqlite3 if using SQLite, or libmysqlclient-dev if using MySQL
RUN apt-get update -qq &amp;amp;&amp;amp; \
    apt-get install --no-install-recommends -y curl libjemalloc2 libvips libpq-dev &amp;amp;&amp;amp; \
    rm -rf /var/lib/apt/lists /var/cache/apt/archives

# Set production environment
ENV RAILS_ENV=&quot;production&quot; \
    BUNDLE_DEPLOYMENT=&quot;1&quot; \
    BUNDLE_PATH=&quot;/usr/local/bundle&quot; \
    BUNDLE_WITHOUT=&quot;development&quot;

# Throw-away build stage to reduce size of final image
FROM base AS build

# Install packages needed to build gems
RUN apt-get update -qq &amp;amp;&amp;amp; \
    apt-get install --no-install-recommends -y build-essential curl git pkg-config libyaml-dev &amp;amp;&amp;amp; \
    rm -rf /var/lib/apt/lists /var/cache/apt/archives

# Install application gems
COPY Gemfile Gemfile.lock ./
RUN bundle install &amp;amp;&amp;amp; \
    rm -rf ~/.bundle/ &quot;${BUNDLE_PATH}&quot;/ruby/*/cache &quot;${BUNDLE_PATH}&quot;/ruby/*/bundler/gems/*/.git &amp;amp;&amp;amp; \
    bundle exec bootsnap precompile --gemfile

# Copy application code
COPY . .

# Precompile bootsnap code for faster boot times
RUN bundle exec bootsnap precompile app/ lib/

# Precompiling assets for production without requiring secret RAILS_MASTER_KEY
RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile

# Final stage for app image
FROM base

# Copy built artifacts: gems, application
COPY --from=build &quot;${BUNDLE_PATH}&quot; &quot;${BUNDLE_PATH}&quot;
COPY --from=build /rails /rails

# Run and own only the runtime files as a non-root user for security
RUN groupadd --system --gid 1000 rails &amp;amp;&amp;amp; \
    useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash &amp;amp;&amp;amp; \
    chown -R rails:rails db log storage tmp
USER 1000:1000

# Entrypoint prepares the database.
ENTRYPOINT [&quot;/rails/bin/docker-entrypoint&quot;]

# Start server via Thruster by default, this can be overwritten at runtime
EXPOSE 80
CMD [&quot;./bin/thrust&quot;, &quot;./bin/rails&quot;, &quot;server&quot;]
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Below I will provide a list of approaches and rules that where applied to the &lt;code&gt;Dockerfile&lt;/code&gt; above to make the final image size efficient.&lt;/p&gt;

&lt;h2 id=&quot;optimize-packages-installations&quot;&gt;Optimize packages installations&lt;/h2&gt;

&lt;p&gt;I am sure you keep only needed software on your local development machine. The same should be applied to Docker images. In the examples below I will consistently making worse &lt;a href=&quot;/assets/files/posts/2025-01-28/original.dockerfile&quot;&gt;the Dockerfile extracted&lt;/a&gt; from the Rails Dockerfile above. I will reference it as an &lt;strong&gt;original&lt;/strong&gt; &lt;code&gt;Dockerfile&lt;/code&gt; version.&lt;/p&gt;

&lt;h3 id=&quot;rule-1-use-minimal-base-images&quot;&gt;Rule #1: Use minimal base images&lt;/h3&gt;

&lt;pre class=&quot;language-dockerfile&quot;&gt;&lt;code&gt;
FROM docker.io/library/ruby:$RUBY_VERSION-slim AS base
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The base image is the starting point for the &lt;code&gt;Dockerfile&lt;/code&gt;. It is the image that is used to create the container. The base image is the first layer in the &lt;code&gt;Dockerfile&lt;/code&gt;, and it is the only layer that is not created by the &lt;code&gt;Dockerfile&lt;/code&gt; itself.&lt;/p&gt;

&lt;p&gt;The base image is specified with the &lt;code&gt;FROM&lt;/code&gt; command, followed by the image name and tag. The tag is optional, and if not specified, the &lt;code&gt;latest&lt;/code&gt; tag is used. The base image can be any image available on Docker Hub or any other registry.&lt;/p&gt;

&lt;p&gt;In the &lt;code&gt;Dockerfile&lt;/code&gt; about, we are using the &lt;code&gt;ruby&lt;/code&gt; image with the &lt;code&gt;3.4.1-slim&lt;/code&gt; tag. The &lt;code&gt;ruby&lt;/code&gt; image is &lt;a href=&quot;https://hub.docker.com/_/ruby&quot;&gt;the official Ruby image&lt;/a&gt; available on Docker Hub. The &lt;code&gt;3.4.1-slim&lt;/code&gt; tag is a slim version of the Ruby image that is based on the &lt;code&gt;debian-slim&lt;/code&gt; image. While the &lt;code&gt;debian-slim&lt;/code&gt; image is a minimal version of the Debian Linux image that is optimized for size. Look at the table below to get an idea of how smaller the &lt;code&gt;slim&lt;/code&gt; image is.&lt;/p&gt;

&lt;pre class=&quot;language-bash&quot;&gt;&lt;code&gt;
➜ docker images --filter &quot;reference=ruby&quot;
REPOSITORY   TAG              IMAGE ID       CREATED      SIZE
ruby         3.4.1-slim       0bf957e453fd   5 days ago   219MB
ruby         3.4.1-alpine     cf9b1b8d4a0c   5 days ago   99.1MB
ruby         3.4.1-bookworm   1e77081540c0   5 days ago   1.01GB
&lt;/code&gt;&lt;/pre&gt;

&lt;figure&gt;
  &lt;figcaption&gt;Debian Releases&lt;/figcaption&gt;

  &lt;em&gt;
    As of January, 2024, current Debian release is called &lt;strong&gt;bookworm&lt;/strong&gt; and the previous one is &lt;strong&gt;bullseye&lt;/strong&gt;.
  &lt;/em&gt;
&lt;/figure&gt;

&lt;p&gt;219 MB instead of 1GB — a huge difference. But what if the &lt;code&gt;alpine&lt;/code&gt; image is even smaller? The &lt;code&gt;alpine&lt;/code&gt; image is based on the Alpine Linux distribution, which is a super lightweight Linux distribution that is optimized for size and security. Alpine uses the &lt;code&gt;musl&lt;/code&gt; library (instead of &lt;code&gt;glibc&lt;/code&gt;) and &lt;code&gt;busybox&lt;/code&gt; (a compact set of Unix utilities) instead of GNU counterparts. While it is technically possible to use the &lt;code&gt;alpine&lt;/code&gt; image to run Rails, I will not cover it in this article.&lt;/p&gt;

&lt;h3 id=&quot;rule-2-minimize-layers&quot;&gt;Rule #2: Minimize layers&lt;/h3&gt;

&lt;pre class=&quot;language-dockerfile&quot;&gt;&lt;code&gt;
RUN apt-get update -qq &amp;amp;&amp;amp; \
    apt-get install --no-install-recommends -y curl libjemalloc2 libvips libpq-dev &amp;amp;&amp;amp; \
    rm -rf /var/lib/apt/lists /var/cache/apt/archives
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Each &lt;code&gt;RUN&lt;/code&gt;, &lt;code&gt;COPY&lt;/code&gt; and &lt;code&gt;FROM&lt;/code&gt; instruction in &lt;code&gt;Dockerfile&lt;/code&gt; creates a new layer. The more layers you have, the bigger the image size. This is why the best practice is to combine multiple commands into a single &lt;code&gt;RUN&lt;/code&gt; instruction. To illustrate this point, let’s look at the example below.&lt;/p&gt;

&lt;pre class=&quot;language-dockerfile&quot;&gt;&lt;code&gt;
# syntax=docker/dockerfile:1
# check=error=true

# Make sure RUBY_VERSION matches the Ruby version in .ruby-version
ARG RUBY_VERSION=3.4.1
FROM docker.io/library/ruby:$RUBY_VERSION-slim AS base

RUN apt-get update -qq
RUN apt-get install --no-install-recommends -y curl
RUN apt-get install --no-install-recommends -y libjemalloc2
RUN apt-get install --no-install-recommends -y libvips
RUN apt-get install --no-install-recommends -y libpq-dev
RUN rm -rf /var/lib/apt/lists /var/cache/apt/archives

CMD [&quot;echo&quot;, &quot;Whalecome!&quot;]
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;I have split the &lt;code&gt;RUN&lt;/code&gt; instruction into multiple lines, which obviously makes them more &lt;a href=&quot;/assets/files/posts/2025-01-28/no-minimize-layers.dockerfile&quot;&gt;human-readable&lt;/a&gt;. But how will it affect the size of the image? Let’s build the image and check it out.&lt;/p&gt;

&lt;pre class=&quot;language-bash&quot;&gt;&lt;code&gt;
➜ time docker build -t no-minimize-layers --no-cache -f no-minimize-layers.dockerfile .
0.31s user 0.28s system 2% cpu 28.577 total
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;It took 28 seconds to build the image, while to build the &lt;a href=&quot;/assets/files/posts/2025-01-28/original.dockerfile&quot;&gt;original version with minimized layers&lt;/a&gt; takes only 19 seconds (&lt;strong&gt;almost 33% faster&lt;/strong&gt;).&lt;/p&gt;

&lt;pre class=&quot;language-bash&quot;&gt;&lt;code&gt;
➜ time docker build -t original --no-cache -f original.dockerfile .
0.25s user 0.28s system 2% cpu 19.909 total
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Let’s check the size of the images.&lt;/p&gt;

&lt;pre class=&quot;language-bash&quot;&gt;&lt;code&gt;
➜ docker images --filter &quot;reference=*original*&quot; --filter &quot;reference=*no-minimize*&quot;
REPOSITORY           TAG       IMAGE ID       CREATED          SIZE
original             latest    f1363df79c8a   8 seconds ago    356MB
no-minimize-layers   latest    ad3945c8a8ee   43 seconds ago   379MB
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The image with minimized layers is 23 MB smaller than the one with no minimized layers. This is a &lt;strong&gt;6% reduction in size&lt;/strong&gt;. While it seems like a small difference in this example, the difference will be much bigger if you split all the &lt;code&gt;RUN&lt;/code&gt; instructions into multiple lines.&lt;/p&gt;

&lt;h3 id=&quot;rule-3-install-only-what-needed&quot;&gt;Rule #3: Install only what needed&lt;/h3&gt;

&lt;p&gt;By default, &lt;code&gt;apt-get install&lt;/code&gt; installs the recommended packages as well as packages you asked it to install. The &lt;code&gt;--no-install-recommends&lt;/code&gt; option tells &lt;code&gt;apt-get&lt;/code&gt; to install only the packages that are explicitly specified and not the recommended ones.&lt;/p&gt;

&lt;pre class=&quot;language-bash&quot;&gt;&lt;code&gt;
➜ time docker build -t without-no-install-recommends --no-cache -f without-no-install-recommends.dockerfile .
0.33s user 0.30s system 2% cpu 29.786 total
&lt;/code&gt;&lt;/pre&gt;

&lt;pre class=&quot;language-bash&quot;&gt;&lt;code&gt;
➜ docker images --filter &quot;reference=*original*&quot; --filter &quot;reference=*recommends*&quot;
REPOSITORY                      TAG       IMAGE ID       CREATED          SIZE
without-no-install-recommends   latest    41e6e37f1e2b   3 minutes ago    426MB
minimize-layers                 latest    dff22c85d84c   17 minutes ago   356MB
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;As you can see, the image without &lt;code&gt;--no-install-recommends&lt;/code&gt; is 70 MB bigger than the &lt;a href=&quot;/assets/files/posts/2025-01-28/original.dockerfile&quot;&gt;original one&lt;/a&gt;. This is a &lt;strong&gt;16% increase in size&lt;/strong&gt;.&lt;/p&gt;

&lt;figure&gt;
  &lt;figcaption&gt;What files where added?&lt;/figcaption&gt;

  &lt;em&gt;
    Use &lt;code&gt;dive&lt;/code&gt; utility to see which files were added to the image – read more about it in the end of the article.
  &lt;/em&gt;
&lt;/figure&gt;

&lt;h3 id=&quot;rule-4-clean-up-after-installations&quot;&gt;Rule #4: Clean up after installations&lt;/h3&gt;

&lt;p&gt;The original &lt;code&gt;Dockerfile&lt;/code&gt; includes the &lt;code&gt;rm -rf /var/lib/apt/lists/* /var/cache/apt/archives&lt;/code&gt; command after the &lt;code&gt;apt-get install&lt;/code&gt; command. This command removes the package lists and archives that are no longer needed after the installation. Let’s see how it affects the image size, to achieve that, I will create a new &lt;code&gt;Dockerfile&lt;/code&gt; &lt;a href=&quot;/assets/files/posts/2025-01-28/without-cleaning.dockerfile&quot;&gt;without the cleaning command&lt;/a&gt;.&lt;/p&gt;

&lt;pre class=&quot;language-dockerfile&quot;&gt;&lt;code&gt;
RUN apt-get update -qq &amp;amp;&amp;amp; \
    apt-get install --no-install-recommends -y curl libjemalloc2 libvips libpq-dev
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Building the images takes almost the same time as the original one, which makes sense.&lt;/p&gt;

&lt;pre class=&quot;language-bash&quot;&gt;&lt;code&gt;
➜ time docker build -t without-cleaning --no-cache -f without-cleaning.dockerfile .
0.28s user 0.30s system 2% cpu 21.658 total
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Let’s check the size of the images.&lt;/p&gt;

&lt;pre class=&quot;language-bash&quot;&gt;&lt;code&gt;
➜ docker images --filter &quot;reference=*original*&quot; --filter &quot;reference=*cleaning*&quot;
REPOSITORY         TAG       IMAGE ID       CREATED          SIZE
without-cleaning   latest    52884fe50773   2 minutes ago    375MB
original           latest    f1363df79c8a   16 minutes ago   356MB
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The image without cleaning is 19 MB bigger than the one with cleaning, this is a &lt;strong&gt;5% increase in size&lt;/strong&gt;.&lt;/p&gt;

&lt;h3 id=&quot;the-worst-scenario&quot;&gt;The worst scenario&lt;/h3&gt;

&lt;p&gt;What if all four optimizations mentioned above are not applied? Let’s create a new &lt;code&gt;Dockerfile&lt;/code&gt; &lt;a href=&quot;/assets/files/posts/2025-01-28/without-optimizations.dockerfile&quot;&gt;without any optimizations&lt;/a&gt; and build the image.&lt;/p&gt;

&lt;pre class=&quot;language-dockerfile&quot;&gt;&lt;code&gt;
# syntax=docker/dockerfile:1
# check=error=true

ARG RUBY_VERSION=3.4.1
FROM docker.io/library/ruby:$RUBY_VERSION AS base

RUN apt-get update -qq
RUN apt-get install -y curl
RUN apt-get install -y libjemalloc2
RUN apt-get install -y libvips
RUN apt-get install -y libpq-dev

CMD [&quot;echo&quot;, &quot;Whalecome!&quot;]
&lt;/code&gt;&lt;/pre&gt;

&lt;pre class=&quot;language-bash&quot;&gt;&lt;code&gt;
➜ time docker build -t without-optimizations --no-cache -f without-optimizations.dockerfile .
0.46s user 0.45s system 1% cpu 1:02.21 total
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Wow, it took more than a minute to build the image.&lt;/p&gt;

&lt;pre class=&quot;language-bash&quot;&gt;&lt;code&gt;
➜ docker images --filter &quot;reference=*original*&quot; --filter &quot;reference=*without-optimizations*&quot;
REPOSITORY              TAG       IMAGE ID       CREATED         SIZE
without-optimizations   latest    45671929c8e4   2 minutes ago   1.07GB
original                latest    f1363df79c8a   27 hours ago    356MB
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The image without optimizations is 714 MB bigger than the original one, this is a &lt;strong&gt;200% increase in size&lt;/strong&gt;. This clearly shows how important it is to optimize the &lt;code&gt;Dockerfile&lt;/code&gt;, larger images take more time to build and consume more disk space.&lt;/p&gt;

&lt;h2 id=&quot;always-use-dockerignore&quot;&gt;Always use .dockerignore&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;.dockerignore&lt;/code&gt; file is similar to the &lt;code&gt;.gitignore&lt;/code&gt; file used by Git. It is used to exclude files and directories from the context of the build. The context is the set of files and directories that are sent to the Docker daemon when building an image. The context is sent to the Docker daemon as a tarball, so it is important to keep it as small as possible.&lt;/p&gt;

&lt;p&gt;If, for any reason, you don’t have the &lt;code&gt;.dockerignore&lt;/code&gt; file in your project, you can create it manually. I suggest you use the official Rails &lt;code&gt;.dockerignore&lt;/code&gt; &lt;a href=&quot;https://github.com/rails/rails/blob/main/railties/lib/rails/generators/rails/app/templates/dockerignore.tt&quot;&gt;file template&lt;/a&gt; as a starting point. Below is an example of what it may look like.&lt;/p&gt;

&lt;pre class=&quot;language-dockerfile&quot;&gt;&lt;code&gt;
# See https://docs.docker.com/engine/reference/builder/#dockerignore-file for more about ignoring files.

# Ignore git directory.
/.git/
/.gitignore

# Ignore bundler config.
/.bundle

# Ignore all environment files.
/.env*

# Ignore all default key files.
/config/master.key
/config/credentials/*.key

# Ignore all logfiles and tempfiles.
/log/*
/tmp/*
!/log/.keep
!/tmp/.keep

# Ignore pidfiles, but keep the directory.
/tmp/pids/*
!/tmp/pids/.keep

# Ignore storage (uploaded files in development and any SQLite databases).
/storage/*
!/storage/.keep
/tmp/storage/*
!/tmp/storage/.keep

# Ignore assets.
/node_modules/
/app/assets/builds/*
!/app/assets/builds/.keep
/public/assets

# Ignore CI service files.
/.github

# Ignore development files
/.devcontainer

# Ignore Docker-related files
/.dockerignore
/Dockerfile*
&lt;/code&gt;&lt;/pre&gt;

&lt;figure&gt;
  &lt;figcaption&gt;Improving security&lt;/figcaption&gt;

  &lt;em&gt;
    Having a &lt;code&gt;.dockerfile&lt;/code&gt; file in the project not only allows excluding unnecessary files and directories (e.g., GitHub workflows from the &lt;code&gt;.github&lt;/code&gt; folder or JavaScript dependencies from the &lt;code&gt;node_modules&lt;/code&gt;) from the context. It also helps to avoid accidentally adding sensitive information to the image. For example, the &lt;code&gt;.env&lt;/code&gt; file that contains the environment variables or the &lt;code&gt;master.key&lt;/code&gt; file that is used to decrypt the credentials.

  &lt;/em&gt;
&lt;/figure&gt;

&lt;h2 id=&quot;use-dive&quot;&gt;Use Dive&lt;/h2&gt;

&lt;p&gt;All the optimizations mentioned above may seem obvious when explained. What to do if you already have a massive image, and you don’t know where to start?&lt;/p&gt;

&lt;p&gt;My favorite and most useful tool is &lt;a href=&quot;https://github.com/wagoodman/dive&quot;&gt;Dive&lt;/a&gt;. Dive is a TUI tool for exploring a Docker image, layer contents, and discovering ways to shrink the image size. Dive can be installed with your system package manager, or you can use its official Docker image to run it. Let’s use the image from our worst scenario.&lt;/p&gt;

&lt;pre class=&quot;language-bash&quot;&gt;&lt;code&gt;
docker run --rm -it -v /var/run/docker.sock:/var/run/docker.sock wagoodman/dive:latest without-optimizations
&lt;/code&gt;&lt;/pre&gt;

&lt;figure class=&quot;not-prose my-8 grid grid-cols-1 justify-items-center py-2&quot;&gt;
  &lt;img src=&quot;/assets/images/posts/2025-01-28/dive.png&quot; alt=&quot;Dive Docker layers inspect tool&quot; style=&quot;max-width: 100%&quot; /&gt;

  
  &lt;figcaption class=&quot;mt-2 block text-center text-sm text-gray-600&quot;&gt;
    Dive Docker layers inspect tool
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;p&gt;In the screenshot above, you can see the inspection of our the most non-optimal image. Dive shows the size of each layer, the total size of the image, and the files that were changed (added, modified, or deleted) in each layer. For me, this is the most useful feature of Dive. By listing the files in the right panel, you can easily identify the files that are not needed and remove commands that add them to the image.&lt;/p&gt;

&lt;p&gt;One thing that I truly love about Dive is that, besides having a terminal UI, it also can provide a CI-friendly output, which can be effective in a local development too. To use it, run Dive with the &lt;code&gt;CI&lt;/code&gt; environment variable set to &lt;code&gt;true&lt;/code&gt;, the output of the command is in the screenshot below.&lt;/p&gt;

&lt;pre class=&quot;language-bash&quot;&gt;&lt;code&gt;
docker run -e CI=true --rm -it -v /var/run/docker.sock:/var/run/docker.sock wagoodman/dive:latest without-optimizations
&lt;/code&gt;&lt;/pre&gt;

&lt;figure class=&quot;not-prose my-8 grid grid-cols-1 justify-items-center py-2&quot;&gt;
  &lt;img src=&quot;/assets/images/posts/2025-01-28/dive-ci.png&quot; alt=&quot;Dive CI-friendly output&quot; style=&quot;max-width: 100%&quot; /&gt;

  
  &lt;figcaption class=&quot;mt-2 block text-center text-sm text-gray-600&quot;&gt;
    Dive CI-friendly output
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;p&gt;My personal preference is to use Dive on a scheduled basis, for example, once a week, to ensure your images are still in a good shape. In the upcoming articles, I will cover automated workflows I use to check my Dockerfile, including Dive and &lt;a href=&quot;https://github.com/hadolint/hadolint&quot;&gt;Hadolint&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&quot;dont-squash-layers&quot;&gt;Don’t squash layers&lt;/h2&gt;

&lt;p&gt;One approach to minimizing image size that I’ve seen is to try to squash the layers. The idea was to combine several layers into a single layer to reduce the image size. Docker had an experimental option &lt;code&gt;--squash&lt;/code&gt;, besides this, there were third-party tools like &lt;a href=&quot;https://github.com/goldmann/docker-squash&quot;&gt;docker-squash&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;While this approach worked in the past, currently it is deprecated and not recommended to use. Squashing layers destroyed Docker’s fundamental feature of layer caching. Apart from that, while using &lt;code&gt;--squash&lt;/code&gt; you could unintentionally include sensitive or temporary files from earlier layers in the final image. This is an all-or-nothing approach that lacks fine-grained control.&lt;/p&gt;

&lt;p&gt;Instead of squashing layers, it is recommended to use multi-stage builds. Rails &lt;code&gt;Dockerfile&lt;/code&gt; already uses multi-stage builds, I will explain how it works in the next article.&lt;/p&gt;

&lt;h2 id=&quot;conclusions&quot;&gt;Conclusions&lt;/h2&gt;

&lt;p&gt;Optimizing Docker images, just like any other optimization, &lt;strong&gt;cannot be done once and forgotten&lt;/strong&gt;. It is an ongoing process that requires regular checks and improvements. I tried to cover the basics, but they are critical to know and understand. In the next articles, I will cover more advanced techniques and tools that can help to make your Docker builds faster and more efficient.&lt;/p&gt;
</description>
        <pubDate>Tue, 28 Jan 2025 00:00:00 +0400</pubDate>
        <link>https://igor.works/blog/rails-dockerfile-best-practices-part-1-image-slimming</link>
        <guid isPermaLink="true">https://igor.works/blog/rails-dockerfile-best-practices-part-1-image-slimming</guid>
        
        <category>Docker</category>
        
        <category>Rails</category>
        
        <category>Captain Speaks</category>
        
        <category>DevOps</category>
        
        
      </item>
    
      <item>
        <title>Use Docker to template local database</title>
        <description>&lt;p&gt;How often do you reset your local development database? Depending on the task I am working on, I can do this even ten times a day. The reasons can be different. You or your teammate pushed an irreversible migration, which is of course not the best practice, but happens in the real life. Or you simply corrupted the data while implementing some new functionality.&lt;/p&gt;

&lt;!--more--&gt;

&lt;h2 id=&quot;tldr&quot;&gt;TL;DR&lt;/h2&gt;

&lt;p&gt;Resetting a local development database is a common task that can take a lot of time when the database dump is large. When you need to reset your local development database, you can use Docker to template it. This will save you a lot of time when your database dump is large.&lt;/p&gt;

&lt;p&gt;The repository with the scripts is available &lt;a href=&quot;https://github.com/igor-alexandrov/docker-db-template&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&quot;the-problem&quot;&gt;The Problem&lt;/h2&gt;

&lt;p&gt;When your project DB size is 100Mb, it is not a problem to run &lt;code&gt;psql -d project_development &amp;lt; project_development.sql&lt;/code&gt; to restore the database. But what if your project database dump is 50 GB or 100 GB? This is a common size of the database for the project that does have some real clients.&lt;/p&gt;

&lt;p&gt;On my Mac M1 Pro, it usually takes about 30 minutes to restore a 50GB dump from the SQL data file. Resetting such a database two times a day can cost you your lunchtime. Sounds so-so.&lt;/p&gt;

&lt;pre class=&quot;language-bash&quot;&gt;&lt;code&gt;
➜  backend git:(invoices) ✗ time psql -d safari_development_1 -f ~/Downloads/safari_development.sql

psql -d safari_development -f ~/Downloads/safari_development.sql  38.48s user 121.23s system 9% cpu 27:49.06 total
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Having the dump in the binary format will make it significantly smaller (8 GB instead of 50), but still, it will take a lot of time to restore it.&lt;/p&gt;

&lt;pre class=&quot;language-bash&quot;&gt;&lt;code&gt;
➜  backend git:(master) time pg_restore -d safari_development ~/Downloads/safari_development.dump

pg_restore -d safari_development ~/Downloads/safari_development.dump  180.61s user 75.97s system 13% cpu 30:39.12 total
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;How to speed things up?&lt;/p&gt;

&lt;h2 id=&quot;general-idea&quot;&gt;General Idea&lt;/h2&gt;

&lt;p&gt;The general idea is elementary – &lt;strong&gt;somehow avoid restoring the database dump&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A similar approach works in the database replication. When your master database goes down for whatever reason, you can immediately switch to the slave database, which will become the master. You don’t need to wait while you will restore the dump from the backup.&lt;/p&gt;

&lt;p&gt;How can we avoid restoring dump when working locally? The idea is to have a template of the database somewhere and simply copy this template instead of restoring the dump with &lt;code&gt;psql&lt;/code&gt; or &lt;code&gt;pg_restore&lt;/code&gt;.&lt;/p&gt;

&lt;figure&gt;
  &lt;figcaption&gt;PostgreSQL Templates&lt;/figcaption&gt;

  &lt;em&gt;
    PostgreSQL has a built-in &lt;a href=&quot;https://www.postgresql.org/docs/current/manage-ag-templatedbs.html&quot;&gt;template system&lt;/a&gt;. These templates allow users to replicate the structure, schema, and any data stored in them into newly created databases. By default, PostgreSQL provides two templates: &lt;code&gt;template0&lt;/code&gt; and &lt;code&gt;template1&lt;/code&gt;. This feature helps standardize new databases and streamline the creation process across multiple instances. However, since during the initialization of the database all the objects from the template are reinitialized again, it still takes quite a lot of time to run.
  &lt;/em&gt;

  &lt;pre class=&quot;language-bash&quot;&gt;&lt;code&gt;
    ➜  backend git:(master) time createdb -T safari_development_template safari_development
    createdb -T safari_development_template safari_development  0.00s user 0.01s system 0% cpu 17:34.90 total
  &lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;

&lt;p&gt;What does it mean with PostgreSQL? This means we should have a full copy of our PostgreSQL cluster on the disk and copy it every time we want to “restore” the database. With PostgreSQL (or any else database except the SQLite) that is installed on directly your host machine, this can be a rather tricky task. You will have to worry about management of PostgreSQL config files, environment variables and tons of other stuff. Believe me, this won’t make you development efficient.&lt;/p&gt;

&lt;p&gt;This is where Docker comes in place. My idea was to have two versions of the database – the template and the current working version, and to have an ability to quickly copy the template to the current version.&lt;/p&gt;

&lt;h2 id=&quot;implementation&quot;&gt;Implementation&lt;/h2&gt;

&lt;p&gt;Before going into the detail, I want to quickly walk through the main Docker concepts that I used in this implementation. The first one is the Docker image. The image is a read-only blueprint with instructions for creating a Docker container. The second one is the Docker container. The container is a runnable instance of an image. Containers and images are managed with &lt;code&gt;docker container&lt;/code&gt; and &lt;code&gt;docker image&lt;/code&gt; &lt;a href=&quot;https://docs.docker.com/reference/cli/docker/container/&quot;&gt;commands&lt;/a&gt; &lt;a href=&quot;https://docs.docker.com/reference/cli/docker/image/&quot;&gt;accordingly&lt;/a&gt;. The third one is the Docker volume. The volume is a persistent data storage mechanism that allows data to exist beyond the lifecycle of a container.&lt;/p&gt;

&lt;figure&gt;
  &lt;figcaption&gt;Docker Volumes 101&lt;/figcaption&gt;

  &lt;em&gt;
    A key feature of a Docker container is its &lt;strong&gt;temporary nature&lt;/strong&gt;. When a container is deleted, any changes made to its file system are lost. To address this, Docker uses volumes. The concept is to store data on the host machine’s file system rather than within the container itself, ensuring it persists beyond the container’s lifecycle.
  &lt;/em&gt;
&lt;/figure&gt;

&lt;p&gt;Dealing with a Docker volume is straightforward. Once the volume is created, it can be attached to the container. When the container is deleted, the volume is still there. You can attach the volume to another container and continue working with the data. Volumes are managed with the &lt;code&gt;docker volume&lt;/code&gt; &lt;a href=&quot;https://docs.docker.com/reference/cli/docker/volume/&quot;&gt;command&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Lets take a look the small script below.&lt;/p&gt;

&lt;pre class=&quot;language-bash&quot;&gt;&lt;code&gt;
#!/bin/bash

set -e

# Create a volume
docker volume create safari-development-template

# Start a container with the PostgreSQL database and attach the volume
docker run --name safari-development-template \
  -p 5432:5432 \
  -e POSTGRES_USER=igor \
  -e POSTGRES_DB=safari_development \
  -e POSTGRES_HOST_AUTH_METHOD=trust \
  -v safari-development-template:/var/lib/postgresql/data \
  -d postgres:17
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;What does it do? It creates a volume with the name &lt;code&gt;safari-development-template&lt;/code&gt; and starts a container with the PostgreSQL database. The container is named &lt;code&gt;safari-development-template&lt;/code&gt; and exposes the port 5432. The database name is &lt;code&gt;safari_development&lt;/code&gt; and the user is &lt;code&gt;igor&lt;/code&gt;. The volume &lt;code&gt;safari-development-template&lt;/code&gt; is attached to the container.&lt;/p&gt;

&lt;p&gt;Once the container is up, I can restore the database dump into it.&lt;/p&gt;

&lt;pre class=&quot;language-bash&quot;&gt;&lt;code&gt;
psql -d safari_development \
  -h localhost \
  -p 5432 &amp;lt; safari_development.sql
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;After about 30 minutes, the database has been restored. With &lt;a href=&quot;https://www.docker.com/products/docker-desktop/&quot;&gt;Docker Desktop&lt;/a&gt;, I can see the volume with its size.&lt;/p&gt;

&lt;figure class=&quot;not-prose my-8 grid grid-cols-1 justify-items-center py-2&quot;&gt;
  &lt;img src=&quot;/assets/images/posts/2024-10-27/docker-volumes-listing.png&quot; alt=&quot;Docker Volumes Listing&quot; style=&quot;max-width: 600px&quot; /&gt;

  
  &lt;figcaption class=&quot;mt-2 block text-center text-sm text-gray-600&quot;&gt;
    Docker Volumes Listing
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;p&gt;The setup we have now is pretty standard. Our database is running in the container and the data is stored on the host machine. Such a setup allows us to do local development. Unfortunately, from the time perspective, it is not anyhow different the restoring the database dump directly on the host machine.&lt;/p&gt;

&lt;p&gt;Now let’s move to the interesting part. Firstly, we need to create another volume that will be used for the working database. This can be done similar to the template volume creation.&lt;/p&gt;

&lt;pre class=&quot;language-bash&quot;&gt;&lt;code&gt;
docker volume create safari-development
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;So now we have two volumes: &lt;code&gt;safari-development-template&lt;/code&gt; and &lt;code&gt;safari-development&lt;/code&gt;. The first one has the data and the second one is empty. The idea is to copy the data from the first volume to the second one. Docker doesn’t provide a direct way to copy the data between the volumes, but the data can be copied between the volumes that are attached to the container. With this idea in mind I decided to use the smallest possible Docker image that has the &lt;code&gt;cp&lt;/code&gt; command.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;busybox&lt;/code&gt; &lt;a href=&quot;https://hub.docker.com/_/busybox&quot;&gt;image&lt;/a&gt; is the best choice for this. It is only 4 MB in size and has many common UNIX utilities. To copy the data between the volumes, I  started a container with the &lt;code&gt;busybox&lt;/code&gt; image and attached both volumes to it to run the &lt;code&gt;cp&lt;/code&gt; command.&lt;/p&gt;

&lt;pre class=&quot;language-bash&quot;&gt;&lt;code&gt;
docker container run --rm -it \
    -v safari-development-template:/from \
    -v safari-development:/to \
    busybox sh -c &quot;cd /from ; cp -av . /to&quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;figure&gt;
  &lt;figcaption&gt;BusyBox&lt;/figcaption&gt;

  &lt;em&gt;
    &lt;a href=&quot;https://busybox.net/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;BusyBox&lt;/a&gt; was created by Bruce Perens in 1995 to address the need for a compact, efficient tool set for embedded systems running Linux. At the time, most Unix utilities were too large and complex for the limited resources of embedded devices. Perens designed BusyBox to combine essential Unix tools into a single, space-saving binary, allowing developers to run key commands in environments with minimal memory and storage.
    &lt;br /&gt;&lt;br /&gt;
    The BusyBox Docker image is a streamlined Linux environment optimized for lightweight, containerized applications, testing, and quick tasks where minimalism is essential. This design allows BusyBox to deliver essential commands (like &lt;code&gt;ls&lt;/code&gt;, &lt;code&gt;cp&lt;/code&gt;, and &lt;code&gt;ping&lt;/code&gt;) in a fraction of the space required by standard Linux distributions, making the Docker image often just a 5 Mb in size.
  &lt;/em&gt;
&lt;/figure&gt;

&lt;p&gt;Once the image is downloaded, it takes about &lt;strong&gt;40 seconds&lt;/strong&gt; to copy the data between the volumes. The time is significantly less than the time it takes to restore the database dump. The data is copied, and we can start the container with the working database with the command below.&lt;/p&gt;

&lt;pre class=&quot;language-bash&quot;&gt;&lt;code&gt;
docker run --name safari-development \
  -p 5432:5432 \
  -e POSTGRES_USER=igor \
  -e POSTGRES_DB=safari_development \
  -e POSTGRES_HOST_AUTH_METHOD=trust \
  -v safari-development:/var/lib/postgresql/data \
  -d postgres:17
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now I can connect to the database and start working on the feature. When I need to reset the database, I can simply stop the container and copy the data from the template volume to the working volume again within less than a minute. This approach saves me a lot of time almost every day.&lt;/p&gt;

&lt;p&gt;To make the process easier and more automated, I created a couple of scripts that can be used to create the template and to switch between the volumes. The repository with the scripts is available &lt;a href=&quot;https://github.com/igor-alexandrov/docker-db-template&quot;&gt;here&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;usage&quot;&gt;Usage&lt;/h2&gt;

&lt;p&gt;To create a template database use &lt;code&gt;docker-db-create-template.sh&lt;/code&gt;, I usually put to the &lt;code&gt;bin&lt;/code&gt; directory of the Rails project. The script that accepts two required parameters:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;code&gt;project_name&lt;/code&gt;, which is safari in our case&lt;/li&gt;
  &lt;li&gt;&lt;code&gt;file_path&lt;/code&gt;, path to the database dump (currently in SQL format)&lt;/li&gt;
&lt;/ul&gt;

&lt;pre class=&quot;language-bash&quot;&gt;&lt;code&gt;
➜  backend git:(master) ✗ ./bin/docker-db-create-template.sh safari ~/Downloads/safari_development.sql
Stopping container safari-development-template                  done
Deleting container safari-development-template                  done
Removing existing volume safari-development-template            done
Creating new volume safari-development-template                 done
Starting container safari-development-template                  done
	Loading /Users/igor/Downloads/safari_development.sql...
into safari_development                                         done

Database safari_development is running on port 5432

What to do next:
1. Run migrations
2. Start Rails server
3. Change the data the way you want it to be in the template
4. Stop Rails server
5. Stop the database container with the command: docker stop safari-development-template
6. Run ./bin/docker-db-use-template.sh safari to use the template
Elapsed time: 1542 seconds
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now you can connect to your database on the port 5432. Do whatever preparations in the template you need. Usually this can include creating yourself a user or any other data modifications that you will need later.&lt;/p&gt;

&lt;p&gt;When you are done with, stop your container with &lt;code&gt;docker stop safari-development-template&lt;/code&gt; command.&lt;/p&gt;

&lt;p&gt;It is time to use your template.&lt;/p&gt;

&lt;pre class=&quot;language-bash&quot;&gt;&lt;code&gt;
➜  backend git:(master) ./bin/docker-db-use-template.sh safari
Stopping container safari-development                             done
Deleting container safari-development                             done
Removing existing volume safari-development                       done
Creating new volume safari-development                            done
Copying data from safari-development-template                     done
Starting container safari-development                             done

Database safari_development is running on port 5432
Elapsed time: 46 seconds
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Only 46 seconds and are ready to go. Start the Rails app again and work on your feature. Each time you will need to reset the database to the template simply run &lt;code&gt;./bin/docker-db-use-template.sh safari&lt;/code&gt; again.&lt;/p&gt;

&lt;h2 id=&quot;conclusions&quot;&gt;Conclusions&lt;/h2&gt;
&lt;p&gt;There are two main takeaways from this post. The first is that you should try to be as effective as possible. When I see that some task takes a lot of time, I try to find a way to speed it up. It is true for my work as developer, CTO, and even true for my personal life. Time is the most valuable resource that we have.&lt;/p&gt;

&lt;p&gt;The second is that creating a tooling around your daily tasks can save you a lot of time. I have many scripts that I use every day. They are not perfect, but they do the job. I am excited to share them with you. For any clarifications or suggestions, feel free to reach me out on &lt;a href=&quot;https://x.com/igor_alexandrov&quot;&gt;X&lt;/a&gt;.&lt;/p&gt;
</description>
        <pubDate>Mon, 28 Oct 2024 00:00:00 +0400</pubDate>
        <link>https://igor.works/blog/use-docker-to-template-local-database</link>
        <guid isPermaLink="true">https://igor.works/blog/use-docker-to-template-local-database</guid>
        
        <category>Docker</category>
        
        <category>DevOps</category>
        
        <category>Captain Speaks</category>
        
        
      </item>
    
      <item>
        <title>Rails Thruster 101</title>
        <description>&lt;p&gt;While we are waiting for the Rails World 2024 conference to start and Kamal 2 to be released, I decided to make some preparations for the upcoming changes in the Rails ecosystem.&lt;/p&gt;

&lt;p&gt;In March 2024 Basecamp team released &lt;a href=&quot;https://github.com/basecamp/thruster/&quot;&gt;Thruster&lt;/a&gt; a small, Go-based proxy server for Rails. While the original idea of Thruster was to provide an ability to build production-ready Rails applications from a single container for the ONCE project, it will fit perfectly to the Kamal ecosystem.&lt;/p&gt;

&lt;!--more--&gt;

&lt;h2 id=&quot;isnt-it-a-redundancy&quot;&gt;Isn’t it a redundancy?&lt;/h2&gt;

&lt;p&gt;Wait? Another proxy server? Why do we need it? Allow me to explain.&lt;/p&gt;

&lt;p&gt;It doesn’t matter whether we use Traefik with Kamal v1 or kamal-proxy with Kamal v2, inside the container we still run Puma (or any other Ruby application server) that serves requests to the application. Besides this, Puma is also responsible to serving application assets – images, stylesheets, JavaScript files, etc. – to the clients. Before Kamal came into the scene, we usually had Nginx in our infrastructure schema. Nginx is a classic web server that can serve static files efficiently. Unfortunately, it is not adapted to the container-based setups as you need to manage its configuration via a config file. However, without Nginx, Puma is not the best choice to serve static files.&lt;/p&gt;

&lt;p&gt;This is where Thruster comes into play. It wraps the Puma instance and efficiently serves static files with X-Sendfile acceleration, while Puma is responsible for the dynamic content.&lt;/p&gt;

&lt;p&gt;Thruster also provides GZip compression which was available in Traefik &lt;a href=&quot;https://doc.traefik.io/traefik/middlewares/http/compress/&quot;&gt;via middleware&lt;/a&gt;, but won’t exist in kamal-proxy. GZip compression plays a crucial role in optimizing web performance by significantly reducing the size of files transmitted between a server and a client. By compressing assets like HTML, CSS, and JavaScript, GZIP decreases bandwidth usage and accelerates the loading times of web pages, providing users with a faster, more seamless browsing experience. This not only enhances user satisfaction but also improves search engine rankings, as faster websites tend to rank higher in search results. Additionally, reducing the size of data transfers lightens the load on servers, allowing them to handle more requests efficiently and lowering hosting costs.&lt;/p&gt;

&lt;p&gt;A reasonable question would be “Why I cannot do a GZIP compression in Rails?”.
Technically, you can with &lt;a href=&quot;https://github.com/rack/rack/blob/main/lib/rack/deflater.rb&quot;&gt;Rack::Deflater&lt;/a&gt;. But since Thruster is already responsible for serving public assets, it can also efficiently handle compression.&lt;/p&gt;

&lt;p&gt;Besides a compression, Thruster also provides a basic HTTP caching of public assets and HTTP/2 support.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;With Thruster your Rails application can be treated as a black box, which can serve any kind of requests efficiently.&lt;/strong&gt;&lt;/p&gt;

&lt;h2 id=&quot;how-to-use&quot;&gt;How to use?&lt;/h2&gt;

&lt;p&gt;Thruster is straightforward to use if you’re already using Puma.&lt;/p&gt;

&lt;p&gt;First, you need to add Thruster to your Gemfile:&lt;/p&gt;

&lt;pre class=&quot;language-ruby&quot;&gt;&lt;code&gt;
# Gemfile

gem &apos;thruster&apos;

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

&lt;p&gt;Run &lt;code&gt;bundle install&lt;/code&gt; and you are ready to go. Since Thruster is written in Go, platform specific binary will be downloaded and installed with the gem.&lt;/p&gt;

&lt;p&gt;I always create binstubs to have all the necessary commands in one place:&lt;/p&gt;

&lt;pre class=&quot;language-bash&quot;&gt;&lt;code&gt;
$ bundle binstubs thruster
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;By default, Thruster will listen on port 80, while Puma will stay on port 3000. Knowing this we should update our application &lt;code&gt;Dockerfile&lt;/code&gt; accordingly. I assume here that your application is already dockerized. I replaced the port that is getting exposed from 3000 to 80 and added &lt;code&gt;./bin/thrust&lt;/code&gt; to the default command.&lt;/p&gt;

&lt;pre class=&quot;language-dockerfile&quot;&gt;&lt;code&gt;
# Dockerfile

# Entrypoint prepares the database.
ENTRYPOINT [&quot;/rails/bin/docker-entrypoint&quot;]

# Start server via Thruster by default, this can be overwritten at runtime
EXPOSE 80
CMD [&quot;./bin/thrust&quot;, &quot;./bin/rails&quot;, &quot;server&quot;]
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The last change is to update &lt;code&gt;./bin/docker-entrypoint&lt;/code&gt; script. I always try to keep it as close to the &lt;a href=&quot;https://github.com/rails/rails/blob/main/railties/lib/rails/generators/rails/app/templates/docker-entrypoint.tt&quot;&gt;original Rails template&lt;/a&gt; as possible. Below is the final version of the script.&lt;/p&gt;

&lt;pre class=&quot;language-bash&quot;&gt;&lt;code&gt;
#!/bin/bash -e

# If running the rails server then create or migrate existing database
if [ &quot;${@: -2:1}&quot; == &quot;./bin/rails&quot; ] &amp;amp;&amp;amp; [ &quot;${@: -1:1}&quot; == &quot;server&quot; ]; then
  ./bin/rails db:prepare
fi

exec &quot;${@}&quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;That is all! Thanks to the Traefik container-based configuration, it will read the exposed port from the container and route the traffic accordingly after you will deploy the changes.&lt;/p&gt;

&lt;p&gt;After deploy will be finished, you can log in to the container and check if Thruster is running:&lt;/p&gt;

&lt;pre class=&quot;language-bash&quot;&gt;&lt;code&gt;
rails@docker:~$ ps aux
USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
rails          1  0.0  0.3 1232848 13124 ?       Ssl  09:26   0:06 /usr/local/bundle/ruby/3.3.0/gems/thruster-0.1.8-x86_64-linux/exe/x86_64-linux/thrust ./bin/rails server
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;/h2&gt;

&lt;p&gt;Since there are no visible cons of using Thruster in your Rails application, I highly recommend using it. It can improve the performance of your application and reduce the load on the server. It is also a good idea to use it as a preparation for the upcoming Kamal 2 release.&lt;/p&gt;
</description>
        <pubDate>Mon, 23 Sep 2024 00:00:00 +0400</pubDate>
        <link>https://igor.works/blog/rails-thruster-101</link>
        <guid isPermaLink="true">https://igor.works/blog/rails-thruster-101</guid>
        
        <category>Ruby</category>
        
        <category>Rails</category>
        
        <category>DevOps</category>
        
        <category>kamal</category>
        
        
      </item>
    
      <item>
        <title>Odd Ruby return values</title>
        <description>&lt;p&gt;Earlier this week I published a &lt;a href=&quot;https://x.com/igor_alexandrov/status/1827955386365677774&quot;&gt;tweet that made some buzz&lt;/a&gt; in the community. To be honest, I didn’t expect that so many people would be interested in this topic. About 29000 views and 1600 votes is quite an impressive result for 9 lines of code.&lt;/p&gt;

&lt;!--more--&gt;

&lt;p&gt;The tweet was about the following code snippet:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;class Klass
  def self.square(value)
    if value &amp;gt; 0
      value * value
    end
  end
end

puts Klass.square(0)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The question was: what is the result of the &lt;code&gt;Klass.square(0)&lt;/code&gt; method call?&lt;/p&gt;

&lt;h2 id=&quot;tldr&quot;&gt;TL;DR&lt;/h2&gt;

&lt;p&gt;The result of the &lt;code&gt;Klass.square(0)&lt;/code&gt; method call is &lt;code&gt;nil&lt;/code&gt;. From 1600 votes, about 1500 people gave the correct answer.&lt;/p&gt;

&lt;h2 id=&quot;explanation&quot;&gt;Explanation&lt;/h2&gt;

&lt;p&gt;Every expression in Ruby has a value.&lt;/p&gt;

&lt;p&gt;A conditional statement in Ruby is an expression that returns &lt;code&gt;nil&lt;/code&gt; if the conditional is &lt;code&gt;false&lt;/code&gt;. Ruby methods return the last expression in the method body.&lt;/p&gt;

&lt;p&gt;So, in the code snippet above, 0 is not greater than 0, so the &lt;code&gt;if&lt;/code&gt; statement returns &lt;code&gt;nil&lt;/code&gt;. The &lt;code&gt;Klass.square&lt;/code&gt; method returns the result of the &lt;code&gt;if&lt;/code&gt; statement (because it is the last expression), which is &lt;code&gt;nil&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Elementary, right? Yes, but since there were about 100 people who gave the wrong answer (0), even such simple things can be tricky and odd.&lt;/p&gt;

&lt;h2 id=&quot;more-odd-examples&quot;&gt;More odd examples&lt;/h2&gt;

&lt;p&gt;In the comments to the tweet, &lt;a href=&quot;https://x.com/lucianghinda/status/1828077212370714890&quot;&gt;Lucian Ghinda pointed&lt;/a&gt; to a similar, odd example originally &lt;a href=&quot;https://x.com/braindeaf/status/1825482461591024056&quot;&gt;found by Rob Lacey&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It turns out that local variable assignment in Ruby assigns nil even if the right part of the expression is not defined.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;irb(main):001&amp;gt; local_var = undefined_var
(irb):1:in `&amp;lt;main&amp;gt;&apos;: undefined local variable or method `undefined_var&apos; for main:Object (NameError)

local_var = undefined_var
            ^^^^^^^^^^^^^
  from /Users/igor/.rbenv/versions/3.2.3/lib/ruby/gems/3.2.0/gems/irb-1.13.2/exe/irb:9:in `&amp;lt;top (required)&amp;gt;&apos;
  from /Users/igor/.rbenv/versions/3.2.3/bin/irb:25:in `load&apos;
  from /Users/igor/.rbenv/versions/3.2.3/bin/irb:25:in `&amp;lt;main&amp;gt;&apos;
irb(main):002&amp;gt; local_var
=&amp;gt; nil
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;To be honest, I didn’t know about this behavior before.&lt;/p&gt;

&lt;h2 id=&quot;why-do-i-ask-this-in-interviews&quot;&gt;Why do I ask this in interviews?&lt;/h2&gt;

&lt;p&gt;In my original post, I mentioned that I ask this question in interviews. This triggered a discussion about whether it is a good idea to ask such questions in interviews.&lt;/p&gt;

&lt;p&gt;Yes, I believe that at the beginning of your career, it is crucial to understand how the language you are working with works. It is important to know all the nuances and dive as deep as you can. Even if the candidate doesn’t know the answer, which is usual and normal in entry-level positions, he can always demonstrate his thinking curve which, start discussion and try to explain the potential behavior. Focusing on how a candidate thinks and approaches problems can reveal a lot about their potential and willingness to learn, which is especially important early in their careers.&lt;/p&gt;

&lt;h2 id=&quot;conclusions&quot;&gt;Conclusions&lt;/h2&gt;

&lt;p&gt;Today you learned, or, I hope, you remembered, that every expression in Ruby has a value. Conditional statements in Ruby are expressions that return &lt;code&gt;nil&lt;/code&gt; if the conditional is &lt;code&gt;false&lt;/code&gt;.&lt;/p&gt;
</description>
        <pubDate>Fri, 30 Aug 2024 00:00:00 +0400</pubDate>
        <link>https://igor.works/blog/odd-ruby-return-values</link>
        <guid isPermaLink="true">https://igor.works/blog/odd-ruby-return-values</guid>
        
        <category>Ruby</category>
        
        <category>Rails</category>
        
        <category>Today You Learned</category>
        
        
      </item>
    
      <item>
        <title>Redirect and return in Rails controllers</title>
        <description>&lt;p&gt;Premature returning from a controller action is a common idiom in Rails applications. I &lt;a href=&quot;https://x.com/igor_alexandrov/status/1825463488954741103&quot;&gt;asked my followers&lt;/a&gt; in Twitter about whether they know or know how to do this correctly, and I am glad to see that most of them gave the correct answer. Let’s dive into the details.&lt;/p&gt;

&lt;!--more--&gt;

&lt;p&gt;Before we start, I suggest you to take a look at the post, I mentioned above, to see the answers and to try to solve the problem by yourself. If, for some reason, you can’t or don’t want to do this, I created a screenshot for you.&lt;/p&gt;

&lt;figure class=&quot;not-prose my-8 grid grid-cols-1 justify-items-center py-2&quot;&gt;
  &lt;img src=&quot;/assets/images/posts/2024-08-23/twitter-post.jpeg&quot; alt=&quot;Original Twitter post with answers&quot; style=&quot;max-width: 600px&quot; /&gt;

  
  &lt;figcaption class=&quot;mt-2 block text-center text-sm text-gray-600&quot;&gt;
    Original Twitter post with answers
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;p&gt;Most of those who votes made the right choice. &lt;strong&gt;Correct answers were options 1 and 2&lt;/strong&gt;. I usually use the first one, but the second option was also correct.&lt;/p&gt;

&lt;p&gt;To understand why the first and the second options are correct, I recommend you to read &lt;a href=&quot;https://ruby-doc.com/docs/ProgrammingRuby/language.html#table_18.4&quot;&gt;the table of operator precedence in Ruby&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&quot;-operator-in-options-2-and-3&quot;&gt;&amp;amp;&amp;amp; operator in options 2 and 3&lt;/h2&gt;

&lt;p&gt;Using &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; in options 2 and 3 is a bit tricky.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;redirect_to(sign_in_path) &amp;amp;&amp;amp; return
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;In the expression above, &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; operator get the result of &lt;code&gt;redirect_to(sign_in_path)&lt;/code&gt; as the left operand and &lt;code&gt;return&lt;/code&gt; as the right operand. This expression will work correct.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;redirect_to sign_in_path &amp;amp;&amp;amp; return
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;However, without explicit brackets, &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; get the result of &lt;code&gt;sign_in_path&lt;/code&gt; as the left operand and &lt;code&gt;return&lt;/code&gt; as the right operand. The value of the return keyword is &lt;code&gt;nil&lt;/code&gt;, so the expression above can be rewritten as:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;redirect_to sign_in_path &amp;amp;&amp;amp; nil
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Whatever &lt;code&gt;&amp;amp;&amp;amp; nil&lt;/code&gt; will return &lt;code&gt;nil&lt;/code&gt;, and &lt;code&gt;redirect_to&lt;/code&gt; will be called with &lt;code&gt;nil&lt;/code&gt; as an argument, which finally will raise an error.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;ActionController::ActionControllerError in TimeOffsController#new
Cannot redirect to nil!
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&quot;option-4&quot;&gt;Option 4&lt;/h2&gt;

&lt;p&gt;There was no way to vote for the fourth option directly, but there were a few who said that all options were correct.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;def show
  redirect_to sign_in_path unless current_user

  @time_off = current_user.time_offs.find(params[:id])

  render @time_off
end
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;In Rails, &lt;code&gt;redirect_to&lt;/code&gt; does only sets correct response headers and status code, but it doesn’t stop the execution of the action. In the example above, if &lt;code&gt;current_user&lt;/code&gt; is &lt;code&gt;nil&lt;/code&gt;, you will face DoubleRenderError.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;AbstractController::DoubleRenderError in TimeOffsController#new
Render and/or redirect were called multiple times in this action.
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&quot;upd-difference-between--and-and&quot;&gt;UPD. Difference between &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; and &lt;code&gt;and&lt;/code&gt;&lt;/h2&gt;

&lt;p&gt;Above in the post, I posted the link to the table of operator precedence in Ruby. Both &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; and &lt;code&gt;and&lt;/code&gt; are logical operators in Ruby, but they have different precedence, &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; has higher precedence than &lt;code&gt;and&lt;/code&gt;. This difference in precedence can lead to unexpected behavior when they are used in complex expressions.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;# Using &amp;amp;&amp;amp;
a = true &amp;amp;&amp;amp; false
b = false &amp;amp;&amp;amp; true

puts a # =&amp;gt; false
puts b # =&amp;gt; false

# Using and
c = true and false
d = false and true

puts c # =&amp;gt; true
puts d # =&amp;gt; false
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Pretty interesting, right?&lt;/p&gt;

&lt;p&gt;In the first case &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt;, the assignment happens after evaluating the expression, so &lt;code&gt;a&lt;/code&gt; and &lt;code&gt;b&lt;/code&gt; are both assigned the result of the logical operation &lt;code&gt;false&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In the second case &lt;code&gt;and&lt;/code&gt;, the assignment happens before the logical operation because &lt;code&gt;and&lt;/code&gt; has lower precedence than &lt;code&gt;=&lt;/code&gt;. So &lt;code&gt;c&lt;/code&gt; is first assigned true, and then the logical and operation is evaluated, which does not affect the assignment.&lt;/p&gt;

&lt;p&gt;Let’s take a look at the another example.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;# Using &amp;amp;&amp;amp;
a = b = false &amp;amp;&amp;amp; true
puts a # =&amp;gt; false
puts b # =&amp;gt; false

# Using and
c = d = false and true
puts c # =&amp;gt; false
puts d # =&amp;gt; false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt;, both &lt;code&gt;a&lt;/code&gt; and &lt;code&gt;b&lt;/code&gt; are assigned the result of &lt;code&gt;false &amp;amp;&amp;amp; true&lt;/code&gt;, which is &lt;code&gt;false&lt;/code&gt;.
With and, &lt;code&gt;d = false&lt;/code&gt; is evaluated first (due to and’s lower precedence), and then &lt;code&gt;c = d&lt;/code&gt; is assigned.&lt;/p&gt;

&lt;p&gt;You should be careful when using &lt;code&gt;and&lt;/code&gt; and &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; in complex expressions. It is better to use &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; for logical operations, while &lt;code&gt;and&lt;/code&gt; can be used to control flow.&lt;/p&gt;

&lt;h2 id=&quot;conclusions&quot;&gt;Conclusions&lt;/h2&gt;

&lt;p&gt;Despite the fact my post was about Ruby operator precedence, there were people who suggested that it is better to use &lt;code&gt;before_action&lt;/code&gt; for such kind of checks. I agree. I will try to be more explicit in my future posts and will provide more context.&lt;/p&gt;

&lt;p&gt;Nevertheless, I hope you learned something new. Stay tuned!&lt;/p&gt;
</description>
        <pubDate>Fri, 23 Aug 2024 00:00:00 +0400</pubDate>
        <link>https://igor.works/blog/redirect-and-return-in-rails-controllers</link>
        <guid isPermaLink="true">https://igor.works/blog/redirect-and-return-in-rails-controllers</guid>
        
        <category>Ruby</category>
        
        <category>Rails</category>
        
        <category>Today You Learned</category>
        
        
      </item>
    
      <item>
        <title>Upgrading Rails application from 7.1 to 7.2</title>
        <description>&lt;p&gt;On August 9th, &lt;a href=&quot;https://edgeguides.rubyonrails.org/7_2_release_notes.html&quot;&gt;Rails 7.2 was released&lt;/a&gt;, bringing a host of new features and improvements to the framework. Ten months ago, I demonstrated &lt;a href=&quot;https://igor.works/blog/upgrading-rails-application-from-70-to-71&quot;&gt;how to upgrade a Rails application from 7.0 to 7.1&lt;/a&gt;, which was well-received by the community. Today, I will take you through the process of upgrading OneTribe from Rails 7.1 to 7.2. This upgrade not only ensures compatibility with the latest Rails enhancements but also allows me to better understand new Rails functionalities and performance optimizations. Let’s dive into the upgrade process and explore what Rails 7.2 has to offer.&lt;/p&gt;

&lt;!--more--&gt;

&lt;h2 id=&quot;prerequisites&quot;&gt;Prerequisites&lt;/h2&gt;

&lt;p&gt;OneTribe runs Ruby 3.3.4 and Rails 7.1.3, which is good because Rails 7.2 requires Ruby 3.1.0 or newer. Nothing changed in code hosting and deployment, we still use GitHub and GitHub Actions with Kamal.&lt;/p&gt;

&lt;h2 id=&quot;dependencies-update&quot;&gt;Dependencies Update&lt;/h2&gt;

&lt;p&gt;The first you do to upgrade Rails is to change the version in the Gemfile.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;# Gemfile
# ...
gem &quot;rails&quot;, &quot;~&amp;gt; 7.2.0&quot;
# ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Than you do &lt;code&gt;bundle update rails&lt;/code&gt; and either you will be see that everything is fine or you will see that something is wrong. In my case, I got an error that &lt;code&gt;pg_party&lt;/code&gt; does not support Rails 7.2 yet. I checked the &lt;a href=&quot;https://github.com/rkrage/pg_party&quot;&gt;gem’s repository&lt;/a&gt; and saw that there was already a PR with support for Rails 7.2, but it has not yet been merged. I decided to use the code from the PR.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;# Gemfile
# ...
gem &quot;pg_party&quot;, github: &quot;marcoroth/pg_party&quot;, branch: &quot;rails-7.2&quot;
# ...
gem &quot;rails&quot;, &quot;~&amp;gt; 7.2.0&quot;
# ...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;After I fixed the issue, I tried to run specs with &lt;code&gt;./bin/rspec&lt;/code&gt; and got a strange error.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;An error occurred while loading ./spec/workers/member/next_birthday_worker_spec.rb.
Failure/Error: require File.expand_path(&apos;../config/environment&apos;, __dir__)

NameError:
  undefined method `validate_find_options&apos; for class `#&amp;lt;Class:ActiveRecord::Base&amp;gt;&apos;
# ./config/application.rb:22:in `&amp;lt;top (required)&amp;gt;&apos;
# ./config/environment.rb:4:in `require_relative&apos;
# ./config/environment.rb:4:in `&amp;lt;top (required)&amp;gt;&apos;
# ./spec/rails_helper.rb:6:in `&amp;lt;top (required)&amp;gt;&apos;
# ./spec/workers/member/next_birthday_worker_spec.rb:3:in `&amp;lt;top (required)&amp;gt;&apos;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;I ran &lt;code&gt;./bin/rspec -b&lt;/code&gt; to see the backtrace and found that the error was caused by the &lt;code&gt;acts_as_paranoid&lt;/code&gt; gem. We used very old version of the gem, so I updated it to the latest version and bundled.&lt;/p&gt;

&lt;p&gt;I found out that I didn’t migrate the database when I fetched the latest changes from the repository. I ran &lt;code&gt;./bin/rails db:migrate&lt;/code&gt; and got another error.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;➜  onetribe git:(rails-7-2) ✗ ./bin/rails db:migrate
Rodauth::Rails.authenticated has been deprecated in favor of Rodauth::Rails.authenticate, which additionally requires existence of the account record.
bin/rails aborted!
NoMethodError: undefined method `with_connection&apos; for an instance of ActiveRecord::ConnectionAdapters::PostgreSQLAdapter (NoMethodError)

        pool.with_connection do |connection|
            ^^^^^^^^^^^^^^^^
Did you mean?  raw_connection

Tasks: TOP =&amp;gt; db:schema:dump
(See full trace by running task with --trace)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;To debug this I again used  &lt;code&gt;--backtrace&lt;/code&gt; option.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;➜  onetribe git:(rails-7-2) ✗ ./bin/rails db:migrate --backtrace
Rodauth::Rails.authenticated has been deprecated in favor of Rodauth::Rails.authenticate, which additionally requires existence of the account record.
bin/rails aborted!
NoMethodError: undefined method `with_connection&apos; for an instance of ActiveRecord::ConnectionAdapters::PostgreSQLAdapter (NoMethodError)

        pool.with_connection do |connection|
            ^^^^^^^^^^^^^^^^
Did you mean?  raw_connection
/Users/igor/.rbenv/versions/3.3.0/lib/ruby/gems/3.3.0/gems/activerecord-7.2.0/lib/active_record/schema_dumper.rb:45:in `dump&apos;
/Users/igor/.rbenv/versions/3.3.0/lib/ruby/gems/3.3.0/gems/hairtrigger-1.0.0/lib/tasks/hair_trigger.rake:18:in `block (4 levels) in &amp;lt;main&amp;gt;&apos;
/Users/igor/.rbenv/versions/3.3.0/lib/ruby/gems/3.3.0/gems/hairtrigger-1.0.0/lib/tasks/hair_trigger.rake:17:in `open&apos;
/Users/igor/.rbenv/versions/3.3.0/lib/ruby/gems/3.3.0/gems/hairtrigger-1.0.0/lib/tasks/hair_trigger.rake:17:in `block (3 levels) in &amp;lt;main&amp;gt;&apos;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Backtrace helps quickly identify the issue. In my case it was the &lt;code&gt;hairtrigger&lt;/code&gt; gem that was outdated and not compatible with Rails 7.2.&lt;/p&gt;

&lt;p&gt;Finally, I was able to migrate and run specs successfully with &lt;code&gt;./bin/rspec&lt;/code&gt;.&lt;/p&gt;

&lt;h2 id=&quot;third-party-deprecations&quot;&gt;Third-party Deprecations&lt;/h2&gt;

&lt;p&gt;During RSpec run I saw a couple of deprecation warnings and decided to fix them before going further.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;DEPRECATION WARNING: ActiveRecord::ConnectionAdapters::ConnectionPool#connection is deprecated
and will be removed in Rails 8.0. Use #lease_connection instead.
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;I don’t use &lt;code&gt;connection&lt;/code&gt; directly in the code, so the message was caused by some gem. The problem was that I had no idea which gem caused the warning. How would you handle this? I found out that &lt;a href=&quot;https://guides.rubyonrails.org/7_1_release_notes.html#add-rails-application-deprecators&quot;&gt;starting from a Rails 7.1&lt;/a&gt; there is a &lt;code&gt;Rails.application.deprecators&lt;/code&gt; API, which can be extremely useful in this case.&lt;/p&gt;

&lt;p&gt;I updated my &lt;code&gt;config/application.rb&lt;/code&gt; file to include the following code:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;# config/application.rb

module OneTribe
  class Application &amp;lt; Rails::Application
    # ...
    deprecators.debug = true if ENV[&quot;DEPRECATION_DEBUG&quot;]
  end
end
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Then I ran the specs with &lt;code&gt;DEPRECATION_DEBUG=true ./bin/rspec&lt;/code&gt; and saw the following output:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;DEPRECATION WARNING: ActiveRecord::ConnectionAdapters::ConnectionPool#connection is deprecated
and will be removed in Rails 8.0. Use #lease_connection instead.
 (called from load at ./bin/rspec:27)
/Users/igor/.rbenv/versions/3.3.0/lib/ruby/gems/3.3.0/gems/test-prof-1.3.3.1/lib/test_prof/before_all/adapters/active_record.rb:15:in `block in all_connections&apos;
  /Users/igor/.rbenv/versions/3.3.0/lib/ruby/gems/3.3.0/gems/test-prof-1.3.3.1/lib/test_prof/before_all/adapters/active_record.rb:13:in `map&apos;
  /Users/igor/.rbenv/versions/3.3.0/lib/ruby/gems/3.3.0/gems/test-prof-1.3.3.1/lib/test_prof/before_all/adapters/active_record.rb:13:in `all_connections&apos;
  /Users/igor/.rbenv/versions/3.3.0/lib/ruby/gems/3.3.0/gems/test-prof-1.3.3.1/lib/test_prof/before_all/adapters/active_record.rb:36:in `begin_transaction&apos;
  /Users/igor/.rbenv/versions/3.3.0/lib/ruby/gems/3.3.0/gems/test-prof-1.3.3.1/lib/test_prof/before_all.rb:24:in `block in begin_transaction&apos;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now everything was clear. The warning was caused by the &lt;code&gt;test-prof&lt;/code&gt; gem. I decided to ignore it for now and continue with the upgrade.&lt;/p&gt;

&lt;h2 id=&quot;rails-deprecations&quot;&gt;Rails Deprecations&lt;/h2&gt;

&lt;p&gt;Another deprecation warning I saw was:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;➜  onetribe git:(rails-7-2) ✗ ./bin/rspec

DEPRECATION WARNING: Defining enums with keyword arguments is deprecated and will be removed
in Rails 8.0. Positional arguments should be used instead:

enum :role, {:employee=&amp;gt;&quot;employee&quot;, :manager=&amp;gt;&quot;manager&quot;, :administrator=&amp;gt;&quot;administrator&quot;}
 (called from &amp;lt;class:Member&amp;gt; at /Users/igor/workspace/onetribe/app/models/member.rb:14)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;In mid-February 2024, there was a &lt;a href=&quot;https://github.com/rails/rails/pull/50987&quot;&gt;PR that deprecated defining enums with keyword arguments&lt;/a&gt;. Before it was possible to define enums like this:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;# app/models/time_off/slot.rb


class TimeOff::Slot &amp;lt; ApplicationRecord
  # ...

  enum allocation_type: { general: 0, extra: 1, auto: 2 }, _prefix: :allocation_type
end
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;With this change you should define enums like this:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-ruby&quot;&gt;# app/models/time_off/slot.rb

class TimeOff::Slot &amp;lt; ApplicationRecord
  # ...

  enum :allocation_type, { general: 0, extra: 1, auto: 2 }, prefix: :allocation_type
end
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This new syntax eliminates the need to prefix options with an underscore.&lt;/p&gt;

&lt;p&gt;For some reason, this change wasn’t mentioned in the Rails 7.2 release notes, but it’s good to know about it.&lt;/p&gt;

&lt;h2 id=&quot;application-configuration-update&quot;&gt;Application Configuration Update&lt;/h2&gt;

&lt;p&gt;Rails has a special task &lt;code&gt;rails app:update&lt;/code&gt; that can help you to update application configuration in an interactive mode. I use VS Code for development and wanted to use its merge tool, so as in the previous update, I specified THOR_MERGE constant before running the command &lt;code&gt;THOR_MERGE=&quot;code --wait&quot; ./bin/rails app:update&lt;/code&gt; and used merge tool (&lt;code&gt;m&lt;/code&gt; option) to track changes over files.&lt;/p&gt;

&lt;p&gt;During the update I found only one notable configration change.&lt;/p&gt;

&lt;p&gt;Annotation of views with filesnames is now anabled by default in development. This setting is handled by &lt;code&gt;config.action_view.annotate_rendered_view_with_filenames = true&lt;/code&gt; in &lt;code&gt;config/environments/development.rb&lt;/code&gt;.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;h3 class=&quot;mb-4 text-lg font-medium text-center&quot;&amp;gt;
  Team Schedule
&amp;lt;/h3&amp;gt;

&amp;lt;!-- BEGIN app/views/companies/time_offs/shared/_monthly_calendar.html.slim --&amp;gt;
&amp;lt;div id=&quot;monthly_calendar&quot;&amp;gt;
  &amp;lt;!-- ... --&amp;gt;
&amp;lt;/div&amp;gt;
&amp;lt;!-- END app/views/companies/time_offs/shared/_monthly_calendar.html.slim --&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;With this setting enabled, you will see comments like this in the HTML source of the page. I am not sure if it’s useful, but it’s good to know about it.&lt;/p&gt;

&lt;h2 id=&quot;app-defaults&quot;&gt;App Defaults&lt;/h2&gt;

&lt;p&gt;After you merge all the changes, you can run the specs again to make sure everything is fine. Now it is time to make sure that your application is ready to run with Rails default settings, applied to every new Rails 7.2 application.&lt;/p&gt;

&lt;p&gt;This can be done in two steps. In &lt;code&gt;config/initializers&lt;/code&gt; there should be a new file named &lt;code&gt;new_framework_defaults_7_2.rb&lt;/code&gt;. It includes all Rails 7.2 default params commented, so you can enable them one by one and make sure that your specs are still green and your application is still working.&lt;/p&gt;

&lt;p&gt;After after, you can change &lt;code&gt;config.load_defaults&lt;/code&gt; in &lt;code&gt;config/application.rb&lt;/code&gt; file to have &lt;code&gt;7.2&lt;/code&gt; value and delete &lt;code&gt;new_framework_defaults_7_2.rb&lt;/code&gt;. This will enable all options at once.&lt;/p&gt;

&lt;h2 id=&quot;development-container&quot;&gt;Development Container&lt;/h2&gt;

&lt;p&gt;One new feature of Rails 7.2 that deserves to be mentioned in this text is format support of Docker development containers. Yes, nobody prevented you from using dev container before Rails 7.2, but now you can generate container configuration for new apps with &lt;code&gt;rails new myapp --devcontainer&lt;/code&gt; command. This will create a new Rails app with the &lt;code&gt;.devcontainer&lt;/code&gt; folder that includes &lt;code&gt;devcontainer.json&lt;/code&gt;, &lt;code&gt;Dockerfile&lt;/code&gt; and &lt;code&gt;docker-compose.yml&lt;/code&gt; files.&lt;/p&gt;

&lt;p&gt;For applications that were upgrade you can use &lt;code&gt;./bin/rails devcontainer&lt;/code&gt; console command to generate the same files.&lt;/p&gt;

&lt;h2 id=&quot;conclusions&quot;&gt;Conclusions&lt;/h2&gt;

&lt;p&gt;Since 7.2 is a minor release, there are not many changes and new features, however the &lt;a href=&quot;https://edgeguides.rubyonrails.org/7_2_release_notes.html&quot;&gt;Release Notes are rather long&lt;/a&gt;. I will not cover everything here, but I suggest you to read them. Maybe I will write a separate post about some of the new features.&lt;/p&gt;

&lt;p&gt;Stay tuned!&lt;/p&gt;
</description>
        <pubDate>Sun, 11 Aug 2024 00:00:00 +0400</pubDate>
        <link>https://igor.works/blog/upgrading-rails-application-from-71-to-72</link>
        <guid isPermaLink="true">https://igor.works/blog/upgrading-rails-application-from-71-to-72</guid>
        
        <category>Ruby</category>
        
        <category>Rails</category>
        
        
      </item>
    
      <item>
        <title>Happy Birthday, Rails! 🎉</title>
        <description>&lt;p&gt;Fifteen years ago, in August 2009, I worked for a small network provider company. I was a junior developer, yes 15 years ago, junior devs didn’t become seniors in 3 months. We used Dolphin SmallTalk for our internal systems, and I had the task of creating a simple web application for our customers. Also in the summer, but 20 years ago in July 2004, David Heinemeier Hansson &lt;a href=&quot;https://rubytalk.org/t/ann-rails-0-5-0-the-end-of-vaporware/12744&quot;&gt;released the first version&lt;/a&gt; of Ruby on Rails. I watched &lt;a href=&quot;https://www.youtube.com/watch?v=Gzj723LkRJY&quot;&gt;this video&lt;/a&gt;, and my life has changed.&lt;/p&gt;

&lt;!--more--&gt;

&lt;p&gt;Rails was a game-changer for me. I’ve been working with PHP and Java before, but Rails was different. It was a framework that was built with developers in mind. It was a framework that was built to make developers happy. And it did. It made me happy.&lt;/p&gt;

&lt;p&gt;Let me explain, fifteen years back, web development was a bit different. We didn’t have all these fancy tools and libraries that we have today. We didn’t have Webpacker, we didn’t have Yarn, we didn’t have Tailwind CSS. Everything was overcomplicated. With Java, you had to start by reading a 600-page book, understanding the concept of Servlets, JSP, and J2EE, and god knows what else J. PHP was everywhere, but it was a mess. Every developer started their path by writing his authentication solution. You had to write your own ORM, you had to write your own MVC framework, and you had to write almost everything from scratch.&lt;/p&gt;

&lt;p&gt;You can say, “What about Django and ASP.NET, dude?”. Yes, they were there. But ASP.NET was a Microsoft thing, and I was a Linux guy. Without .NET Core it was impossible to run ASP.NET on Linux. And, yes, there was &lt;a href=&quot;https://www.mono-project.com&quot;&gt;Mono&lt;/a&gt;, and as far as I remember, it was possible to run ASP.NET on Linux, but the phrase “OpenSource sponsored by Microsoft” sounded like a joke those days.&lt;/p&gt;

&lt;p&gt;Django was a good thing to use for web dev. Compared to everything else it was a breakthrough. And I tried it, but two things made me stick with Rails. First, Django is Python. After using SmallTalk, where everything is an object, Python felt like a more multi-paradigm language, which it is. The second thing is that Django does a lot of things too complicated. I remember my impressions from the routing based on regular expressions. I am not saying that it is bad, of course not. I just needed something more straightforward.&lt;/p&gt;

&lt;p&gt;And Rails was it. It was more straightforward because it was built on the principle of “Convention over Configuration”. It was more straightforward because it was built on the principle of “Don’t Repeat Yourself”. It was more straightforward because it was built on the principle of “You Ain’t Gonna Need It”. It was more straightforward because it was built on the principle of “Keep It Simple, Stupid”.&lt;/p&gt;

&lt;p&gt;Rails meets its 20th birthday in good shape. Yes, like every software, it has its problems, it becomes &lt;a href=&quot;https://survey.stackoverflow.co/2024/technology#1-web-frameworks-and-technologies&quot;&gt;more and less popular&lt;/a&gt; from time to time. It has fans and haters. But it is still here. It is still my tool to make money and to make me happy. And I am grateful for that.&lt;/p&gt;

&lt;p&gt;Happy Birthday, Rails! 🎉&lt;/p&gt;
</description>
        <pubDate>Sat, 03 Aug 2024 00:00:00 +0400</pubDate>
        <link>https://igor.works/blog/happy-birthday-rails</link>
        <guid isPermaLink="true">https://igor.works/blog/happy-birthday-rails</guid>
        
        <category>Rails</category>
        
        <category>Ruby</category>
        
        
      </item>
    
  </channel>
</rss>
