In this blog series, we’ll look at how Docker is used at Movio. In this first post, we’ll explore how we use it to streamline our development process. In the next few posts, we’ll explore how we use it in our test and production environments.
Splitting Movio’s core applications into smaller, single-responsibility microservices is part of our strategy for dealing with code complexity and preserving flexibility with tech choices. However, a microservice-based approach introduces other challenges, such as ensuring that an application’s environment is properly configured. To support this, we introduced Docker as a core part of our infrastructure.
Docker lets you package an application within a complete OS environment, which then runs in a software container. A software container is similar to a virtual machine, but much closer to the metal - for example, memory isn’t preallocated for the container, but is requested dynamically from the host.
Using a container means that things like library dependencies can be pre-installed independent of the host system. Other dependencies such as configuration files can also be preset with sane defaults and placed at the expected location for the application. The end result is that with Docker we end up with a standardized environment for each application that runs the same way on any system. What libraries and packages you have installed on the host system doesn’t matter, because the application won’t interact with it.
For the development team, this leads to some neat benefits:
Applications run predictably on different operating systems
For example, one of the more painful differences between OS X and different flavours of Linux is where Apache is installed and how it gets configured (or if the default web server is even Apache!). Running it inside a container means that everyone can rely on the same configuration.
In the cases where you do need custom configuration, Docker lets you set environment variables when you start a container which you can use to tweak the way the container operates. For more complex configuration, Docker gives you the functionality to mount directories and files from the host into a container. We’ve found that a good practice here is, where possible, to prefer using a folder of separate configuration files and include them into a master configuration. These two techniques allow a very precise level of fine-tuning of a container.
Alternatively, you might find yourself in a situation where you want to mount a directory so that data created by a process inside the container will live beyond the lifetime of the container. One use case for this is when you have a database, where you might want the data persisted even if the container is stopped, removed or crashes. Mounting the application’s data directory is a quick way to achieve this as it will store all data on the host.
Integration between components is fairly painless
If you have a microservice that needs to integrate with a microservice maintained by another squad, a docker pull is all you need to do to have that microservice up and running within your local environment, without having to worry about dependencies.
A major upside is that this process faithfully replicates how these services interact in production, down to how they would communicate. This gives us a lot of confidence that our test results in a local environment accurately reflect what would happen in a production environment.
Docker images are composable
This lets us easily reuse work we’ve already done. We have a curated collection of core images that we build from, which means that all our images have the same basic infrastructure available. This makes it easy for squads to use images prepared by other squads.
Internally, Docker handles this by storing each part of an image as a distinct layer and composing them to produce the final image. A benefit of this approach is that by sharing a common base, the total size of our Docker repositories is vastly reduced. It also means that pulling down new images takes much less time because only layers unique to the image you’re pulling are retrieved.
Using Docker encourages closer collaboration between developers, testers and operations
Developers need to consciously think about what is required for an application to run outside of a development environment. This reduces issues associated with over-the-wall mentality, where a “finished” product is handed off to ops with the expectation that they can “just get it running” in production.
Using images also means that a tester’s environment is standardized and aligned with production since all they really need is Docker installed. They also don’t suffer from poor documentation on how to get an application running - they, too, can simply pull the prepared image and starting using it immediately.
Docker introduces a new set of challenges
Relying heavily on Docker’s composition mechanism means that some of our images have a long chain of base image dependencies. For example, let’s say we have a microservice written in Clojure. This service would be built on top of the Clojure image. However, our Clojure image is built from the Java image, which in turn is built from our common base image. If we make a change to the base image, every intermediate image needs to be rebuilt for our microservice to take advantage of it even though the intermediate images haven’t changed. This adds a lot of overhead.
Since Docker relies on Linux kernel functionality, using it on OS X does actually require a virtual machine running a Linux-based OS. The official tools rely on VirtualBox. This creates a unique situation where files and folders must first be mounted to the VM, and then again into the Docker container. While the tools handle this for you already, certain operations such as chown and chmod refuse to work.
A related issue is with case sensitivity in the file system. OS X’s default file system is case insensitive, but typical Linux file systems are case sensitive. If the host runs OS X and the container is Linux-based, incorrectly named files can go unnoticed when the file is in a mounted directory. The moment you try to run that container on a host system with a case-sensitive file system, it won’t be able to find that file.
We work around these issues by having an authoritative Linux-based build machine that both builds and tests our Docker images. This lets us catch errors that might have gone unnoticed when developing.
There is definitely an initial learning curve when it comes to using Docker, but taking everything into account, we’ve found that Docker has increased our development productivity. Onboarding a new member to a squad used to take days to set up their local environment and troubleshooting numerous environmental issues. With Docker, there is very little dependence on the local environment and in most cases we have the application running the same day they are introduced to it. We see efficiencies like this more and more as we grow.
Stay tuned for our next post on how we use Docker to support our continuous integration infrastructure!
Read the rest of our Docker series:
Part Two: How to use Rundeck for Docker builds & deployments