A hybrid development Docker Compose setup for Rails

Lots of developers choose between dockerizing their development setup or leaving it as is. There is also a viable hybrid approach in combining Docker Compose with native processes.

I am usually in the camp of running things directly or creating Vagrant environments that closely resemble what I normally run. I also think a lot between introducing more layers than I need, so I usually run without Docker if I can.

Nevertheless, I realized that running a Docker Compose setup alongside your regular Puma and Sidekiq processes is actually a pretty nice sweet spot to be in. It’s what we use at Phrase.

Why, but why

The arguments for dockerizing the whole development environment are usually in terms of matching production. That means running the same versions of databases, utilities, and services. Having it formalized also means that every team member can immediatelly start working or return to a working setup.

I understand this argument a lot as it’s the reason I usually had a Vagrant environment around for my own projects. Even when I developed without a virtual machine, I would write a Vagrantfile to be able to run things in case of anything breaking. So I get it.

But it’s not the same with Docker. Dockerizing an entire development setup requires a bit different mindset in my opinion. And while leaving virtual machines behind sounds like an improvement, performance might still suffer.

This makes you think if dockerizing everything is worth it. Seems like full Docker setups are a minority for this reason.

Can we not go overboard and still enjoy some Docker, though? What’s an alternative?

The alternative is installating Ruby, Rails, and system utilities as usual while dockerizing the rest. This way we solve the annoying part of managing different databases at the cost of not solving the parity in system dependencies.

It’s not perfect, but it’s simple. It’s getting 80% of benefits for 20% of effort. The end result should be running bin/dev and bin/rails test as usual. Not a single command would have to run within a container.

Implementation

There are three steps to turn a regular setup to a hybrid Docker Compose one. We’ll write the docker-compose.yml specification of our databases, update the ports in Rails configuration files, and finally include Docker Compose in our Procfile.dev.

The Docker Compose file for a typical new Rails application with a relational database and Redis server might look like the following:

# docker-compose.yml
version: '3.7'

services:
  postgres:
    image: postgres:14.2
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
    ports:
      - 54320:5432
    volumes:
      - postgres:/var/lib/postgresql/data

  redis:
    image: redis:5.0.4
    command: redis-server /etc/redis.conf
    ports:
      - 63790:6379
    volumes:
      - redis:/data

volumes:
  redis:
  postgres:

The first thing you might notice is that it’s very short and understandable. Two database services, each with a volume for data and ports we expose to the host. The PostgreSQL server is run with a default password while we can omit a password for Redis.

Remember that some other services or databases might require different development and test entries, but this is not necessary here as we can use the same servers for both environments (the database name will differ).

Running with this Compose setup is as easy as typing docker-compose up and updating your Rails configuration.

If you have to run Docker with sudo, add your user to the docker group first:

$ sudo gpasswd -a $USER docker
$ newgrp docker

And start Docker Compose:

$ docker-compose up

docker-compose up should download the database images and start these two services for you.

Now that your databases are ready, update the Rails configuration:

# config/database.yml
development:
  <<: *default
  username: postgres
  password: postgres
  # 5432 for local, 54320 for Docker Compose
  port: 54320
  host: "0.0.0.0"
  database: app_development
...

# config/cable.yml
development:
  adapter: redis
  # 6379 for local, 63790 for Docker Compose
  url: redis://localhost:63790/1
...

At this point you should be able to run bin/rails s, bin/rails test and other usual commands against these new databases.

Finally, to put these things together, we’ll update Procfile.dev:

$ cat Procfile.dev
web: bin/rails server -p 3000
css: yarn build:css --watch
live_reload: bin/guard
js: yarn build --watch
services: docker-compose up

Conclusion

If we now want to start Rails in development, all we have to do is to run bin/dev as usual.

We haven’t solved everything with the new setup, but we gained a lot for very little effort. I think that’s the setup I’ll go with in my kit.

← IT'S OUT NOW

I wrote a complete guide on web application deployment. Ruby with Puma, Python with Gunicorn, NGINX, PostgreSQL, Redis, networking, processes, systemd, backups, and all your usual suspects.

More →