Using Docker in your development environment

Javier Aguadero

Javier Aguadero


Share this article

Containers are becoming more and more popular each day and many new container orchestration tools and technologies are appearing on the market. There's lots of discussion about the best way to run your applications in production using containers, how to split your application into microservices, as well as how to create your testing pipeline with containers.

Nowadays, Docker is the most popular tool for creating and managing containers. In conjunction with Docker, you can use Docker Compose, which allows you to manage the different containers of a multi-container application.

Containers provide improvements to CI pipelines, allowing you to reproduce your testing environment for each execution. Containers they facilitate the way you manage your applications in production environments with immutable containers, and can also improve your development environment. In this article, I talk about the advantages of using containers in your development environment as well as the benefits of setting up docker-compose in all of your projects.

3 reasons why you should use containers for development

1. Isolation

The main advantage of having all your dependencies installed in a container is that they are isolated. You can have different environments set for many different projects that use different versions of those dependencies without any conflict between them. Installing any new dependency or technology in your container environment won't affect any of your other environments nor your host machine, meaning you don't need to be worried about any "update breaking everything".

2. Reproducibility

With containers, you can easily reproduce your environment. If something goes wrong, you can just destroy your container and start a new one based on a prebuilt image. You don't need to waste time trying to figure out what installation or modification has messed everything up, you can just recreate your environment.

3. Lightweight

As they are based on the kernel of the host machine, your container only contains the software you need for running your application, making it smaller than a virtual machine.

Locking your dependencies in your image to build the containers faster

Every project has its own dependencies, and these dependencies might change during the life of a project. To make your application work in your container, these dependencies must be present in the container and must be updated if they change.

Let's say you have a Ruby on Rails project. You can use the Bitnami Rails container image, a Docker image which is ready to be used with an already existing Ruby on Rails project. During the creation of the container, this image automatically checks the dependencies of the project and installs anything that is needed. This means that, if your Docker image doesn't have any of your project's dependencies installed, it will install them each time you create your container, making the creation of the container quite slow.

To speed this up, you can create your own image instead of using a prebuilt one. To create your own image, create a Dockerfile in your project and then use the command docker build. In the case of our example, it would look similar to the following:

FROM bitnami/rails:latest

MAINTAINER <YOUR TEAM> # Your team name and email

RUN install_packages <SYSTEM PACKAGES> # Any package needed by any of the dependencies of your project

COPY Gemfile* /app/
WORKDIR /app

RUN bundle install # We install the dependencies inside the image

This Dockerfile uses the Bitnami Rails Docker image as a base image. Then, it installs any system package required by any of the dependencies, copies the Gemfile inside the image and installs the gems using bundler.

Once you have this file in your project, you can build your image by running the following:

$ docker build -t myapp_image .

This creates an image called myapp_image. You can push your image to any container registry you like. This means, the rest of your team members don't need to build the image from scratch, they just need to download the prebuilt image. A container registry is a service that allows you to upload and store Docker images publicly or privately. The following are some examples of the registries you can use:

Additionally, you can create a job in your CI pipeline to automatically create and push your image when there's any change made in the project's dependencies.

What's docker-compose? Why should I add it to my projects?

docker-compose is a Docker tool that allows you to define a multi-container application and manage its containers. Everything is defined in a docker-compose.yml file which should be present in the root of your project. This file defines the containers your application uses and the configuration the containers use. As this file resides in your project's repository, any developer in your team who has Docker and Docker Compose installed can use this configuration and start the project without having to set up anything else.

Let's continue with the Ruby on Rails project mentioned above. At the root of the project, you can place the following sample docker-compose.yml file:

version: '2'

services:
  mariadb:
    image: 'bitnami/mariadb:latest'
    environment:
      - ALLOW_EMPTY_PASSWORD=yes

  myapp:
    tty: true # Enables debugging capabilities when attached to this container.
    image: 'bitnami/rails:latest'
    environment:
      - DATABASE_URL=mysql2://mariadb/my_app_development
    depends_on:
      - mariadb
    ports:
      - 3000:3000
    volumes:
      - .:/app

Once this file is in the remote repository, any developer can clone the repo and run the following command:

$ docker-compose up

When it finishes, a database container running MariaDB, and a Rails container running the application are created, both of which are connected and already configured. Any new developer that joins to the team just needs to follow the same procedure to get the application running; there's no need to install any dependency other than docker or docker-compose. How many times have you spent hours installing dependencies for a project to make it work? With docker-compose it only takes one minute to have the application up and running.

Because the images used in the containers are defined in the docker-compose.yml file, this no only allows you to reduce the time needed to start working on a project, it also ensures that all the developers in your team uses the same environment.

Working on more than one branch in your project

At Bitnami, I usually work on different features of the same project at the same time. While I wait for the code review and CI testing to finish, I usually create a new branch and start working on a new feature. This means that I sometimes have different branches in the same project which use different dependencies. This is because a new feature might require a new dependency or requires a dependency update. Therefore, each time you switch between branches (because tests didn't pass or someone asked you to add an extra change), you must handle the differences in each of the dependencies and then reinstall the dependencies again (which could cause issues). In these situations containers and docker-compose are very useful.

Let's continue with the same Rails example above. As I mentioned, the Bitnami Rails container automatically checks the dependencies required. Imagine that you have two different branches: branch A which contains the older version of the dependencies (that are no longer installed in the container), and branch B which has the new dependencies already installed. If you are at branch B, and you want to go back to branch A you just need to run the following:

$ git checkout A
$ docker-compose stop myapp
$ docker-compose rm myapp
$ docker-compose up myapp

Using these commands, you have switched back to branch A, stopped and destroyed the application container and created it again. When creating the container again, it contains the old dependencies, as they are the ones locked in the image; it won't install the new ones as they are not present in branch A. When switching back to branch B again, you just need to recreate your container which will install the dependencies for you in a new fresh container. This approach prevents possible inconsistency issues generated by installing and uninstalling dependencies.

Extra: Optimize your images

Docker images are composed of layers and these layers can be shared between different images. This means that, if you have many projects and each of those projects have their own image, you can save a lot of space by reusing some of the layers used in your images.

All the Bitnami Docker images are based on minideb, a minimal Linux distribution based on Debian created by Bitnami. This means that all the images share the first layers.

To see where each of these layers are used, you can use dockviz, a tool that shows you all the hierarchy of the layers and images. The following is an example of its output:

└─<missing> Virtual Size: 51.7 MB
  └─<missing> Virtual Size: 51.7 MB
    └─<missing> Virtual Size: 86.0 MB
      └─<missing> Virtual Size: 86.0 MB
        └─<missing> Virtual Size: 106.2 MB
          └─<missing> Virtual Size: 106.2 MB
            └─<missing> Virtual Size: 106.2 MB
              └─<missing> Virtual Size: 106.2 MB
                └─<missing> Virtual Size: 107.7 MB
                  └─<missing> Virtual Size: 107.7 MB
                    └─<missing> Virtual Size: 107.7 MB
                      └─<missing> Virtual Size: 107.7 MB
                        └─<missing> Virtual Size: 107.7 MB
                          ├─<missing> Virtual Size: 313.1 MB
                          │ ├─<missing> Virtual Size: 313.1 MB
                          │ │ └─<missing> Virtual Size: 384.7 MB
                          │ │   └─<missing> Virtual Size: 411.1 MB
                          │ │     └─<missing> Virtual Size: 411.1 MB
                          │ │       └─<missing> Virtual Size: 411.1 MB
                          │ │         └─<missing> Virtual Size: 411.1 MB
                          │ │           └─<missing> Virtual Size: 411.1 MB
                          │ │             └─<missing> Virtual Size: 411.1 MB
                          │ │               └─7d272c8236fc Virtual Size: 411.1 MB Tags: bitnami/ruby:2.1
                          │ ├─<missing> Virtual Size: 313.1 MB
                          │ │ └─<missing> Virtual Size: 384.7 MB
                          │ │   └─<missing> Virtual Size: 412.3 MB
                          │ │     └─<missing> Virtual Size: 412.3 MB
                          │ │       └─<missing> Virtual Size: 412.3 MB
                          │ │         └─<missing> Virtual Size: 412.3 MB
                          │ │           └─<missing> Virtual Size: 412.3 MB
                          │ │             └─<missing> Virtual Size: 412.3 MB
                          │ │               └─ec82d1bc3733 Virtual Size: 412.3 MB Tags: bitnami/ruby:2.2
                          │ └─<missing> Virtual Size: 313.1 MB
                          │   └─<missing> Virtual Size: 384.7 MB
                          │     └─<missing> Virtual Size: 413.3 MB
                          │       └─<missing> Virtual Size: 413.3 MB
                          │         └─<missing> Virtual Size: 413.3 MB
                          │           └─<missing> Virtual Size: 413.3 MB
                          │             └─<missing> Virtual Size: 413.3 MB
                          │               └─<missing> Virtual Size: 413.3 MB
                          │                 └─<missing> Virtual Size: 413.3 MB Tags: bitnami/ruby:2.3
                          └─<missing> Virtual Size: 107.7 MB
                            └─<missing> Virtual Size: 108.2 MB
                              └─<missing> Virtual Size: 592.1 MB
                                └─<missing> Virtual Size: 592.1 MB
                                  └─<missing> Virtual Size: 592.1 MB
                                    └─<missing> Virtual Size: 592.1 MB
                                      └─<missing> Virtual Size: 592.1 MB
                                        └─6349cd7c9dff Virtual Size: 592.1 MB Tags: bitnami/mariadb:latest

As you can see, the first layers are shared between all the images, and there's one extra layer shared between all the Ruby images.

All this means is that, if you need special dependencies for your tests, you can create a test image based on your development image, which uses less space than creating two different images from scratch which might introduce extra changes in the layers underneath. This also affects the time it takes to pull the image from the registry, because if the layer is already present in your environment, it won't be downloaded again.

Conclusions

Containers not only help you in your CI or production environment, they also provide improvements to your development environment. They help you lock your dependencies and make sure all your team members use the same environment. Containers are lightweight and allow you to easily switch between different branches of your repository that uses different dependencies. Using docker-compose in your projects also reduces the amount of software the team members joining your projects need to install and configure the project can work in their environment. All of this simplifies the management of your local environment, spending less time on fixing issues or updating dependencies and more time on developing.

If you want to start using containers in your projects, checkout our Bitnami's containers offering where you can find a list of Docker images as well as docker-compose files for those images to use in your projects.