DevOps / Docker
Posted 3.20.2018
By Matt Glaser

Using Docker for Environment Parity

if(Dev === Staging === Production){ console.log('Joy') }

As the technology we use to build things continues to evolve and change, the techniques that we use to build and deploy websites grows more complex and sometimes less stable as new methods and software mature and are replaced by newer, cooler, buggier approaches.

The concepts behind the “Twelve-Factor App” are an important bulwark against the rising tide of controlled chaos that we encounter each day as web developers. In general, it’s a specification that emphasizes stability in all facets of development. This is a good thing in our business, but it can be difficult to achieve at a practical level without the right tools.

Worrying about environmental parity is a must if you maintain a website, and it’s also a core concept behind containerization, specifically Docker.

While there are many important core concepts in the Twelve-Factor App specification, this article will focus mainly on the achieving parity between computers and/or servers, which is laid out pretty clearly as Factor X:

X. Dev/prod parity
Keep development, staging, and production as similar as possible

There is an excellent and brief explanation of Factor X here. It says that no matter where your application is (your dev laptop, a cloud server, etc.) the environment that it runs on should be as similar to the others as possible. There are some very good reasons for this.

Without Parity, Every Release to Production is Blind

If you work in Node JS a lot like we do, there are hundreds or thousands of dependencies in even a modestly-sized project’s node_modules folder. Building a Node-based project requires that you have (of course) Node running on your personal machine. There are several ways to install Node depending on your operating system, and at any given time, the latest stable version of Node is a serious moving target, as are the versions of all software in your project.

Quite simply, it is very easy for two developers working on the same project to be working on different versions of Node, and different versions of various dependencies unless you’re pretty careful.

Consider a common setup that doesn’t respect environment parity. Developer A starts the initial project, installs Node on their machine (or already has it installed) and includes a bunch of dependencies in their codebase. Developer B comes on to the project a few months later and installs or updates Node on their machine. All of a sudden, Developer B might be using a different version of Node, maybe even a different major release. It’s even possible that because of this difference, the dependencies will feature different versions.

In actuality, the app will probably still work fine, and it’s easy for these two developers to keep their versions in sync if they want to, but the real problem is what happens on the live site.

There are countless ways to run a NodeJS app on a server. Services like Heroku and AWS have turnkey solutions that will deploy and start your app for you with some automation. Many folks will just stand up their own server and deploy to it directly. No matter what you use to host your app (and this is true for any language), there is a very good possibility that, at some point, you’re going to push some code that works on your machine running Node 9 to a server that is running Node 6 and the whole thing will get borked because you’re using a feature that doesn’t exist on your production server yet.

This is only one small example of what can go wrong without environment parity. Lots of other things can differ between environments, especially within more complex applications. There are ways to avoid this, and it’s not that big of a deal to keep stuff updated, but what if you have not two but 20 developers working on a project? Are you going to trust each of them to be vigilant about software versions? Do you even want them spending time thinking about it?

Parity is built into Docker images

NB! If you're not a web developer, you may not care about what's in this section, but read on if you're curious!

The next section of this article will make a lot more sense if you know at the very least what a Docker image is. It’s kind of hard to find a plain-spoken description of it so I’ll endeavor to sum it up here:

A Docker image is a single file that has everything you need to run your application’s code contained within it.

This is a little simplistic but it’s basically true. Note that I wrote that you’re running your application’s code specifically, not the whole application. When using best-practices, a Docker image is stateless, meaning it doesn’t have your database or any credentials or settings in it. Just the code. So let’s say you have a Docker image running an app you’ve built in NodeJS. That image would probably include:

  • A minimal linux base, like Ubuntu or (most of the time) Alpine
  • A specific version of NodeJS
  • Dependencies, like Express and ReactJS
  • A web server (maybe Express or something else)

If you were just running a simple HTML page, this would be overkill but it could work out of the box. When you need a database or something else that’s stateful, you’ll obviously need other stuff to run the app. Additionally, many applications will have more than one Docker image/container that depend on one another, but let’s keep it simple for now.

The salient information here is that the Docker image is basically immutable and completely portable. It can be destroyed and restarted again and it’ll run exactly the same way no matter where it is. This is exactly what we want. Rather than running the app off your laptop’s OS, running it in a VPS, etc, where you can easily diverge in terms of compatible software and versions, you run everything from the same Docker image, which essentially guarantees parity.

This isn’t a new concept; tools like Ansible, Puppet/Chef and Vagrant have been doing this kind of work for years. Containerization also isn’t new, it’s just that Docker has finally made it (more or less) accessible to the unwashed masses. The cool part of this is that when you build a Docker image, environmental parity is right in there, free of charge, unless you work really hard to break it.

Prescriptions for parity in Docker

Think of these points as sort of sub-factors to Factor X: Dev/prod parity. These are rules that we follow at Aleph to make sure our dev environments are as close as possible to production.

NB! These are mainly about keeping tooling the same. The CI/CD stuff that helps the personnel and time gap stuff in line is for a different article.

  • Use the same Dockerfile for all environments. If you have a Dockerfile-dev file and a Dockerfile-prod file, you’re #doingitwrong.
  • Use Docker Compose locally to mimic your production stack. If your app uses e.g. MongoDB in the cloud, make sure your local environment uses a Docker image with the same version.
  • Write tests against the production build. You shouldn’t push changes until you’ve tested that the production build of your app running in Docker. The best thing you can do is write automated tests to run the Docker container and make sure it’s at least up and running before deploying to the cloud.
  • Use multi-stage builds to make sure that environment parity is present at buildtime as well as runtime.

The Rub

Just because Docker is powerful and makes containerization more accessible doesn’t make it easy to use. Quite honestly, many of the steps we’ve had to take to get it working as a development tool feel like it’s almost not worth it. If you’ve never heard of Docker before, keep the following in mind:

  1. There’s a steep learning curve, even for very talented/experienced developers.
  2. It’s probably overkill for a great many projects.

If you’re still running a dozen WordPress sites on a LAMP stack that you built from scratch on DigitalOcean, but none of them get much traffic and you only push changes to the code via FTP once a year, don’t sweat Docker unless you want a really frustrating new hobby. On the other hand, if you’re switching to (or already using) modern building and testing practices on your website and want your app to work predictably in all environments, Docker could be an essential tool if you have the time to learn it.