Why you should use Docker in development.
What is the point of containerizing something like a Rails app in a development environment? What do we get in return for learning Docker and how it works there?
It used to be that if I started programming a new app in say, Ruby on Rails, the way I would setup my development environment (on a Mac) was to use RVM to set the Ruby version, and then use Homebrew to install a bunch of other support things (MySQL, Redis, etc). If I was joining an app already in development (at a new job) then they had a similar procedure. I would be given an “on boarding document” that listed out the steps needed to get the app up and running:
- Set Ruby to 2.3 with RVM
- brew install mysql
- etc, etc.
Someone had written this document for the first hire and it was usually rewritten and expanded by each new person added to the development team (and it still wasn’t ever complete).
That is the old way (or at least the old way I experienced). Sounds terrible now that I remember it. It was terrible and it could easily take a whole day to on board to a new team with an old app. That is really too long.
Is the new Docker way any better? This article will cover the reasons why you should be using Docker in development, with only the basics of terms and how to set it up (which is dependent on the app you are writing anyway). First we will cover a few terms we need to know when talking about Docker.
Docker Terms and Jargon
First, some terminology, starting with what is Docker? Docker is kinda like a lightweight virtual machine. It doesn’t require the loading and virtualization of a whole operating system so Docker starts running quickly and runs more efficiently than a VM (virtual machine) but they are similar concepts.
The first thing you do to use Docker is to write a Dockerfile for your app. A Dockerfile is nothing but a list of instructions including stuff like:
- What image to start from (ruby:2.7 in the example below).
- What commands to run (install nodejs & bundle install in the example below)
- What the working directory is.
- What files to copy from the host computer.
A simple (very simple) Dockerfile for a Rails app looks something like this:
# Dockerfile
FROM ruby:2.7
RUN apt-get update -yqq
RUN apt-get install -yqq --no-install-recommends nodejs
COPY . /usr/src/app/
WORKDIR /usr/src/app
RUN bundle install
From this Dockerfile you can create a Docker image. Docker images are standalone, executable packages that have everything you need to run an application. An image is a saved version of all the instructions you wrote in the Dockerfile. All the dependencies downloaded and commands run to setup your app exactly as it needs.
You create a Docker image from a Dockerfile like this:
# This will run all the instructions in the Docker file and save the resulting image to your computer.
docker build -t my-image-name .
Once you have this Docker Image you can start a Docker container. A Docker container is a running copy of the image that you just created. In the image you probably downloaded the SQL server and pulled in a Rails server. In the container those are running and you should be able to see your app in action. Then it is time to do some programming.
# Start a named docker image
docker run my-image-name
Why use Docker
So why is all of this good for developers? We can certainly install Ruby on a Mac (or whatever) with RVM, brew install nodejs, and then run bundle install without resorting to using Docker even if that is the old fashioned way. But writing a Dockerfile is pretty easy and staring up a Docker container that someone else wrote (your team members when you join a new company) is even easier. So what else do we get by using Docker?
Skip installing dependencies
The first thing that using Docker gives you is that you get to skip installing all the dependencies to run a new app on your computer. Most jobs I have had have started with an “on boarding” document that lists out the steps a new team member needs to take to get the app running on their computer. That is basically a list of software you have to install onto your computer one by one. If you use Docker then all of that is already written up in the Dockerfile and “installing the dependencies” is as simple as building and/or running the docker image.
Everything is in the container
Since all the dependencies you are now installing are staying in the container you won’t affect anything else on your development machine. If you have a different version of (for instance MySQL) installed on your computer then that won’t conflict with the MySQL you have installed using Docker. This also applies for programming language versions and any other dependencies that you need to install.
In development this mean that you get exactly the versions you need and won’t need to debug subtle errors that can arise from version mismatches. It also means that you can have multiple versions of dependencies installed in multiple apps but can avoid any dependency mismatch errors that might arise across apps because the different versions are contained (pun intended) to their own containers.
Easy tear down
Another advantage of Docker is that the tear down is as easy as the setup. When you are done with your development environment then you can stop the Docker containers and delete the images very easily. This is good if you need to move from project to project (for instance if you are a freelancer) but also for dealing with version upgrades in a long running project.
Pretend you are working in a team of 20 and 1 person is tasked with upgrading the app from Rails 6 to Rails 6.1. That person can update the Dockerfile and Docker Image, test that it is working well, and distribute it to the rest of the team. The other 19 team members can delete their old images and containers and run the new containers without concerning themselves that old, unused dependencies will linger on their computer after they are no longer needed.
One Dependency to Rule them All
I’ve been talking about dependencies (and the pain of managing them) a lot in this post. What Docker gives you is the option of having only one dependency needed on your machine: Docker itself. If you have Docker installed then you can run the other dependencies (databases, programming languages, etc) without worrying about how to make them work on your machine. Docker abstracts all that away with it’s containers (which remember are kinda like lightweight Virtual Machines).
Mac, Linux, Windows, Raspberry Pi, Unraid, whatever
Docker works in a lot of places so you have more options for software development. Gone are the days where every new programmer on a team needs a brand new Mac Book Pro at the cost of several thousand dollars. Docker can work on all kinds of machines so your options for development are expanded. I like working on Ubuntu and deploying images to my local network server running Unraid but if you want to work on a Mac or Windows or whatever then go for it.
Production Ready
Since you are developing against a Docker container instead of just installing all the dependencies on your machine you are making your deploy to production (and testing, staging, other staging, etc) much easier. You have already wrapped up the dependencies that you need inside the Docker image so you won’t have to replicate them in production (there are some caveats to this especially for more advanced cases). You can deploy the whole image to your production machine using something like Kamal very easily.
Ready for Testing
Speaking of sending your image to other environments, testing is made easier by using Docker. Plenty of CI/CD providers will use Docker containers as part of the testing before deploy strategy. They can run with the same images you are running in development and production to ensure a consistent experience for your automatic tests or QA people. You can also use Docker containers with GitHub Actions to test new Pull Requests before deploying them as part of an automatic testing strategy.
Conclusion
Dockerizing your app and its dependencies might seem like extra work at first but it really isn’t too difficult. Once you grok the basic concepts thne you can create images that stay the same in development, testing and production. The images will also help you onboard new teammates faster and prevent duplication of efforts when upgrading dependencies. Moving to Docker based development is well worth the effort.