Back to Blog

Dev → Test → Prod Parity with Docker

By Noah Zoschke -

The Twelve Factor App methodology is a series of best practices to follow when building your app to make it easy to deploy to the cloud.

One of the factors is Dev/Prod Parity:

Keep development, staging, and production as similar as possible

It argues that we should aim to reduce gaps between development and production to increase software quality and velocity. Every gap, be it between people, time or computing environments, adds friction.

One way this commonly manifests is that code “works for me!” on your laptop, but doesn’t start on your team mate’s without extra work. The worst case scenario is that this code is deployed and doesn’t work in production, taking the service down.

This friction can add up to significant time over the lifecycle of software. In the above scenario you and your fellow Developers are spending precious time futzing with your laptop environment, and your fellow DevOps engineers are getting paged because the service is down.

Dev/Prod Parity is an aspiration goal in Twelve Factor, but it’s actually possible to achieve with Docker Images and Containers.

What Parity?

A very common setup is Homebrew for development on a Mac laptop, CircleCI for testing and Heroku for production. With these systems, it’s hard to know, let alone guarantee, what version of Node.js, ImageMagick and Postgres are used in each environment.

Laptop, CI Service, and Production Service Incompatibility Matrix Laptop, CI Service, and Production Service Incompatibility Matrix

It’s not impossible to set it all up, but there are pitfalls.

You need to:

  • Maintain good documentation and scripts in the app codebase to help maintain a Homebrew environment
  • Wrangle a circle.yml file, and SSH in to failed test runners to figure out what packages to install to get the test environment running
  • Understand what the Heroku Buildpacks and Runtime add to build and run your app in production
  • Add hooks to your code base to turn functions off or on in development, testing and production

Once it works, it’s smooth sailing. Until you want to upgrade Ruby, or add PhantomJS for web acceptance testing, or Homebrew packages change, or…

Parity Begins in Development With Containers

Containerization technology, made accessible by Docker, offers a better way.

First, install the fantastic Docker for Mac to get a Docker environment running on your laptop. This app is practically magic, and offers a fast Linux environment on your Mac laptop.

Next, put in some effort to “Dockerize” your app. The goal is to boot your app with a single command: docker-compose up or convox start. This involves writing a Dockerfile and a docker-compose.yml file.

Apps that already follow the Twelve Factor Methodology aren’t too hard to get into shape.

First, you need to describe each process in your Procfile as a “Service” in docker-compose.yml. You also need to add a Service for any database your app uses like Postgres or Redis. This frees you from needing to use anything in Homebrew for development.

    version: '2'
    services:
      web:
        build: .
        command: ["node", "web.js"]
        links:
         - redis
      worker:
        build: .
        command: ["node", "worker.js"]
        links:
         - redis
      redis:
        image: redis

Next, you need write a Dockerfile that can build and run your app. You need to pick a base operating system, add system dependencies, then add a few more commands to install the language packages your app uses.

I recommend using Ubuntu 16.04. Ubuntu 16.04 makes it easy to get the latest Node.JS, Ruby, Python, and PHP system packages. It is a “Long Term Support” distribution that will receive security updates through 2021. This is another huge advantage of Docker: we are no longer constrained by the limited CircleCI and Heroku operating system choices.

    # start from a base Image
    FROM ubuntu:16.04
    
    # install system dependencies
    RUN apt-get update && \
      apt-get install -y nodejs npm && \
      update-alternatives --install /usr/bin/node node /usr/bin/nodejs 10
    
    # specify the app location
    WORKDIR /app
    
    # install app dependencies
    COPY package.json /app/package.json
    RUN npm install
    
    # add app source code
    COPY . /app

Getting it all working might take some effort, but it’s so satisfying when docker-compose up works:

    $ docker-compose up

    Creating network "myapp_default" with the default driver
    Pulling redis (redis:latest)...
    Building web
    Step 1 : FROM ubuntu:16.04
    Step 2 : RUN apt-get update &&   apt-get install -y nodejs npm &&   update-alternatives --install /usr/bin/node node /usr/bin/nodejs 10
    Step 3 : WORKDIR /app
    Step 4 : COPY package.json /app/package.json
    Step 5 : RUN npm install
    Step 6 : COPY . /app
    Building worker
    Creating myapp_redis_1
    Creating myapp_web_1
    Creating myapp_worker_1
    redis_1   | 1:M 27 Oct 14:17:16.329 # Server started, Redis version 3.2.0
    worker_1  | [nodemon] 1.11.0
    worker_1  | [nodemon] to restart at any time, enter `rs`
    worker_1  | [nodemon] watching: *.*
    worker_1  | [nodemon] starting `node worker.js`
    worker_1  | worker running
    web_1     | [nodemon] 1.11.0
    web_1     | [nodemon] to restart at any time, enter `rs`
    web_1     | [nodemon] watching: *.*
    web_1     | [nodemon] starting `node web.js`
    web_1     | web running at [http://127.0.0.1:8000/](http://127.0.0.1:8000/)

Test Parity Follows

Now that docker-compose up boots your app, try docker-compose run web npm test or the equvalent. It might just work!

    $ docker-compose run web npm test

    > myapp@1.0.0 test myapp
    >  User
        #create()
          ✓ should create a new user
      ...
      45 passing (1800ms)

If it doesn’t, you’ll need a bit more tweaks to your Dockerfile and docker-compose.yml. You might be missing a few system dependences or environment your tests need.

That effort we put in before is paying off big time. There is little to no configuration needed to run tests. And now every team member can run tests on their laptop with a single command. No Homebrew required!

Test Strategies

If the “Dockerized” app runs on a Docker for Mac environment with a single command, it will certainly run on any test service that supports Docker. There are many:

Before, we needed to write complex configuration for these platforms. Now we need to follow the simple steps to enable Docker and run a single command.

Prod Strategies

If our app is so well behaved that it runs on a laptop and a test environment with a single command, we can confidently deploy it to the cloud.

AWS has a number of Docker strategies:

Not to mention Heroku Container Runtime, Google Container Engine, Azure Container Service, Docker Cloud, Joyent Triton.

There are almost too many strategies here! We will have to explore the pros and cons of these approaches in future post.

But you can be confident that you’ll be able to run your app in the cloud, again with little to no extra configuration.

Conclusion

Dev/Prod parity is a desirable goal and is possible with Docker Images and Containers.

Put in the work to write a Dockerfile and docker-compose.yml file that works on Docker for Mac, and you have unlocked many paths to test your app and run it in production with little to no additional configuration.

This will increase software quality and velocity, and pay big dividends over the lifetime of the software.

What do you think? Are you already enjoying dev/test/prod parity? Is Docker part of your success? If not, how do you plan to get there?