How many times have you deployed your app that was working perfectly in your local environment to production, only to see it break? Whether it was directly related to the bug or feature you were working on, or another random issue entirely, this happens all too often for most developers. Errors like this not only slow you down, but they're also embarrassing.
Why does this happen? Usually, it's because your development environment on your local machine is different from the production environment you're deploying to. The tenth factor of the Twelve-Factor App is Dev/prod parity. This means that your development, staging, and production environments should be as similar as possible. The authors of the Twelve-Factor App spell out three "gaps" that can be present. They are:
In this post, we will mostly focus on the tools gap, and how to bridge that gap in a Node application with Docker.
In the Node ecosystem, the tools gap usually manifests itself either in differences in Node and npm versions, or differences in package dependency versions. If a package author publishes a breaking change in one of your dependencies or your dependencies' dependencies, it is entirely possible that your app will break on the next deployment (assuming you reinstall dependencies with npm install on every deployment), while it runs perfectly on your local machine.
Although you can work around this issue using tools like npm shrinkwrap, adding Docker to the mix will streamline your deployment life cycle and minimize broken deployments to production.
Docker is unique because it can be used the same way in development and production. When you enable the architecture of your app to run inside containers, you can easily scale out and create small containers that can be composed together to make one awesome system. Then, you can mimic this architecture in development so you never have to guess how your app will behave in production. In regards to the time gap and the personnel gap, Docker makes it easier for developers to automate deployments, thereby decreasing time to production and making it easier for full-stack teams to own deployments.
When developing inside Docker containers, the two most important concepts are docker-compose and volumes.
docker-compose helps define mulit-container environments and the ability to run them with one command.
Here are some of the more often used docker-compose commands:
Volumes allow you to mount files from the host machine into the container. When the files on your host machine change, they change inside the container as well. This is important so that we don't have to constantly rebuild containers during development every time we make a change. You can also use a tool like node-mon to automatically restart the node app on changes.
Let's walk through some tips and tricks with developing Node apps inside Docker containers.
When you start a new project with Docker, you'll first want to define a barebones Dockerfile and docker-compose.yml to get you started. Here's an example Dockerfile:
FROM node:6.2.1
RUN useradd --user-group --create-home --shell /bin/false app-user
ENV HOME=/home/app-user
USER app-user
WORKDIR $HOME/app
This Dockerfile displays two best practices:
Here's an example starter docker-compose.yml:
web:
build: .
volumes:
- .:/home/app-user/app
Pretty simple right? Here we are telling Docker to build the web service based on our Dockerfile and create a volume from our current host directory to /home/app-user/app inside the container.
This simple setup lets you build the container with docker-compose build and then run bash inside it with docker-compose run --rm web /bin/bash. Now, it's essentially the same as if you were SSH'd into a remote server or working off a VM, except that any file you create inside the container will be on your host machine and vice versa.
With that in mind, you can bootstrap your Node app from inside your container using npm init -y and npm shrinkwrap. Then, you can install any modules you need such as Express.
With that done, we need to update our Dockerfile to install dependencies from npm when the image is built. Here is the updated Dockerfile:
FROM node:6.2.1
RUN useradd --user-group --create-home --shell /bin/false app-user
ENV HOME=/home/app-user
COPY package.json npm-shrinkwrap.json $HOME/app/
RUN chown -R app-user:app-user $HOME/*
USER app-user
WORKDIR $HOME/app
RUN npm install
Notice that we had to change the ownership of the copied files to app-user. This is because files copied into a container are automatically owned by root.
We also need to make an update to our docker-compose.yml to make sure that our modules are installed inside the container properly.
web:
build: .
volumes:
- .:/home/app-user/app
- /home/app-user/app/node_modules
Without adding a data volume to /home/app-user/app/node_modules, the node_modules wouldn't exist at runtime in the container because our host directory, which won't contain the node_modules directory, would be mounted and hide the node_modules directory that was created when the container was built. For more information, see this Stack Overflow post.
Once you've got an entry point to your app ready to go, simply add it as a CMD in your Dockerfile:
CMD ["node", "index.js"]
This will automatically start your app on docker-compose up.
Running tests inside your container is easy as well.
docker-compose --rm run web npm test
You could easily hook this into CI.
Now going to production with your Docker-powered Node app is a breeze! Just use docker-compose again. You will probably want to define another docker-compose.yml that is especially written for production use. This means removing volumes, binding to different ports, setting NODE_ENV=production, and so on. Once you have a production config file, you can tell docker-compose to use it, like so:
docker-compose -f docker-compose.yml -f docker-compose.production.yml up
The -f lets you specify a list of files that are merged in the order specified.
Here is a complete Dockerfile and docker-compose.yml for reference:
# Dockerfile
FROM node:6.2.1
RUN useradd --user-group --create-home --shell /bin/false app-user
ENV HOME=/home/app-user
COPY package.json npm-shrinkwrap.json $HOME/app/
RUN chown -R app-user:app-user $HOME/*
USER app-user
WORKDIR $HOME/app
RUN npm install
CMD ["node", "index.js"]
# docker-compose.yml
web:
build: .
ports:
- '3000:3000'
volumes:
- .:/home/app-user/app
- /home/app-user/app/node_modules
John Oerter is a software engineer from Omaha, Nebraska, USA. He has a passion for continuous improvement and learning in all areas of software development, including Docker, JavaScript, and C#. He blogs here.