Docker for Development

Wouter van der Meulen
Wouter van der Meulen
Aug 27 2021
Posted in Engineering & Technology

Accelerate development with Docker Compose

Docker for Development

If you do anything (web) development related you probably have heard of Docker in some way, shape or form. To some, it's a black box that will only complicate things. And to others, it's the greatest invention since sliced bread. There's something to be said about either stance. But in the end, it's a great tool for the right job. Today we will discuss its ability to accelerate development workflows with ease.

As a developer who has worked for agencies with many different projects, my biggest pet peeve is the amount of deviating versions of third party software. The newest project will use PostgreSQL 13, but older projects might still be using version 9. Trying to install multiple versions of Postgres on the local system will inevitably end in conflicts and tears.

Docker and, most importantly, Compose can help us set up development environments without having to install dependencies on our local machine.

What is docker

For the uninitiated, Docker is a lightweight containerization engine. Simply put, it allows you to run software separately from the main operating system. This means that you can pre-package your software to quickly get up and running, as well as running dependencies completely separate from other software.

There is a lot more to Docker than that, but that isn't important for now. I would advise reading the official docs if you're interested in learning more about the technology.

Now, let's set up a development project!

The Project

For this example I will use Ruby on Rails. Not for any other reason than that, it's the framework I'm most familiar with.

Don't worry, we won't be building any features into the website. It will merely act as an example for installing dependencies and setting up database connections.

In order to get a running Rails application we will need two containers, one for the Rails app, and one for our database. We will use Postgres as our database of choice.

The example Ruby on Rails application is available here, including the Docker files.

Docker Files

"What is a Dockerfile?", you might ask. It's a configuration file that defines the steps to create a container image. Let's take a look at a basic example.

#./Dockerfile

FROM ruby:3.0.0

RUN apt-get update -y && apt-get install -y build-essential

It's best to think of the Dockerfile as a list of instructions to install software. As that's what it does most of the time.

We can build the Docker image by running docker build .. During the build, Docker will use the FROM statement to download the ruby image with the 3.0.0 tag from Docker Hub. This is an image provided by Docker themselves, but Docker Hub is filled with community-made Docker images as well. Please, do not carelessly use community images without verifying what is being installed on them. Sometimes, it's best to build your own images from the official images.

After downloading the base image, docker will execute all the defined steps in the file. Docker treats each step as a layer, and will cache it. Any subsequent build will skip unchanged layers, which saves a lot of time.

The Rails application will require Ruby and a few system packages for the basic libraries. The following Docker file will provide us with everything we need for Ruby on Rails:

# ./Dockerfile

# Overridable arguments
ARG RUBY_VERSION=3.0.2

# Source image
FROM ruby:${RUBY_VERSION}

# Variables
ENV APP_HOME /usr/src/app

# Get basic development tools
RUN apt-get update -qq && apt-get install -y build-essential

# For Postgres
RUN apt-get install -y libpq-dev

# For imagemagick
RUN apt-get install -y imagemagick

# For nokogiri
RUN apt-get install -y libxml2-dev libxslt1-dev

# For a JS runtime
RUN apt-get install -y nodejs

# Install yarn through sources
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
RUN echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
RUN apt-get update -y
RUN apt-get install yarn -y

# Turn of documentation for gems to speed up gem installation
RUN echo 'gem: --no-document' >> /etc/.gemrc

# Update rubygems and install (latest) bundler
RUN gem update --system
RUN gem install bundler

# Create basic app directory
RUN mkdir -p $APP_HOME
WORKDIR $APP_HOME

RUN mkdir -p tmp/pids

# Add Gemfile and install gems
ADD Gemfile $APP_HOME
ADD Gemfile.lock $APP_HOME
RUN bundle install

# Add package.json to install frontend dependencies
ADD package.json $APP_HOME
# ADD package-lock.json $APP_HOME
ADD yarn.lock $APP_HOME
RUN yarn install

# Add the full codebase to the container
ADD . /usr/src/app

Compose

Compose, or Docker Compose, is an orchestration tool for Docker. It allows you to set up a Docker infrastructure using a single configuration file.

You might have seen two variations of Docker Compose, one is docker compose and the other is docker-compose. Initially, there was only docker-compose, which was a standalone Python program to provide orchestration. Currently, Docker has included a built-in (GO based) version of compose within the docker command. At the time of writing, it's not as feature complete as the Python version, and might have some minor differences in the actual inner workings. For the purposes of setting up a development environment either will do their job just fine.

The compose file for the Rails application might look like this:

# ./docker-compose.yml
version: '3.7'

services:
  db:
    image: postgres:13 # The image to pull from Docker Hub
    volumes: # Link a local directory to the container, for persistant data
      - db-data:/var/lib/postgresql/data
    environment:
      - POSTGRES_HOST_AUTH_METHOD=trust
  web:
    command: bundle exec puma -C config/puma.rb
    build: . # Will build the local Dockerfile
    depends_on: # Tells Docker that the web service needs to wait for the db to come available
      - db
    ports: # Expose the Rails ports to our local machine
      - "3000:3000"
    environment:
      - DATABASE_NAME=webapp
      - DATABASE_HOST=db
      - RAILS_MAX_THREADS=5
    volumes: # linking the codebase to the container so the container updates after local changes
      - .:/usr/src/app

volumes: # Creates a local volume, this will be stored in a common location. Which keeps database values in a single location
  db-data:

All we're doing is starting a container for Postgres and Rails application. Because no network is defined, compose will automatically connect the two images through a Docker network specific for this Compose file. Inside the Docker network, each image will be made available on their service name. This means that Rails will be able to connect to the database through db:5432.

All that's left to do is to start the application:

docker compose up

Docker will pull and build images if necessary, and then proceed to start them. After booting, the Postgres database won't be populated yet. Rails can create the development and test databases for us.

docker compose exec web bundle exec rake db:setup

If you point the browser to localhost:3000, you should be able to see the lovely Rails splash page. And you're ready to develop. Any changes you make to the local codebase will be synced to the container, so all changes will be updated in a few seconds.

Security note

The Postgres database in this example is set to trust all connections, this is merely for development purposes. Please do not use this compose file in production!

As always, you can find us available for any question you might have via our Support Channel.

Keep up-to-date with the latest news