Docker for skeptics
Tags: docker, kubernetes, ruby, elixir
Published: 2022-05-18 08:29:47

I first tried Docker in early 2014. I was working for a software agency. We were looking for ways to move away from manually configuring virtual servers for Ruby on Rails application deployments that involved copy-pasting a bunch of commands from a runbook. Back then Kubernetes wasn’t released to the public yet ([1.0 got released in 2015](https://cloudplatform.googleblog.com/2015/07/Kubernetes-V1-Released.html)), Docker Compose was called Fig, and [people were thinking that they could use Fig for running applications in production](https://www.docker.com/blog/welcoming-the-orchard-and-fig-team/).

I tried to get a Ruby on Rails app running in production where the container is monitored and automatically restarted, so an equivalent of tools like systemd. I looked into using Docker for running the database and found the topic of persistence immature at that time. As having persistent data storage for my database was very important for me, I got scared there.

I also looked into how I can make sure that the containers are started in the right order, so, for example, Postgres gets started before the app server automatically, in case of a complete server restart.

The limitations I discovered at that time made me skeptical about Docker. If I had been working at Google, I probably would have started contributing to Kubernetes in my “20% learning time”. Instead I gave a recommendation to my colleagues to [use Chef](https://blog.sundaycoding.com/blog/2014/03/10/learn-chef-its-hard-but-worth-it/) and decided to stay away from Docker.

In the next few years I avoided Docker and responded to the proposals involving Docker with the grumpy “how about not?” approach. The first time I looked at it without skepticism was when [Amr Abdelwahab](https://twitter.com/amrAbdelwahab) delivered the talk “[The broken promise of dockerization in the land of Ruby](https://www.youtube.com/watch?v=ua6tJ9d8pE8)” in early 2020 at Ruby User Group Berlin. Amr gave a compelling presentation that showed me that Docker improved a lot over the course of the last couple of years.

What I took away from the presentation was the use of multi stage builds to avoid repetition in Dockerfiles, improved performance in development, and a Makefile to hide the complexity of long commands. I encourage you to check out the [repository with the boilerplate](https://github.com/amrabdelwahab/docker-ruby-boilerplate).

It still took me over a year to come back to this idea. It happened for the first time in a hobby project called [Happy News](https://github.com/adamniedzielski/happy_news) where I played with a [strange GitHub Actions workflow](https://github.com/adamniedzielski/happy_news/pull/33/files): the image is first built and then the tests run inside the container. This was more to prove a point about the holy grail of dev / CI / prod parity than anything else. GitHub Actions are convenient for running tests without explicitly using Docker anyway.

What I’m especially proud of is using the same Dockerfile to [deploy to Heroku using their Container Registry](https://github.com/adamniedzielski/happy_news/pull/35/files). I currently see it as the best deployment method for bootstrapping products, because it allows you to transition to Kubernetes easily, if you happen to start earning some money and actually having traffic.

We use the same approach to Docker in development at my team at Marley Spoon. My team is called RAMEN and we develop an internal tool for our colleagues from the Culinary department. This tool enables them to create new recipes and plan weekly menus for Marley Spoon and Dinnerly customers.

The system has an API part which is a monolithic Ruby on Rails application. In development we need a few services running - app server, Postgres, [Que](https://github.com/que-rb/que), and [MailCatcher](https://mailcatcher.me/). We use Docker Compose for making sure that they’re running properly. My colleague Bogdan also got exposed to Amr’s ideas and introduced a Makefile to our Docker setup. This makes it easier for other team members to start the containers without being exposed to the complexity of the Docker Compose commands. As we’re a cross-functional team with some folks focusing on the frontend I see a big advantage of this.

You can see a good overview of the setup in an [open source application that I created](https://github.com/adamniedzielski/sidekiq-staged_push-demo):

```Dockerfile
# Dockerfile

FROM ruby:3.1.0-alpine AS dev
RUN apk add build-base postgresql-dev tzdata git bash
WORKDIR /app
ENV BUNDLE_PATH=/bundle \
    BUNDLE_BIN=/bundle/bin \
    GEM_HOME=/bundle
ENV PATH="${BUNDLE_BIN}:${PATH}"

FROM dev AS ci
COPY Gemfile Gemfile.lock ./
RUN bundle install
COPY . ./
```

```yaml
# docker-compose.yml

version: "3.9"
services:
  web:
    build:
      context: .
      target: dev
    env_file:
      - .env.dev
    environment:
      - BUNDLE_ENTERPRISE__CONTRIBSYS__COM
    stdin_open: true
    tty: true
    command: bundle exec rails server --binding 0.0.0.0
    volumes:
      - ".:/app"
      - bundle:/bundle
    ports:
      - "3000:3000"
    depends_on:
      - db
      - redis
  sidekiq:
    build:
      context: .
      target: dev
    env_file:
      - .env.dev
    stdin_open: true
    tty: true
    command: bundle exec sidekiq
    volumes:
      - ".:/app"
      - bundle:/bundle
    depends_on:
      - db
      - redis
  db:
    image: "healthcheck/postgres:alpine"
    environment:
      POSTGRES_PASSWORD: db_password
  redis:
    image: "redis:6-alpine"
    command: redis-server
    ports:
      - "6379:6379"
    volumes:
      - "redis:/data"
volumes:
  bundle:
  redis:
```

```Makefile
# Makefile

build:
	docker-compose build

bundle:
	docker-compose run --rm web bundle install

dbsetup:
	docker-compose run --rm web bundle exec rails db:setup

server:
	docker-compose run --rm --service-ports web

console:
	docker-compose run --rm web bundle exec rails console

sidekiq:
	docker-compose run --rm --service-ports sidekiq

rubocop:
	docker-compose run --rm web bundle exec rubocop

bash:
	docker-compose run --rm web bash
```

```Bash
# .env.dev

DATABASE_USERNAME=postgres
DATABASE_PASSWORD=db_password
DATABASE_HOST=db
REDIS_URL=redis://redis:6379/1
USE_STAGED_PUSH=true
USE_SIDEKIQ_ENTERPRISE=false
```

[sidekiq-staged_push](https://github.com/adamniedzielski/sidekiq-staged_push) is my first Ruby gem that uses Docker for development. The configuration doesn’t differ much, but I’m including the link here for reference. I got so excited with how easy the setup becomes for open source contributors that I [submitted a PR to Sidekiq to add it](https://github.com/mperham/sidekiq/pull/5183). Sadly it didn’t get accepted, but I finally learned [what this “.PHONY” in a Makefile means](https://stackoverflow.com/questions/2145590/what-is-the-purpose-of-phony-in-a-makefile).

What followed was a[ simple Telegram bot that nudges people to switch to Signal](https://github.com/adamniedzielski/signal_nudge/). Setting up dev and prod was a matter of copying over and adjusting the 4 files included above. I also noticed that it’s about 100 times faster to set up a new Ruby version on my machine if I use Docker and not rbenv. That’s when I realized that using Docker for Ruby became my new default approach and I changed from a Docker skeptic to a Docker preacher.

One thing that I haven’t figured out yet is how to [investigate gem source code](https://github.com/amrabdelwahab/docker-ruby-boilerplate/issues/2) with this setup. I’ve also recently [started playing around](https://github.com/adamniedzielski/my-messaging-app) with converting the same approach to the land of Elixir. If you have ideas how to improve it please help me!