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