davideisinger.com

My personal website
Log | Files | Refs | README

evilmartians-com-6ehmrb.txt (54068B)


      1 [1]
      2 [2]Hire Martians
      3 [3]
      4 Services
      5 [4]
      6 Clients
      7 [5]
      8 Products
      9 [6]
     10 Open Source
     11 [7]
     12 Blog
     13 [8]
     14 Events
     15 [9]
     16 Podcast
     17 [10]
     18 [11]
     19 [12]
     20 [13]
     21 [14]
     22 [15]Hire Martians
     23 [16]
     24 Services
     25 [17]
     26 Clients
     27 [18]
     28 Products
     29 [19]
     30 Open Source
     31 [20]
     32 Blog
     33 [21]
     34 Events
     35 [22]
     36 Podcast
     37 [23]
     38 [24]
     39 [25]
     40 [26]
     41 [27]
     42 7-
     43 Oct
     44 8
     45 Meet us at Rocky Mountain Ruby in Boulder, Colorado!
     46 [28]Hire Martians
     47 [29]
     48 [30]
     49 [31]
     50 [32]
     51 
     52 Ruby on Whales: Dockerizing Ruby and Rails development
     53 
     54 March 15, 2022
     55 [svg]
     56 Cover for Ruby on Whales: Dockerizing Ruby and Rails developmentCover for Ruby
     57 on Whales: Dockerizing Ruby and Rails development
     58 
     59 Topics
     60 
     61   • [33]Backend
     62   • [34]Full Cycle Software Development
     63   • [35]Performance Optimization
     64   • [36]Ruby on Rails
     65   • [37]Ruby
     66   • [38]Docker
     67   • [39]PostgreSQL
     68   • [40]Node.js
     69 
     70 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
     71 
     72   • [svg]
     73     Vladimir Dementyev
     74 
     75     Vladimir Dementyev
     76 
     77     Principal Backend Engineer
     78 
     79 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
     80 
     81 Translations
     82 
     83   • Japanese[41]クジラに乗ったRuby: Evil Martians流Docker+Ruby/Rails開発環境構
     84     85   • Chinese[42]骑鲸之路——Docker模式下的Rails开发环境构筑
     86 
     87 This post introduces the Docker configuration I use for developing my Ruby on
     88 Rails projects. This configuration came out of—and then further evolved—during
     89 [DEL:production:DEL] development at Evil Martians. Read on to learn all the
     90 details, and feel free to use it, share it, and enjoy!
     91 
     92 Notice: This article is regularly updated with the best and latest
     93 recommendations; for details, take a look at the [43]Changelog.
     94 
     95 So, where to start? This has been a pretty long journey: back in the day, I
     96 used to develop using Vagrant, but its VMs were a bit too heavy for my 4GB RAM
     97 laptop. In 2017, I decided to make the switch to containers, and this was how I
     98 first began using Docker. But don’t get the impression that this was an instant
     99 fix! I was in search of a configuration that was perfect for myself, my team,
    100 and well, everyone else. And something which was just good enough would not cut
    101 it. It took quite some time to develop a standard approach (as more formerly
    102 enshrined with the first release of this article in 2019). Since that first
    103 iteration of this post revealed my secret to the world, many Rails teams and
    104 devs have adopted my technique, and actually, they’ve helped to contribute and
    105 improve it!
    106 
    107 With that out of the way, let me just go ahead and present the config itself.
    108 Along the way, I’ll explain almost every line (because we’ve all had enough of
    109 those cryptic tutorials that just assume you know stuff).
    110 
    111 This post was originally adapted from my talk at RailsConf 2019: [44]
    112 “Terraforming legacy Rails applications”.
    113 
    114     The source code can be found in the [45]evilmartians/ruby-on-whales
    115     repository on GitHub.
    116 
    117 Before we get on with it, let’s note that we’ll be using up-to-date software
    118 versions for this example: Docker Desktop 20.10+ and Docker Compose v2,
    119 Ruby 3.1.0, PostgreSQL 14, etc.
    120 
    121 The bulk of the post consists mostly of annotated code and configuration
    122 examples, structured as follows:
    123 
    124   • [46]The basics: Dockerfile and docker-compose.yml
    125   • [47]Introducing Dip
    126   • [48](Micro-)services vs Docker for development
    127   • [49]From development to production
    128   • [50]Introducing the Ruby on Whales interactive generator
    129 
    130 Basic Docker configuration
    131 
    132 [51]Dockerfile
    133 
    134 The Dockerfile defines our Ruby application’s environment. This environment is
    135 where we’ll run servers, access the console (rails c), perform tests, do Rake
    136 tasks, and otherwise interact with our code in any way as developers:
    137 
    138 # syntax=docker/dockerfile:1
    139 
    140 ARG RUBY_VERSION
    141 ARG DISTRO_NAME=bullseye
    142 
    143 FROM ruby:$RUBY_VERSION-slim-$DISTRO_NAME
    144 
    145 ARG DISTRO_NAME
    146 
    147 # Common dependencies
    148 RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
    149   --mount=type=cache,target=/var/lib/apt,sharing=locked \
    150   --mount=type=tmpfs,target=/var/log \
    151   rm -f /etc/apt/apt.conf.d/docker-clean; \
    152   echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache; \
    153   apt-get update -qq \
    154   && DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends \
    155     build-essential \
    156     gnupg2 \
    157     curl \
    158     less \
    159     git
    160 
    161 # Install PostgreSQL dependencies
    162 ARG PG_MAJOR
    163 RUN curl -sSL https://www.postgresql.org/media/keys/ACCC4CF8.asc | \
    164     gpg --dearmor -o /usr/share/keyrings/postgres-archive-keyring.gpg \
    165     && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/postgres-archive-keyring.gpg] https://apt.postgresql.org/pub/repos/apt/" \
    166     $DISTRO_NAME-pgdg main $PG_MAJOR | tee /etc/apt/sources.list.d/postgres.list > /dev/null
    167 RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
    168   --mount=type=cache,target=/var/lib/apt,sharing=locked \
    169   --mount=type=tmpfs,target=/var/log \
    170   apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get -yq dist-upgrade && \
    171   DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends \
    172     libpq-dev \
    173     postgresql-client-$PG_MAJOR
    174 
    175 # Install NodeJS and Yarn
    176 ARG NODE_MAJOR
    177 ARG YARN_VERSION=latest
    178 RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
    179     --mount=type=cache,target=/var/lib/apt,sharing=locked \
    180     --mount=type=tmpfs,target=/var/log \
    181     apt-get update && \
    182     apt-get install -y curl software-properties-common && \
    183     curl -fsSL https://deb.nodesource.com/gpgkey/nodesource.gpg.key | apt-key add - && \
    184     echo "deb https://deb.nodesource.com/node_${NODE_MAJOR}.x $(lsb_release -cs) main" | tee /etc/apt/sources.list.d/nodesource.list && \
    185     apt-get update && \
    186     DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends nodejs
    187 RUN npm install -g yarn@$YARN_VERSION
    188 
    189 # Application dependencies
    190 # We use an external Aptfile for this, stay tuned
    191 COPY Aptfile /tmp/Aptfile
    192 RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
    193   --mount=type=cache,target=/var/lib/apt,sharing=locked \
    194   --mount=type=tmpfs,target=/var/log \
    195   apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get -yq dist-upgrade && \
    196   DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends \
    197     $(grep -Ev '^\s*#' /tmp/Aptfile | xargs)
    198 
    199 # Configure bundler
    200 ENV LANG=C.UTF-8 \
    201   BUNDLE_JOBS=4 \
    202   BUNDLE_RETRY=3
    203 
    204 # Store Bundler settings in the project's root
    205 ENV BUNDLE_APP_CONFIG=.bundle
    206 
    207 # Uncomment this line if you want to run binstubs without prefixing with `bin/` or `bundle exec`
    208 # ENV PATH /app/bin:$PATH
    209 
    210 # Upgrade RubyGems and install the latest Bundler version
    211 RUN gem update --system && \
    212     gem install bundler
    213 
    214 # Create a directory for the app code
    215 RUN mkdir -p /app
    216 WORKDIR /app
    217 
    218 # Document that we're going to expose port 3000
    219 EXPOSE 3000
    220 # Use Bash as the default command
    221 CMD ["/bin/bash"]
    222 
    223 This configuration only contains the essentials, and so it can be used as a
    224 starting point. Let me illustrate what we’re are doing here a bit further. The
    225 first three lines might look a bit strange:
    226 
    227 ARG RUBY_VERSION
    228 ARG DISTRO_NAME=bullseye
    229 FROM ruby:$RUBY_VERSION-slim-$DISTRO_NAME
    230 
    231 Why not just use FROM ruby:3.1.0, or whatever is the stable Ruby version du
    232 jour? Well, we’re going this route because we want to make our environment
    233 configurable from the outside using Dockerfile as a sort of a template:
    234 
    235   • The exact versions of the runtime dependencies are specified in the 
    236     docker-compose.yml (see below 👇).
    237   • The list of apt-installable dependencies is stored in a separate file
    238     (also, see below 👇👇).
    239 
    240 Additionally, we parameterize the Debian release (bullseye by default) to make
    241 sure we’re adding the correct sources for our other dependencies (such as
    242 PostgreSQL).
    243 
    244 Alright, now, note that we declare the argument once again after the FROM
    245 statement:
    246 
    247 FROM ruby:$RUBY_VERSION-slim-$DISTRO_NAME
    248 ARG DISTRO_NAME
    249 
    250 That’s the tricky part of how Dockerfiles work: the args are reset after the 
    251 FROM statement. For more details, check out [52]this issue.
    252 
    253 Moving on, the rest of the file contains the actual build steps. First, we’ll
    254 need to manually install some common system dependencies (Git, cURL, etc.), as
    255 we’re using the slim base Docker image to reduce the size:
    256 
    257 # Common dependencies
    258 RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
    259   --mount=type=cache,target=/var/lib/apt,sharing=locked \
    260   --mount=type=tmpfs,target=/var/log \
    261   apt-get update -qq \
    262   && DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends \
    263     build-essential \
    264     gnupg2 \
    265     curl \
    266     less \
    267     git
    268 
    269 We’ll explain all the details of installing system dependencies below,
    270 alongside the application-specific dependencies.
    271 
    272 Installing PostgreSQL and NodeJS via apt requires adding their deb package
    273 repos to the sources list.
    274 
    275 Here’s PostgreSQL (based on the [53]official documentation):
    276 
    277 ARG PG_MAJOR
    278 RUN curl -sSL https://www.postgresql.org/media/keys/ACCC4CF8.asc | \
    279     gpg --dearmor -o /usr/share/keyrings/postgres-archive-keyring.gpg \
    280     && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/postgres-archive-keyring.gpg] https://apt.postgresql.org/pub/repos/apt/" \
    281     $DISTRO_NAME-pgdg main $PG_MAJOR | tee /etc/apt/sources.list.d/postgres.list > /dev/null
    282 RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
    283   --mount=type=cache,target=/var/lib/apt,sharing=locked \
    284   --mount=type=tmpfs,target=/var/log \
    285   apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get -yq dist-upgrade && \
    286   DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends \
    287     libpq-dev \
    288     postgresql-client-$PG_MAJOR
    289 
    290 Since we aren’t expecting anyone to use this Dockerfile without [54]
    291 Docker Compose, we don’t provide a default value for the PG_MAJOR argument (the
    292 same applies to NODE_MAJOR below, and YARN_VERSION further below).
    293 
    294 Also, notice that in the code above that the DISTRO_NAME argument which we
    295 defined at the very beginning of the file comes back into play.
    296 
    297 And, we repeat our apt-get ... apt-get clean spell again: we want to make sure
    298 all the major pieces of our environment are built in an isolated way (this will
    299 help us to better utilize Docker cache layers when performing upgrades).
    300 
    301 For NodeJS (from the [55]NodeSource repo):
    302 
    303 ARG NODE_MAJOR
    304 RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
    305     --mount=type=cache,target=/var/lib/apt,sharing=locked \
    306     --mount=type=tmpfs,target=/var/log \
    307     apt-get update && \
    308     apt-get install -y curl software-properties-common && \
    309     curl -fsSL https://deb.nodesource.com/gpgkey/nodesource.gpg.key | apt-key add - && \
    310     echo "deb https://deb.nodesource.com/node_${NODE_MAJOR}.x $(lsb_release -cs) main" | tee /etc/apt/sources.list.d/nodesource.list && \
    311     apt-get update && \
    312     DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends nodejs
    313 
    314 Then, we install Yarn via NPM:
    315 
    316 ARG YARN_VERSION=latest
    317 RUN npm install -g yarn@$YARN_VERSION
    318 
    319 So, why are we adding NodeJS and Yarn in the first place? Although Rails 7
    320 allows you to [56]go Node-less via [57]import maps or precompiled binaries
    321 (like [58]tailwindcss-rails), these additions increase the chances of
    322 supporting legacy pipelines or adding modern Webpacker alternatives.
    323 
    324 Now it’s time to install the application-specific dependencies:
    325 
    326 COPY Aptfile /tmp/Aptfile
    327 RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
    328   --mount=type=cache,target=/var/lib/apt,sharing=locked \
    329   --mount=type=tmpfs,target=/var/log \
    330   apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get -yq dist-upgrade && \
    331   DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends \
    332     $(grep -Ev '^\s*#' /tmp/Aptfile | xargs)
    333 
    334 Let’s talk about that Aptfile trick a bit:
    335 
    336 COPY Aptfile /tmp/Aptfile
    337 RUN apt-get install \
    338     $(grep -Ev '^\s*#' /tmp/Aptfile | xargs) \
    339 
    340 I borrowed this idea from [59]heroku-buildpack-apt, which allows for installing
    341 additional packages on Heroku. If you’re using this buildpack, you can even
    342 re-use the same Aptfile for both the local and the production environment.
    343 
    344 Our [60]default Aptfile contains only a single package (we’ll use Vim to edit
    345 the Rails Credentials):
    346 
    347 vim
    348 
    349 In one of the previous projects I worked on, we generated PDFs using LaTeX and 
    350 [61]TexLive. In a case like that, our Aptfile might look like this:
    351 
    352 vim
    353 # TeX packages
    354 texlive
    355 texlive-latex-recommended
    356 texlive-fonts-recommended
    357 texlive-lang-cyrillic
    358 
    359 By doing this we can keep task-specific dependencies in a separate file, thus
    360 making our Dockerfile more universal.
    361 
    362 With regards to DEBIAN_FRONTEND=noninteractive, I kindly ask you to take a look
    363 at this [62]answer on Ask Ubuntu.
    364 
    365 The --no-install-recommends option helps save some space (and makes our image
    366 smaller) by disabling the installation of recommended packages. You can see
    367 more [63]about saving disk space here.
    368 
    369 That first (fairly cryptic) part of every RUN statement that installs packages
    370 also serves the same purpose: it moves out the local repository of retrieved
    371 package files into a cache that will be preserved between builds. We need this
    372 magic to be in every RUN statement that installs packages to make sure this
    373 particular [64]Docker layer doesn’t contain any garbage. It also greatly speeds
    374 up image build!
    375 
    376 [65]RUN --mount is relatively new feature of Docker. Traditionally, every
    377 package installation step would be appended with a dark spell full of rm and 
    378 truncate commands to clean up temporary files.
    379 
    380 The final part of the Dockerfile is mostly devoted to Bundler:
    381 
    382 # Configure bundler
    383 ENV LANG=C.UTF-8 \
    384   BUNDLE_JOBS=4 \
    385   BUNDLE_RETRY=3 \
    386 
    387 # Store Bundler settings in the project's root
    388 ENV BUNDLE_APP_CONFIG=.bundle
    389 
    390 # Uncomment this line if you want to run binstubs without prefixing with `bin/` or `bundle exec`
    391 # ENV PATH /app/bin:$PATH
    392 
    393 # Upgrade RubyGems and install the latest Bundler version
    394 RUN gem update --system && \
    395     gem install bundler
    396 
    397 Using LANG=C.UTF-8 sets the default locale to UTF-8. This is an emotional
    398 setting, as otherwise, Ruby would use US-ASCII for strings—and that’d mean
    399 waving goodbye to those sweet, sweet emojis! 👋
    400 
    401 Setting BUNDLE_APP_CONFIG is required if you’ll use the <root>/.bundle folder
    402 to store project-specicic Bundler settings (like credentials for private gems).
    403 The default Ruby image [66]defines this variable so Bundler doesn’t fall back
    404 to the local config.
    405 
    406 Optionally, you can add your <root>/bin folder to the PATH in order to run
    407 commands without bundle exec. We don’t do this by default, because it could
    408 break in a multi-project environment (for instance, when you have local gems or
    409 engines in your Rails app).
    410 
    411 Previously, we also had to specify the Bundler version (taking advantage of 
    412 [67]some hacks to make sure it’s picked up by the system). Luckily, since
    413 Bundler 2.3.0, we no longer need to manually install the version defined in the
    414 Gemfile.lock (BUNDLED_WITH). Instead, to avoid conflicts, Bundler [68]does this
    415 for us.
    416 
    417 [69]compose.yml
    418 
    419 [70]Docker Compose is a tool we can use to orchestrate our containerized
    420 environment. It allows us to link containers to each other, and to define
    421 persistent volumes and services.
    422 
    423 Below is the compose file for developing a typical Rails application with
    424 PostgreSQL as the database, and with Sidekiq as the background job processor:
    425 
    426 x-app: &app
    427   build:
    428     context: .
    429     args:
    430       RUBY_VERSION: '3.2.2'
    431       PG_MAJOR: '15'
    432       NODE_MAJOR: '18'
    433   image: example-dev:1.0.0
    434   environment: &env
    435     NODE_ENV: ${NODE_ENV:-development}
    436     RAILS_ENV: ${RAILS_ENV:-development}
    437   tmpfs:
    438     - /tmp
    439     - /app/tmp/pids
    440 
    441 x-backend: &backend
    442   <<: *app
    443   stdin_open: true
    444   tty: true
    445   volumes:
    446     - ..:/app:cached
    447     - bundle:/usr/local/bundle
    448     - rails_cache:/app/tmp/cache
    449     - node_modules:/app/node_modules
    450     - packs:/app/public/packs
    451     - packs-test:/app/public/packs-test
    452     - history:/usr/local/hist
    453     - ./.psqlrc:/root/.psqlrc:ro
    454     - ./.bashrc:/root/.bashrc:ro
    455   environment: &backend_environment
    456     <<: *env
    457     REDIS_URL: redis://redis:6379/
    458     DATABASE_URL: postgres://postgres:postgres@postgres:5432
    459     WEBPACKER_DEV_SERVER_HOST: webpacker
    460     MALLOC_ARENA_MAX: 2
    461     WEB_CONCURRENCY: ${WEB_CONCURRENCY:-1}
    462     BOOTSNAP_CACHE_DIR: /usr/local/bundle/_bootsnap
    463     XDG_DATA_HOME: /app/tmp/caches
    464     YARN_CACHE_FOLDER: /app/node_modules/.yarn-cache
    465     HISTFILE: /usr/local/hist/.bash_history
    466     PSQL_HISTFILE: /usr/local/hist/.psql_history
    467     IRB_HISTFILE: /usr/local/hist/.irb_history
    468     EDITOR: vi
    469   depends_on: &backend_depends_on
    470     postgres:
    471       condition: service_healthy
    472     redis:
    473       condition: service_healthy
    474 
    475 services:
    476   rails:
    477     <<: *backend
    478     command: bundle exec rails
    479 
    480   web:
    481     <<: *backend
    482     command: bundle exec rails server -b 0.0.0.0
    483     ports:
    484       - '3000:3000'
    485     depends_on:
    486       webpacker:
    487         condition: service_started
    488       sidekiq:
    489         condition: service_started
    490 
    491   sidekiq:
    492     <<: *backend
    493     command: bundle exec sidekiq -C config/sidekiq.yml
    494 
    495   postgres:
    496     image: postgres:14
    497     volumes:
    498       - .psqlrc:/root/.psqlrc:ro
    499       - postgres:/var/lib/postgresql/data
    500       - history:/user/local/hist
    501     environment:
    502       PSQL_HISTFILE: /user/local/hist/.psql_history
    503       POSTGRES_PASSWORD: postgres
    504     ports:
    505       - 5432
    506     healthcheck:
    507       test: pg_isready -U postgres -h 127.0.0.1
    508       interval: 5s
    509 
    510   redis:
    511     image: redis:6.2-alpine
    512     volumes:
    513       - redis:/data
    514     ports:
    515       - 6379
    516     healthcheck:
    517       test: redis-cli ping
    518       interval: 1s
    519       timeout: 3s
    520       retries: 30
    521 
    522   webpacker:
    523     <<: *app
    524     command: bundle exec ./bin/webpack-dev-server
    525     ports:
    526       - '3035:3035'
    527     volumes:
    528       - ..:/app:cached
    529       - bundle:/usr/local/bundle
    530       - node_modules:/app/node_modules
    531       - packs:/app/public/packs
    532       - packs-test:/app/public/packs-test
    533     environment:
    534       <<: *env
    535       WEBPACKER_DEV_SERVER_HOST: 0.0.0.0
    536       YARN_CACHE_FOLDER: /app/node_modules/.yarn-cache
    537 
    538 volumes:
    539   bundle:
    540   node_modules:
    541   history:
    542   rails_cache:
    543   postgres:
    544   redis:
    545   packs:
    546   packs-test:
    547 
    548 We define six services and two extension fields (x-app and x-backend). [71]
    549 Extension fields allow us to define common parts of the configuration. We can
    550 attach YAML anchors to them, and later, embed anywhere in the file.
    551 
    552 NOTE: In the end, we don’t use Docker Compose or execute the docker compose up
    553 command in order to run our application. Instead, we use Dip (see [72]below),
    554 and thus, the compose.yml file only acts as a services registry. Another
    555 important thing to note is that we put the compose.yml file into the .dockerdev
    556 / folder. This is why we mount the source code as ..:/app and not .:/app.
    557 Please, keep this in mind if you’re considering using this configuration
    558 without Dip (which is not recommended).
    559 
    560 On that note, let’s go ahead and take a thorough look at each service.
    561 
    562 x-app
    563 
    564 The main purpose of this extension is to provide all the required information
    565 to build our application container (as defined in the Dockerfile above):
    566 
    567 x-app: &app
    568   build:
    569     context: .
    570     args:
    571       RUBY_VERSION: '3.2.2'
    572       PG_MAJOR: '15'
    573       NODE_MAJOR: '18'
    574 
    575 What is the context? The context directory defines the [73]build context for
    576 Docker. This is something like a working directory for the build process—for
    577 example, when we execute the COPY command. As this directory is packaged and
    578 sent to the Docker daemon every time an image is built, it’s better to keep it
    579 as small as possible. We’re good here, since our context is just the .dockerdev
    580 folder.
    581 
    582 And, as we mentioned earlier, we’ll specify the exact version of our
    583 dependencies using the args as declared in the Dockerfile.
    584 
    585 It’s also a good idea to pay attention to the way we tag images:
    586 
    587 image: example-dev:1.0.0
    588 
    589 One of the benefits of using Docker for development is the ability to
    590 automatically synchronize configuration changes across the team. This means the
    591 only time you need to upgrade the local image version is when you make changes
    592 to it (or to the arguments or files it relies on). Using example-dev:latest is
    593 like shooting yourself in the foot.
    594 
    595 Keeping an image version also helps work with two different environments
    596 without any additional hassle. For example, when working on a long-standing
    597 “chore/upgrade-to-ruby-3” branch, you can easily switch to master and use the
    598 older image with the older version of Ruby: no need to rebuild anything.
    599 
    600     Rule of thumb: Increase the version number in the image tag every time you
    601     change Dockerfile or its arguments (upgrading dependencies, etc.)
    602 
    603 Next, we add some common environment variables (those shared by multiple
    604 services, e.g., Rails and Webpacker):
    605 
    606 environment: &env
    607   NODE_ENV: ${NODE_ENV:-development}
    608   RAILS_ENV: ${RAILS_ENV:-development}
    609 
    610 There are several things going on here, but I’d like to focus on just one: the 
    611 X=${X:-smth} syntax. This could be translated as “For X variable within the
    612 container, if present, use the host machine’s X env variable, otherwise, use
    613 another value”. Thus, we make it possible to run a service in a different
    614 environment specified along with a command, e.g., RAILS_ENV=test docker-compose
    615 up rails.
    616 
    617 Note that we’re using a dictionary value (NODE_ENV: xxx) and not a list value (
    618 - NODE_ENV=xxx) for the environment field. This allows us to re-use the common
    619 settings (see below).
    620 
    621 We also tell Docker to [74]use tmpfs for the /tmp folder within a container—and
    622 also for the tmp/pids folder of our application. This way, we ensure that no 
    623 server.pid survives a container exit (say goodbye to any “A server is already
    624 running” errors):
    625 
    626 tmpfs:
    627   - /tmp
    628   - /app/tmp/pids
    629 
    630 x-backend
    631 
    632 Alright, so now, we’ve finally reached the most interesting part of this post.
    633 
    634 This service defines the shared behavior of all Ruby services.
    635 
    636 Let’s talk about the volumes first:
    637 
    638 x-backend: &backend
    639   <<: *app
    640   stdin_open: true
    641   tty: true
    642   volumes:
    643     - ..:/app:cached
    644     - rails_cache:/app/tmp/cache
    645     - bundle:/usr/local/bundle
    646     - history:/usr/local/hist
    647     - node_modules:/app/node_modules
    648     - packs:/app/public/packs
    649     - packs-test:/app/public/packs-test
    650     - ./.psqlrc:/root/.psqlrc:ro
    651     - ./.bashrc:/root/.bashrc:ro
    652     - ./.pryrc:/root/.pryrc:ro
    653   environment: &backend_environment
    654     <<: *env
    655     REDIS_URL: redis://redis:6379/
    656     DATABASE_URL: postgres://postgres:postgres@postgres:5432
    657     WEBPACKER_DEV_SERVER_HOST: webpacker
    658     MALLOC_ARENA_MAX: 2
    659     WEB_CONCURRENCY: ${WEB_CONCURRENCY:-1}
    660     BOOTSNAP_CACHE_DIR: /usr/local/bundle/_bootsnap
    661     XDG_DATA_HOME: /app/tmp/caches
    662     YARN_CACHE_FOLDER: /app/node_modules/.yarn-cache
    663     HISTFILE: /usr/local/hist/.bash_history
    664     PSQL_HISTFILE: /usr/local/hist/.psql_history
    665     IRB_HISTFILE: /usr/local/hist/.irb_history
    666     EDITOR: vi
    667   depends_on: &backend_depends_on
    668     postgres:
    669       condition: service_healthy
    670     redis:
    671       condition: service_healthy
    672 volumes:
    673   - ..:/app:cached
    674   - bundle:/usr/local/bundle
    675   - rails_cache:/app/tmp/cache
    676   - node_modules:/app/node_modules
    677   - packs:/app/public/packs
    678   - packs-test:/app/public/packs-test
    679   - history:/usr/local/hist
    680   - ./.psqlrc:/root/.psqlrc:ro
    681   - ./.bashrc:/root/.bashrc:ro
    682 
    683 The Docker team is striving to make Docker work faster on MacOS. The latest
    684 releases (since [75]4.6.0) come with VirtioFS accelerated directory sharing and
    685 virtualization.framework support. Go check the “Experimental Features” tab in
    686 the Docker Desktop Preferences. You might find the resulting performance
    687 improvement to be pretty amazing: ([76]regular actions become ~2x faster)!
    688 
    689 The first item in the volumes list mounts the project directory to the /app
    690 folder within a container using the cached strategy. This cached modifier was
    691 the key to efficient Docker development on macOS.
    692 
    693 Wait, was?
    694 
    695 Yeah. Was. That’s because since the release of gRPC FUSE synchronization, it’s 
    696 [77]no longer needed. Still, I decided to keep it for a while, for two reasons:
    697 first, some of your teammates may still be using older Docker desktop versions,
    698 and second, I ran some benchmarks and found that using older osxfs file sharing
    699 could have better performance (but only when using :cached). So, even on modern
    700 versions of Docker, it could make sense to uncheck the “Use gRPC FUSE for file
    701 sharing” option inside the preferences menu.
    702 
    703 The next line tells our container to use a volume named bundle to store the
    704 contents of /usr/local/bundle (this is where gems are stored [78]by default).
    705 By doing this, we persist our gem data across runs: all the volumes defined in 
    706 compose.yml will stay put until we run compose down --volumes.
    707 
    708 The following lines have also been dutifully placed in order to nullify the
    709 “Docker is slow on Mac” curse. We put all the generated files into Docker
    710 volumes to avoid any heavy disk operations on the host machine:
    711 
    712 - rails_cache:/app/tmp/cache
    713 - node_modules:/app/node_modules
    714 - packs:/app/public/packs
    715 - packs-test:/app/public/packs-test
    716 
    717     To give Docker a suitably fast speed on macOS, follow these two rules: use 
    718     :cached to mount source files (if not using gRPC FUSE), and use volumes for
    719     generated content (assets, bundle, etc.).
    720 
    721 NOTE: If you’re using Sprockets (or Propshaft), don’t forget to add a dedicated
    722 volume to store the assets (assets:/app/public/assets). For tailwindcss-rails,
    723 add something like assets_builds:/app/assets/builds.
    724 
    725 We’ll then mount different command line tools configuration files and a volume
    726 to persist their history:
    727 
    728 - history:/usr/local/hist
    729 - ./.psqlrc:/root/.psqlrc:ro
    730 - ./.bashrc:/root/.bashrc:ro
    731 
    732 Oh, and why is psql in the Ruby container? That’s because it’s used internally
    733 when you run rails dbconsole.
    734 
    735 Pressing onward, our [79].psqlrc file contains the following trick which makes
    736 it possible to specify the path to the history file via the env variable—thus
    737 allowing us to specify the path to the history file via the PSQL_HISTFILE env
    738 variable, or otherwise, fall back to the default $HOME/.psql_history:
    739 
    740 \set HISTFILE `[[ -z $PSQL_HISTFILE ]] && echo $HOME/.psql_history || echo $PSQL_HISTFILE`
    741 
    742 The .bashrc file allows us to add terminal customizations within a container:
    743 
    744 alias be="bundle exec"
    745 
    746 Alright, let’s talk about the environment variables:
    747 
    748 environment: &backend_environment
    749   <<: *env
    750   # ----
    751   # Service discovery
    752   # ----
    753   REDIS_URL: redis://redis:6379/
    754   DATABASE_URL: postgres://postgres:postgres@postgres:5432
    755   WEBPACKER_DEV_SERVER_HOST: webpacker
    756   # ----
    757   # Application configuration
    758   # ----
    759   MALLOC_ARENA_MAX: 2
    760   WEB_CONCURRENCY: ${WEB_CONCURRENCY:-1}
    761   # -----
    762   # Caches
    763   # -----
    764   BOOTSNAP_CACHE_DIR: /usr/local/bundle/_bootsnap
    765   # This env variable is used by some tools (e.g., RuboCop) to store caches
    766   XDG_DATA_HOME: /app/tmp/cache
    767   # Puts the Yarn cache into a mounted volume for speed
    768   YARN_CACHE_FOLDER: /app/node_modules/.yarn-cache
    769   # ----
    770   # Dev tools
    771   # ----
    772   HISTFILE: /usr/local/hist/.bash_history
    773   PSQL_HISTFILE: /usr/local/hist/.psql_history
    774   IRB_HISTFILE: /usr/local/hist/.irb_history
    775   EDITOR: vi
    776 
    777 First of all, we “inherit” variables from the common environment variables (<<:
    778 *env).
    779 
    780 The first group of variables (DATABASE_URL, REDIS_URL, and 
    781 WEBPACKER_DEV_SERVER_HOST) connect our Ruby application to other services.
    782 
    783 The DATABASE_URL and WEBPACKER_DEV_SERVER_HOST variables are supported by Rails
    784 (ActiveRecord and Webpacker respectively) out of the box. Some libraries
    785 (Sidekiq) also support REDIS_URL, but not all of them: for instance, Action
    786 Cable must be explicitly configured.
    787 
    788 The second group contains some application-wide settings. For example, we
    789 define MALLOC_ARENA_MAX and WEB_CONCURRENCY to help us keep Ruby memory
    790 handling in check.
    791 
    792 Read more about Ruby memory spells and techniques:
    793 
    794 [80]Cables vs. malloc_trim, or yet another Ruby memory usage benchmark
    795 
    796 Cables vs. malloc_trim, or yet another Ruby memory usage benchmark
    797 
    798 March 19, 2019
    799 Read also
    800 
    801 Also, we have the variables responsible for storing caches in Docker volumes (
    802 BOOTSNAP_CACHE_DIR, XDG_DATA_HOME, YARN_CACHE_FOLDER).
    803 
    804 We use [81]bootsnap to speed up application load time. We store its cache in
    805 the same volume as the Bundler data. This is because this cache mostly contains
    806 the gem data, and we want to make sure the cache is reset every time we drop
    807 the Bundler volume (for instance, during a Ruby version upgrade).
    808 
    809 The final group of variables aim to improve the developer experience. HISTFILE:
    810 /usr/local/hist/.bash_history is the most significant here: it tells Bash to
    811 store its history in the specified location, thus making it persistent. The
    812 same goes for PSQL_HISTFILE and IRB_HISTFILE.
    813 
    814 NOTE: You need to configure IRB to store history in the specified location. To
    815 do that, drop these lines into your .irbrc file:
    816 
    817 IRB.conf[:HISTORY_FILE] = ENV["IRB_HISTFILE"] if ENV["IRB_HISTFILE"]
    818 
    819 Finally, EDITOR: vi is used, for example, by the rails credentials:edit command
    820 to manage credentials files.
    821 
    822 And with that, the only lines in this service we’ve yet to cover are:
    823 
    824 stdin_open: true
    825 tty: true
    826 
    827 These lines make this service interactive, that is, they provide a TTY. We need
    828 this, for example, to run the Rails console or Bash within a container.
    829 
    830 This is the same as running a Docker container with the -it option.
    831 
    832 rails
    833 
    834 The rails server is our default backend service. The only thing it overrides is
    835 the command to execute:
    836 
    837 rails:
    838   <<: *backend
    839   command: bundle exec rails
    840 
    841 This service is meant for executing all the commands needed in development (
    842 rails db:migrate, rspec, etc.).
    843 
    844 web
    845 
    846 The web service is meant for launching a web server. It defines the exposed
    847 ports and the required dependencies to run the app itself.
    848 
    849 webpacker
    850 
    851 The only thing I want to mention here is the WEBPACKER_DEV_SERVER_HOST: 0.0.0.0
    852 setting: it makes the Webpack dev server accessible from the outside (it runs
    853 on localhost by default).
    854 
    855 Health checks
    856 
    857 When running common Rails commands such as db:migrate, we want to ensure that
    858 the DB is up and ready to accept connections. How can we tell Docker Compose to
    859 wait until a dependent service is ready? We can use [82]health checks!
    860 
    861 You’ve probably noticed that our depends_on definition isn’t just a list of
    862 services:
    863 
    864 backend:
    865   # ...
    866   depends_on:
    867     postgres:
    868       condition: service_healthy
    869     redis:
    870       condition: service_healthy
    871 
    872 postgres:
    873   # ...
    874   healthcheck:
    875     test: pg_isready -U postgres -h 127.0.0.1
    876     interval: 5s
    877 
    878 redis:
    879   # ...
    880   healthcheck:
    881     test: redis-cli ping
    882     interval: 1s
    883     timeout: 3s
    884     retries: 30
    885 
    886 Introducing Dip
    887 
    888 If you still think that Docker Compose way is too complicated, there’s a tool
    889 called [83]Dip (developed by one of my colleages at Evil Martians) which aims
    890 to make the developer experience even smoother.
    891 
    892 [84]Reusable development containers with Docker Compose and Dip
    893 
    894 Reusable development containers with Docker Compose and Dip
    895 
    896 November 17, 2020
    897 Read also
    898 
    899 [85]Dip is a thin wrapper over docker compose, which provides a switch from
    900 infrastructure-oriented flow to development-oriented one. The key benefits of
    901 using Dip are as follows:
    902 
    903   • The ability to define application-specific interactive commands and
    904     sub-commands.
    905   • The dip provision flow to quickly set up a development environment from
    906     scratch.
    907   • Support for multiple compose.yml files (including OS-specific
    908     configurations).
    909 
    910 With Dip in place, to start working on the app locally, you just need to
    911 execute a few commands:
    912 
    913 # Builds a Docker image if none, runs additional commands
    914 $ dip provision
    915 # Runs a Rails server with the defined dependencies
    916 $ dip rails s
    917 => Booting Puma
    918 => Rails 7.0.2.2 application starting in development
    919 => Run `bin/rails server --help` for more startup options
    920 [1] Puma starting in cluster mode...
    921 ...
    922 [1] - Worker 0 (PID: 9) booted in 0.0s, phase: 0
    923 
    924 Here is our typical [86]dip.yml file:
    925 
    926 version: '7.1'
    927 
    928 # Define default environment variables to pass
    929 # to Docker Compose
    930 environment:
    931   RAILS_ENV: development
    932 
    933 compose:
    934   files:
    935     - .dockerdev/compose.yml
    936   project_name: example_demo
    937 
    938 interaction:
    939   # This command spins up a Rails container with the required dependencies (such as databases),
    940   # and opens a terminal within it.
    941   runner:
    942     description: Open a Bash shell within a Rails container (with dependencies up)
    943     service: rails
    944     command: /bin/bash
    945 
    946   # Run a Rails container without any dependent services (useful for non-Rails scripts)
    947   bash:
    948     description: Run an arbitrary script within a container (or open a shell without deps)
    949     service: rails
    950     command: /bin/bash
    951     compose_run_options: [ no-deps ]
    952 
    953   # A shortcut to run Bundler commands
    954   bundle:
    955     description: Run Bundler commands
    956     service: rails
    957     command: bundle
    958     compose_run_options: [ no-deps ]
    959 
    960   # A shortcut to run RSpec (which overrides the RAILS_ENV)
    961   rspec:
    962     description: Run RSpec commands
    963     service: rails
    964     environment:
    965       RAILS_ENV: test
    966     command: bundle exec rspec
    967 
    968   rails:
    969     description: Run Rails commands
    970     service: rails
    971     command: bundle exec rails
    972     subcommands:
    973       s:
    974         description: Run Rails server at http://localhost:3000
    975         service: web
    976         compose:
    977           run_options: [service-ports, use-aliases]
    978 
    979   yarn:
    980     description: Run Yarn commands
    981     service: rails
    982     command: yarn
    983     compose_run_options: [ no-deps ]
    984 
    985   psql:
    986     description: Run Postgres psql console
    987     service: postgres
    988     default_args: anycasts_dev
    989     command: psql -h postgres -U postgres
    990 
    991   'redis-cli':
    992     description: Run Redis console
    993     service: redis
    994     command: redis-cli -h redis
    995 
    996 provision:
    997   - dip compose down --volumes
    998   - dip compose up -d postgres redis
    999   - dip bash -c bin/setup
   1000 
   1001 Let me explain some bits of this in further detail.
   1002 
   1003 First, the compose section:
   1004 
   1005 compose:
   1006   files:
   1007     - .dockerdev/compose.yml
   1008   project_name: example_demo
   1009 
   1010 Here we should specify the path to our Compose configuration (.dockerdev/
   1011 compose.yml). Accordingly, we can run dip from the project root, and the
   1012 correct configuration will be picked up.
   1013 
   1014 The project_name is important: if we don’t specify it, the folder containing
   1015 the compose.yml file would be used (“dockerdev”), which could lead to
   1016 collisions between different projects.
   1017 
   1018 The rails command is also worth some additional attention:
   1019 
   1020 rails:
   1021   description: Run Rails commands
   1022   service: rails
   1023   command: bundle exec rails
   1024   subcommands:
   1025     s:
   1026       description: Run Rails server at http://localhost:3000
   1027       service: web
   1028       compose:
   1029         run_options: [service-ports, use-aliases]
   1030 
   1031 By default, the dip rails command would call bundle exec rails within a Rails
   1032 container. However, we use the subcommand feature of Dip here to treat dip
   1033 rails s differently:
   1034 
   1035   • We use the web service, not rails (so, the deps are up).
   1036   • We expose the service ports (3000 in our case).
   1037   • We also enable network aliases, so other services can access this container
   1038     via the web hostname.
   1039 
   1040 Under the hood, this will result in the following Docker Compose command:
   1041 
   1042 docker compose run --rm --service-ports --use-aliases web
   1043 
   1044 Note that it uses run, and not up. This difference makes our server
   1045 terminal-accessible. For example, this means that we can attach a debugger and
   1046 use it without any problems (with the up command the terminal is
   1047 non-interactive).
   1048 
   1049 Interactive provisioning
   1050 
   1051 To learn how to keep configuration under control, check out this “Terraforming
   1052 Rails” series:
   1053 
   1054 [87]Anyway Config: Keep your Ruby configuration sane
   1055 
   1056 Anyway Config: Keep your Ruby configuration sane
   1057 
   1058 April 14, 2020
   1059 [svg]
   1060 Cover for Anyway Config: Keep your Ruby configuration sane
   1061 Read also
   1062 
   1063 For most applications, building an image and setting up a database is not
   1064 enough to start developing: beyond this, some kind of secrets, or credentials,
   1065 or .env files are required. Here, we’ve managed to use Dip to help new
   1066 engineers quickly assemble all these wayfallen parts by providing an
   1067 interactive provision experience.
   1068 
   1069 Let’s consider, for example, that we need to put a .env.development.local file
   1070 with some secret info and also configure RubyGems to download packages from a
   1071 private registry (say, Sidekiq Pro). For this, I’ll write the following
   1072 provision script:
   1073 
   1074 # The command is extracted, so we can use it alone
   1075 configure_bundler:
   1076   command: |
   1077     (test -f .bundle/config && cat .bundle/config | \
   1078       grep BUNDLE_ENTERPRISE__CONTRIBSYS__COM > /dev/null) ||
   1079     \
   1080       (echo "Sidekiq ent credentials: "; read -r creds; dip bundle config --local enterprise.contribsys.com $creds)
   1081 
   1082 provision:
   1083   - (test -f .env.development.local) || (echo "\n\n ⚠️  .env.development.local file is missing\n\n"; exit 1)
   1084   - dip compose down --volumes
   1085   - dip configure_bundler
   1086   - (test -f config/database.yml) || (cp .dockerdev/database.yml.example config/database.yml)
   1087   - dip compose up -d postgres redis
   1088   - dip bash -c bin/setup
   1089 
   1090 Below you can see a demonstration of this command running in action:
   1091 
   1092 An interactive Dip provisioning example
   1093 
   1094 Services vs Docker for development
   1095 
   1096 You can use a good ‘ol [88]Makefile to do the same, for sure. However, we’ve
   1097 found that using a dedicated tool (like Dip) to define everything in a
   1098 declarative manner is more efficient.
   1099 
   1100 One more use case for standardizing the development setup is to make it
   1101 possible to run multiple independent services locally. Let me quickly
   1102 demonstrate how we do this with Dip. First, you need to dockerize each
   1103 application (following this post). After that, we need to connect the apps to
   1104 each other. How can we do this? With the help of Docker Compose [89]external
   1105 networks.
   1106 
   1107 We add the following line to the dip.yml for each app:
   1108 
   1109 # ...
   1110 provision:
   1111   # Make sure the named network exists
   1112   - docker network inspect my_project > /dev/null 2>&1 || \
   1113     docker network create my_project
   1114 # ...
   1115 
   1116 Finally, we attach services to this network via aliases in the compose.yml
   1117 files:
   1118 
   1119 # service A: compose.yml
   1120 service:
   1121   ruby:
   1122     # ...
   1123     networks:
   1124       default:
   1125       project:
   1126         aliases:
   1127           - project-a
   1128 
   1129 networks:
   1130   project:
   1131     external:
   1132       name: my_project
   1133 
   1134 # service B: compose.yml
   1135 service:
   1136   web:
   1137     # ...
   1138     environment:
   1139       # We can access the service A via its alias defined for the external network
   1140       SERVICE_URL: http://project-a:3000
   1141 
   1142 networks:
   1143   project:
   1144     external:
   1145       name: my_project
   1146 
   1147 From development to production
   1148 
   1149 So, here’s one of the most popular questions we’ve been asked since launching
   1150 the first version of this article: how to go live with Docker? To answer this,
   1151 we’d need to write a entirely new article… and we will 😉.
   1152 
   1153 For now, let me give a sneak preview of how can we extend the current
   1154 development setup to cover the production environment as well.
   1155 
   1156 First of all, we’re not going to talk about a Docker Compose-based deployment,
   1157 so compose.yml is out. All we need is to update our Docker image to reflect the
   1158 difference between development and production:
   1159 
   1160  1. For security reasons, we should execute the code on behalf of the regular,
   1161     non-root user.
   1162  2. We should keep all the required dependencies and artifacts within the image
   1163     itself; we cannot use volumes (the image should be self-contained).
   1164  3. We should keep and copy the source code into a container.
   1165  4. The resulting image should be as slim as possible.
   1166 
   1167 To achieve this, we’ll refactor our existing Dockerfile to define multiple
   1168 stages (and to support [90]multi-stage builds). Below is the annotated example:
   1169 
   1170 # syntax=docker/dockerfile:1
   1171 
   1172 ARG RUBY_VERSION
   1173 ARG DISTRO_NAME=bullseye
   1174 
   1175 # Here we add the the name of the stage ("base")
   1176 FROM ruby:$RUBY_VERSION-slim-$DISTRO_NAME AS base
   1177 
   1178 ARG PG_MAJOR
   1179 ARG NODE_MAJOR
   1180 ARG YARN_VERSION
   1181 
   1182 # Common dependencies
   1183 # ...
   1184 # The following lines are exactly the same as before
   1185 # ...
   1186 # ...
   1187 WORKDIR /app
   1188 
   1189 EXPOSE 3000
   1190 CMD ["/bin/bash"]
   1191 
   1192 # Then, we define the "development" stage from the base one
   1193 FROM base AS development
   1194 
   1195 ENV RAILS_ENV=development
   1196 
   1197 # The major difference from the base image is that we may have development-only system
   1198 # dependencies (like Vim or graphviz).
   1199 # We extract them into the Aptfile.dev file.
   1200 COPY Aptfile.dev /tmp/Aptfile.dev
   1201 RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
   1202   --mount=type=cache,target=/var/lib/apt,sharing=locked \
   1203   --mount=type=tmpfs,target=/var/log \
   1204   apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get -yq dist-upgrade && \
   1205   DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends \
   1206     $(grep -Ev '^\s*#' /tmp/Aptfile.dev | xargs)
   1207 
   1208 # The production-builder image is responsible for installing dependencies and compiling assets
   1209 FROM base as production-builder
   1210 
   1211 # First, we create and configure a dedicated user to run our application
   1212 RUN groupadd --gid 1005 my_user \
   1213   && useradd --uid 1005 --gid my_user --shell /bin/bash --create-home my_user
   1214 USER my_user
   1215 RUN mkdir /home/my_user/app
   1216 WORKDIR /home/my_user/app
   1217 
   1218 # Then, we re-configure Bundler
   1219 ENV RAILS_ENV=production \
   1220   LANG=C.UTF-8 \
   1221   BUNDLE_JOBS=4 \
   1222   BUNDLE_RETRY=3 \
   1223   BUNDLE_APP_CONFIG=/home/my_user/bundle \
   1224   BUNDLE_PATH=/home/my_user/bundle \
   1225   GEM_HOME=/home/my_user/bundle
   1226 
   1227 # Install Ruby gems
   1228 COPY --chown=my_user:my_user Gemfile Gemfile.lock ./
   1229 RUN mkdir $BUNDLE_PATH \
   1230   && bundle config --local deployment 'true' \
   1231   && bundle config --local path "${BUNDLE_PATH}" \
   1232   && bundle config --local without 'development test' \
   1233   && bundle config --local clean 'true' \
   1234   && bundle config --local no-cache 'true' \
   1235   && bundle install --jobs=${BUNDLE_JOBS} \
   1236   && rm -rf $BUNDLE_PATH/ruby/${RUBY_VERSION}/cache/* \
   1237   && rm -rf /home/my_user/.bundle/cache/*
   1238 
   1239 # Install JS packages
   1240 COPY --chown=my_user:my_user package.json yarn.lock ./
   1241 RUN yarn install --check-files
   1242 
   1243 # Copy code
   1244 COPY --chown=my_user:my_user . .
   1245 
   1246 # Precompile assets
   1247 # NOTE: The command may require adding some environment variables (e.g., SECRET_KEY_BASE) if you're not using
   1248 # credentials.
   1249 RUN bundle exec rails assets:precompile
   1250 
   1251 # Finally, our production image definition
   1252 # NOTE: It's not extending the base image, it's a new one
   1253 FROM ruby:$RUBY_VERSION-slim-$DISTRO_NAME AS production
   1254 
   1255 # Production-only dependencies
   1256 RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
   1257   --mount=type=cache,target=/var/lib/apt,sharing=locked \
   1258   --mount=type=tmpfs,target=/var/log \
   1259   apt-get update -qq \
   1260   && apt-get dist-upgrade -y \
   1261   && DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends \
   1262     curl \
   1263     gnupg2 \
   1264     less \
   1265     tzdata \
   1266     time \
   1267     locales \
   1268   && update-locale LANG=C.UTF-8 LC_ALL=C.UTF-8
   1269 
   1270 # Upgrade RubyGems and install the latest Bundler version
   1271 RUN gem update --system && \
   1272     gem install bundler
   1273 
   1274 # Create and configure a dedicated user (use the same name as for the production-builder image)
   1275 RUN groupadd --gid 1005 my_user \
   1276   && useradd --uid 1005 --gid my_user --shell /bin/bash --create-home my_user
   1277 RUN mkdir /home/my_user/app
   1278 WORKDIR /home/my_user/app
   1279 USER my_user
   1280 
   1281 # Ruby/Rails env configuration
   1282 ENV RAILS_ENV=production \
   1283   BUNDLE_APP_CONFIG=/home/my_user/bundle \
   1284   BUNDLE_PATH=/home/my_user/bundle \
   1285   GEM_HOME=/home/my_user/bundle \
   1286   PATH="/home/my_user/app/bin:${PATH}" \
   1287   LANG=C.UTF-8 \
   1288   LC_ALL=C.UTF-8
   1289 
   1290 EXPOSE 3000
   1291 
   1292 # Copy code
   1293 COPY --chown=my_user:my_user . .
   1294 
   1295 # Copy artifacts
   1296 # 1) Installed gems
   1297 COPY --from=production-builder $BUNDLE_PATH $BUNDLE_PATH
   1298 # 2) Compiled assets (by Webpacker in this case)
   1299 COPY --from=production-builder /home/my_user/app/public/packs /home/my_user/app/public/packs
   1300 # 3) We can even copy the Bootsnap cache to speed up our Rails server load!
   1301 COPY --chown=my_user:my_user --from=production-builder /home/my_user/app/tmp/cache/bootsnap* /home/my_user/app/tmp/cache/
   1302 
   1303 CMD ["bundle", "exec", "rails", "server", "-b", "0.0.0.0"]
   1304 
   1305 Introducing the Ruby on Whales interactive generator
   1306 
   1307 As a bonus, our Ruby on Whales [91]repository ships with a Rails template
   1308 (published on [92]Rails Bytes), which can help you quickly adopt Docker for
   1309 development by running a single command (and answering a few questions).
   1310 
   1311 Without further ado, check out the demonstartion below:
   1312 
   1313 An interactive Ruby on Whales installer
   1314 
   1315 You can give it a try by running a single command:
   1316 
   1317 rails app:template LOCATION='https://railsbytes.com/script/z5OsoB'
   1318 
   1319 Radio wave representing wind sounds on Mars
   1320 
   1321 Acknoledgements
   1322 
   1323 I would like to thank:
   1324 
   1325   • [93]Sergey Ponomarev for sharing performance tips and helping battle-test
   1326     the initial dockerization attempts.
   1327   • [94]Mikhail Merkushin for his work on Dip.
   1328   • [95]Dmitriy Nemykin for helping with the major (v2) upgrade.
   1329   • [96]Oliver Klee ([97]Brain Gourmets) for continuous PRs with the
   1330     configuration improvements and actualization.
   1331 
   1332 Radio wave representing wind sounds on Mars
   1333 
   1334 Changelog
   1335 
   1336 2.0.3 (2023-09-21)
   1337 
   1338   • Upgrade Node.js installation script.
   1339 
   1340 2.0.2 (2022-11-30)
   1341 
   1342   • Use RUN --mount for caching packages between builds instead of manual
   1343     cleanup.
   1344 
   1345 2.0.1 (2022-03-22)
   1346 
   1347   • Replace deprecated apt-key with gpg.
   1348 
   1349 2.0.0 (2022-03-02)
   1350 
   1351   • Major upgrade and new chapters.
   1352 
   1353 1.1.4 (2021-10-12)
   1354 
   1355   • Added tmp/pids to tmpfs (to deal with “A server is already running”
   1356     errors).
   1357 
   1358 1.1.3 (2021-03-30)
   1359 
   1360   • Updated Dockerfile to mitigate MiniMagic licensing issues. See [98]
   1361     terraforming-rails#35
   1362   • Use dictionary to organize environment variables. See [99]
   1363     terraforming-rails#6
   1364 
   1365 1.1.2 (2021-02-26)
   1366 
   1367   • Update dependencies versions. See [100]terraforming-rails#28
   1368   • Allow to use comments in Aptfile. See [101]terraforming-rails#31
   1369   • Fix path to Aptfile inside Dockerfile. See [102]terraforming-rails#33
   1370 
   1371 1.1.1 (2020-09-15)
   1372 
   1373   • Use .dockerdev directory as build context instead of project directory. See
   1374     [103]terraforming-rails#26 for details.
   1375 
   1376 1.1.0 (2019-12-10)
   1377 
   1378   • Change base Ruby image to slim.
   1379   • Specify Debian release for Ruby version explicitly and upgrade to buster.
   1380   • Use standard Bundler path (/usr/local/bundle) instead of /bundle.
   1381   • Use Docker Compose file format v2.4.
   1382   • Add health checking to postgres and redis services.
   1383 
   1384 Join our email newsletter
   1385 
   1386 Get all the new posts delivered directly to your inbox. Unsubscribe anytime.
   1387 
   1388 [104][                    ]Your email[105][                    ]
   1389 Subscribe
   1390 Or [107]subscribe to a feed
   1391 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   1392 [108][email protected]
   1393 
   1394 United States
   1395 
   1396 [109]+1 888 400 5485
   1397 
   1398 Portugal
   1399 
   1400 [110]+351 308 808 570
   1401 
   1402 Japan
   1403 
   1404 [111]+81 6 6225 1242
   1405 
   1406   • [112]Contact us
   1407   • [113]Careers
   1408   • [114]日本語版
   1409 
   1410 [115]Privacy policy
   1411 [116]Cookie & privacy preferences
   1412 [117]Notice at collection
   1413 
   1414 Designed and developed by Evil Martians
   1415 
   1416 
   1417 References:
   1418 
   1419 [1] https://evilmartians.com/
   1420 [2] https://cal.com/team/evilmartians/exploration
   1421 [3] https://evilmartians.com/services
   1422 [4] https://evilmartians.com/clients
   1423 [5] https://evilmartians.com/products
   1424 [6] https://evilmartians.com/opensource
   1425 [7] https://evilmartians.com/chronicles
   1426 [8] https://evilmartians.com/events
   1427 [9] https://evilmartians.com/devpropulsionlabs
   1428 [10] https://x.com/evilmartians
   1429 [11] https://www.linkedin.com/company/evil-martians
   1430 [12] https://github.com/evilmartians
   1431 [13] https://www.youtube.com/@evil.martians
   1432 [14] https://evilmartians.com/
   1433 [15] https://cal.com/team/evilmartians/exploration
   1434 [16] https://evilmartians.com/services
   1435 [17] https://evilmartians.com/clients
   1436 [18] https://evilmartians.com/products
   1437 [19] https://evilmartians.com/opensource
   1438 [20] https://evilmartians.com/chronicles
   1439 [21] https://evilmartians.com/events
   1440 [22] https://evilmartians.com/devpropulsionlabs
   1441 [23] https://x.com/evilmartians
   1442 [24] https://www.linkedin.com/company/evil-martians
   1443 [25] https://github.com/evilmartians
   1444 [26] https://www.youtube.com/@evil.martians
   1445 [27] https://evilmartians.com/events/evolution-of-real-time-and-anycable-rocky-mountain
   1446 [28] https://cal.com/team/evilmartians/exploration
   1447 [29] https://x.com/evilmartians
   1448 [30] https://www.linkedin.com/company/evil-martians
   1449 [31] https://github.com/evilmartians
   1450 [32] https://www.youtube.com/@evil.martians
   1451 [33] https://evilmartians.com/chronicles?categories=backend
   1452 [34] https://evilmartians.com/chronicles?services=software-development
   1453 [35] https://evilmartians.com/chronicles?services=audit-and-optimization
   1454 [36] https://evilmartians.com/chronicles?skills=rubyonrails
   1455 [37] https://evilmartians.com/chronicles?skills=ruby
   1456 [38] https://evilmartians.com/chronicles?skills=docker
   1457 [39] https://evilmartians.com/chronicles?skills=postgresql
   1458 [40] https://evilmartians.com/chronicles?skills=nodejs
   1459 [41] https://techracho.bpsinc.jp/hachi8833/2022_04_07/116843
   1460 [42] https://xfyuan.github.io/2020/07/dockeerizing-rails-development/
   1461 [43] https://evilmartians.com/chronicles/ruby-on-whales-docker-for-ruby-rails-development#changelog
   1462 [44] https://noti.st/palkan/vhsbxO/terraforming-legacy-rails-applications
   1463 [45] https://github.com/evilmartians/ruby-on-whales
   1464 [46] https://evilmartians.com/chronicles/ruby-on-whales-docker-for-ruby-rails-development#basic-docker-configuration
   1465 [47] https://evilmartians.com/chronicles/ruby-on-whales-docker-for-ruby-rails-development#introducing-dip
   1466 [48] https://evilmartians.com/chronicles/ruby-on-whales-docker-for-ruby-rails-development#services-vs-docker-for-development
   1467 [49] https://evilmartians.com/chronicles/ruby-on-whales-docker-for-ruby-rails-development#from-development-to-production
   1468 [50] https://evilmartians.com/chronicles/ruby-on-whales-docker-for-ruby-rails-development#introducing-the-ruby-on-whales-interactive-generator
   1469 [51] https://github.com/evilmartians/ruby-on-whales/blob/main/example/.dockerdev/Dockerfile
   1470 [52] https://github.com/moby/moby/issues/34129
   1471 [53] https://www.postgresql.org/download/linux/debian/
   1472 [54] https://docs.docker.com/compose/
   1473 [55] https://github.com/nodesource/distributions/blob/master/README.md#debinstall
   1474 [56] https://world.hey.com/dhh/modern-web-apps-without-javascript-bundling-or-transpiling-a20f2755
   1475 [57] https://github.com/WICG/import-maps
   1476 [58] https://github.com/rails/tailwindcss-rails
   1477 [59] https://github.com/heroku/heroku-buildpack-apt
   1478 [60] https://github.com/evilmartians/terraforming-rails/blob/master/examples/dockerdev/.dockerdev/Aptfile
   1479 [61] https://www.tug.org/texlive/
   1480 [62] https://askubuntu.com/a/972528
   1481 [63] http://xubuntugeek.blogspot.com/2012/06/save-disk-space-with-apt-get-option-no.html
   1482 [64] https://docs.docker.com/storage/storagedriver/#images-and-layers
   1483 [65] https://github.com/moby/buildkit/blob/master/frontend/dockerfile/docs/reference.md#run---mount
   1484 [66] https://github.com/docker-library/ruby/issues/129#issue-229195231
   1485 [67] https://github.com/evilmartians/terraforming-rails/pull/24
   1486 [68] https://github.com/rubygems/rubygems/pull/4076
   1487 [69] https://github.com/evilmartians/ruby-on-whales/blob/main/example/.dockerdev/compose.yml
   1488 [70] https://docs.docker.com/compose/
   1489 [71] https://github.com/compose-spec/compose-spec/blob/master/spec.md#extension
   1490 [72] https://evilmartians.com/chronicles/ruby-on-whales-docker-for-ruby-rails-development#introducing-dip
   1491 [73] https://docs.docker.com/compose/compose-file/#context
   1492 [74] https://docs.docker.com/v17.09/engine/admin/volumes/tmpfs/#choosing-the-tmpfs-or-mount-flag
   1493 [75] https://www.docker.com/blog/speed-boost-achievement-unlocked-on-docker-desktop-4-6-for-mac/
   1494 [76] https://twitter.com/palkan_tula/status/1504499523216945167
   1495 [77] https://github.com/docker/for-mac/issues/5402
   1496 [78] https://github.com/infosiftr/ruby/blob/9b1f77c11d663930f4175c683b1c5f268d4d8191/Dockerfile.template#L47
   1497 [79] https://github.com/evilmartians/ruby-on-whales/blob/main/example/.dockerdev/.psqlrc
   1498 [80] https://evilmartians.com/chronicles/cables-vs-malloc_trim-or-yet-another-ruby-memory-usage-benchmark
   1499 [81] https://www.github.com/Shopify/bootsnap
   1500 [82] https://docs.docker.com/compose/compose-file/compose-file-v3/#healthcheck
   1501 [83] https://evilmartians.com/opensource/dip
   1502 [84] https://evilmartians.com/chronicles/reusable-development-containers-with-docker-compose-and-dip
   1503 [85] https://evilmartians.com/opensource/dip
   1504 [86] https://github.com/evilmartians/ruby-on-whales/blob/main/example/dip.yml
   1505 [87] https://evilmartians.com/chronicles/anyway-config-keep-your-ruby-configuration-sane
   1506 [88] https://makefile.site/
   1507 [89] https://docs.docker.com/compose/networking/#use-a-pre-existing-network
   1508 [90] https://docs.docker.com/develop/develop-images/multistage-build/
   1509 [91] https://github.com/evilmartians/ruby-on-whales
   1510 [92] https://railsbytes.com/public/templates/z5OsoB
   1511 [93] https://github.com/sponomarev
   1512 [94] https://github.com/bibendi
   1513 [95] https://github.com/fargelus/
   1514 [96] https://github.com/oliverklee
   1515 [97] https://www.braingourmets.com/
   1516 [98] https://github.com/evilmartians/terraforming-rails/pull/35
   1517 [99] https://github.com/evilmartians/terraforming-rails/pull/6
   1518 [100] https://github.com/evilmartians/terraforming-rails/pull/28
   1519 [101] https://github.com/evilmartians/terraforming-rails/pull/31
   1520 [102] https://github.com/evilmartians/terraforming-rails/issues/33
   1521 [103] https://github.com/evilmartians/terraforming-rails/issues/26
   1522 [107] https://evilmartians.com/chronicles.atom
   1523 [108] mailto:[email protected]
   1524 [109] tel:+18884005485
   1525 [110] tel:+351308808570
   1526 [111] tel:+81662251242
   1527 [112] https://evilmartians.com/contact-us
   1528 [113] https://wellfound.com/company/evilmartians
   1529 [114] https://evilmartians.jp/
   1530 [115] https://evilmartians.com/privacy
   1531 [116] https://evilmartians.com/cookies
   1532 [117] https://evilmartians.com/privacy#notice_at_collection