TECH

Using Docker for Rails development

blogpost

Recently I got assigned to an old project and while luckily it had instructions on how to set it up locally in the Read.me the number of steps was damn too high. So instead of wasting half a day executing them, I wasted 2 days automating them (future devs will thank me… maybe).
I wanted to get as close as possible to a one command local env setup.

To simplify the process as you have guessed from the title of the article I decided to set up docker and docker-compose. I started with an example config from awesome-compose.

Level 0: just get the app to run on docker

FROM ruby:2.6.5


WORKDIR /myapp
COPY Gemfile /myapp/Gemfile
COPY Gemfile.lock /myapp/Gemfile.lock
RUN bundle install


COPY . /myapp
COPY config/database.yml.example /myapp/config/database.yml


CMD bundle exec rails s -p 8080 -b '0.0.0.0'

I won’t enter into details too much the main concept is:
- first, copy just the gemfiles and install gems (this is done to take advantage of the caching mechanism in building images to speed up rebuilding)
- copy the rest of the app code
- create a config file for the database based on a template (the details will be passed through ENV variables)

Since this time the goal is local developments assets compilation is not an issue we want to optimize.

Next, the maestro orchestrating the local env: docker-compose

services:
  db:
    image: postgres
    volumes:
      - ./tmp/db:/var/lib/postgresql/data
    environment:
      POSTGRES_PASSWORD: password
  web:
    build: .
    command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
    ports:
      - "3000:3000"
    environment:
      POSTGRES_HOST: db
      REDIS_URL: redis://redis
      REDIS_HOST: redis
    depends_on:
      - db
      - redis
  redis:
    image: 'redis:5-alpine'
    command: redis-server

Most of it is thanks to the help of awesome-compose
What we want to achieve here:
- Run our app alongside a postgres database and a redis server
- Have the postgres database data persist
- Use env variables to pass the component names for routing

As a warning note: avoid mapping any port that’s not necessary. So no mapping of 3456:3456 for postgres db. While it’s not an issue for local dev if you use a similar config on a production server it could open you to some brute-force attacks.

The odd thing: you may have noticed that we have both a REDIS_URL and a REDIS_HOST env. It’s because the default redis cache use requires the url param to have the protocol redis:// at the beginning, but our app also uses redis_store for session_store which requires just the host without the protocol. Chances are you don’t use redis_store and can safely remove the unnecessary env.

Level 1: add some dev quality-of-life configs

So this config was enough for me to get rolling and work on the project and contribute but it’s not the most comfortable thing due to a couple of issues:

Any change to the code required turning off the docker-compose instances, rebuilding and turning the infrastructure on again and that adds up.
I can't use binding.pry do debug

So to be able to cut on the restarts necessary and auto update code we will use volumes:

  web:
    build: .
    volumes:
      - .:/myapp

Adding a volume will mirror the current dir to the app dir in the image will automatically mirror any change we make to the code. While this won't help with changes to config files it will handle most changes in real-time (Assuming we run on typical dev env configs)

To enable access to binding.pry you need to add the following to the web service

  web:
    build: .
    volumes:
      - .:/myapp
    tty: true # for binding.pry
    stdin_open: true # for binding.pry

But adding this won’t allow us to just access the debug console from the console used to run docker-compose we will need to find the id of the web process and connect to it:

docker ps
 docker attach 75cde1ab8133

Level 2: add some reusability and sidekiq

One last thing missing now in my case was the possibility to also run sidekiq. It was not necessary in the beginning but once the development went on we needed to work on some async tasks to handle long data import processes.

x-my-app: &my_app
 build: .
 volumes:
   - .:/myapp
 environment:
   POSTGRES_HOST: db
   REDIS_URL: redis://redis
   REDIS_HOST: redis
 depends_on:
   - db
   - redis
 tty: true # for binding.pry
 stdin_open: true # for binding.pry


services:
 db:
   image: postgres
   volumes:
     - ./tmp/db:/var/lib/postgresql/data
   environment:
     POSTGRES_PASSWORD: password
 web:
   <<: *my_app
   command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
   ports:
     - "3000:3000"
 sidekiq:
   <<: *my_app
   command: 'sidekiq -L ./log/sidekiq.log -C ./config/sidekiq-dev.yml -P tmp/pids/sidekiq.pid'
 redis:
   image: 'redis:5-alpine'
   command: redis-server

So as you can see before starting to define services we created a block that will contain all the common configs between the rails app and the sidekiq process containers. The only differences are the command and the ports since sidekiq doesn't need any open and we can’t have 2 containers trying to occupy the same port anyway.

And with this addition, I’d consider the docker config done for this project. While it still requires more than one command to setup initially with the database creation/migration/seeding it’s a step requiring additional scripting on top of docker-compose. Maybe for another article.
The commands toolbox

As a parting note i’d like to list the docker command i found myself using most often:

  • docker-compose up --build // start the app
  • docker-compose down // stop it if running in the background
  • docker-compose ps // check the status of currently running images
  • docker-compose run web bash // run commands
  • docker images // show list of built images
  • docker image prune // delete unused images since they can take up a lot of hard drive
  • docker ps / docker kill // if docker-compose not doing the job

Tip: you may consider creating an alias for docker-compose to avoid typing it every time.

Read more on our blog

Check out the knowledge base collected and distilled by experienced
professionals.
bloglist_item
Tech

The buzzwords are Readability, Reusability, Maintainability. Here's the long version:

Modern web applications can grow in complexity. We often need to manage workflows more complex than simple...

bloglist_item
Tech

Over the years I had to deal with applications and system that have a long history of already being "legacy".
On top of that I met with clients/product owners that never want you to spend time ref...

bloglist_item
Tech

How many times have you searched for that one specific library that meets your needs? How much time have you spent customizing it to fit your project's requirements? I must admit, waaay too much. T...