Dockerize your Development Environment for NodeJS
Tags: javascript, docker, node, webdev
Published: 2020-10-06 11:05:23

Using Docker in your development workflow has a positive impact on your productivity. It eliminates the typical "It worked on my machine" type of bugs and the setup on a different machine only requires a running Docker daemon and nothing else.
Before we get started implementing we will go over Docker real quick.

### What is Docker?

Docker is a platform that can run *containers*, packages of software. To run these containers Docker uses OS-level virtualization. You can think of a container as a lightweight version of a virtual machine.

All containers you run on your Docker platform are isolated from one another. For example, the host, on which Docker runs, and one container running on the host, do not share the same filesystem except to explicitly tell them to.

To start a container you need a Docker *image*. This image is the blueprint for your container. You can take already predefined images from [Docker-Hub](https://hub.docker.com/) or configure your own ones by writing a so-called Dockerfile.

This is just a quick overview of Docker if you want to dig deeper I encourage you to start [here](https://www.docker.com/resources/what-container).

### Why would you dockerize your development workflow?

In the introduction, I already touched on one benefit of using Docker in your development environment. This being the fact that it gets rid of the typical "It works on my machine" issue. Some other benefits are:

- Standardize development workflow between team members even more
- Reduction of production-only bugs if you use Docker for deployment too (Configurations between production and development can be quite similar)
- Getting rid of the forementioned "Works on my machine" type of bugs

### Getting started
We start out by creating a new folder in which we place our project, and we create our Dockerfile like this:

```Shell
$ mkdir node-docker && cd node-docker
$ touch Dockerfile
```

#### Dockerfile
The container that we will use for our express application will be configured in the Dockerfile. For that, we need to give it some life:
```Docker
FROM node:latest

WORKDIR /usr/src/app
COPY package*.json ./
ENV PORT 5000

RUN npm cache clear --force && npm install

ENTRYPOINT ["npm", "start"]
```
**FROM** tells Docker to get an image called *node* (version: latest) from the docker hub.

**WORKDIR** sets the directory in which all the upcoming commands will be executed.

**COPY** does exactly what it says, it gets the *package.json* and *package-lock.json* and copies it to the *WORKDIR*.

**ENV** sets an environment variable inside the container with the name *PORT* and the value 5000

**RUN** executes the commands we pass in. In this case, clearing the npm cache and then installing all the dependencies from *package.json*.

**ENTRYPOINT** executes the command you insert here, right when the docker container is started

#### Simple Express App
Now that we have our Dockerfile ready to go we need a simple express application that we can run inside a container. For that, we create two new files like this:
```Shell
$ touch server.js package.json
```
*package.json* will get two dependencies, first express, and second nodemon:
```JSON
{
  "name": "node-docker",
  "version": "1.0.0",
  "description": "",
  "main": "server.js",
  "scripts": {
    "start": "nodemon server.js"
  },
  "author": "Jakob Klamser",
  "license": "MIT",
  "dependencies": {
    "express": "^4.17.1"
  },
  "devDependencies": {
    "nodemon": "^2.0.4"
  }
}
```
The express application will just return simple HTML when hitting the main page. Therefore *server.js* should look like this:
```JavaScript
const express = require('express');

const app = express();

const PORT = process.env.PORT || 5000;

app.get('/', (req, res) => {
  res.send(`
    <h1>Express + Docker</h1>
    <span>This projects runs inside a Docker container</span>
  `);
});

app.listen(PORT, () => {
  console.log(`Listening on port ${PORT}!`);
});
```

#### .dockerignore
Before we start setting up a MongoDB container together with our express container, we want to exclude some files from the running container. The syntax of a *.dockerignore* files is exactly the same as for a *.gitignore* file:
```SQL
# Git
.git
.gitignore

# Docker
Dockerfile
docker-compose.yml

# NPM dependencies
node_modules
```
#### docker-compose.yml
Last but not least we want to define a *docker-compose.yml*. This file will contain all the information needed to run the express application and the MongoDB at the same time in two different containers. Let's go ahead and create the file.
```Shell
$ touch docker-compose.yml
```
Now we configure it like this:
```YAML
version: '3'
services:
  api:
    build: .
    ports:
      - "5000:5000"
    depends_on:
      - mongo
    volumes:
      - "./:/usr/src/app"
      - "reserved:/usr/src/app/node_modules"
  mongo:
    image: "mongo" 
    ports:
      - "27017:27017"
volumes:
  reserved:
```
**version**: First we define the version of the docker-compose we want to use. There are quite a lot of differences between version 3 and 2, so be careful when picking a version!

**services**: This is the section in which we define our express API (api) and the MongoDB (mongo)

**build & image**: *build* tells Docker to build an image out of a Dockerfile. In our case we want it to use the Dockerfile in the current directory. That's why we put . as a parameter because this defines the current directory. *image* tells Docker to pull an already existing image from docker hub.

**ports & volumes**: As the name of *ports* suggests we define the ports here. The colon is a mapping operator. We map the port 5000 of the container to the port 5000 of our host system, in this case, our local machine so that we can access the application outside of the container. The same goes for the port mapping of the MongoDB. *volumes* do something similar but this time with volumes. We map our local directory in which we write our code into the WORKDIR of the container. This way the container immediately reacts if we change anything in the source code.

**reserved**: This is a special volume that the local *node_modules* folder if existing, won't override the *node_modules* folder inside the container.

If you run the following command Docker will create an image from our Dockerfile and then run both containers (api and mongo):
```Shell
$ docker-compose up
```
If you want to stop the containers just use this command:
```Shell
$ docker-compose down
```

### Conclusion
This is a simple Docker development environment setup that can easily be extended. If you want to change the database or add an Nginx to render your frontend, just go ahead and add a new service to *docker-compose.yml* or change an existing one.

You can also dockerize .NET Core, Java, or GoLang applications if you want to. Tell me about your experience with Docker in the comment section down below, I'd love to hear it!

The code for this is up on my [GitHub](https://github.com/JakobKIT/node-docker) as usual.

<span>Photo by <a href="https://unsplash.com/@exdigy?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Dominik Lückmann</a> on <a href="https://unsplash.com/s/photos/container-port?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Unsplash</a></span>