An Exhaustive Guide to Writing Dockerfiles for Node.js Web Apps
Tags: docker, node, kubernetes, webdev
Published: 2019-12-03 11:38:44

---
title: An Exhaustive Guide to Writing Dockerfiles for Node.js Web Apps
published: true
date: 2018-02-07 00:00:00 UTC
tags: docker, nodejs, kubernetes, web development
canonical_url: https://blog.hasura.io/an-exhaustive-guide-to-writing-dockerfiles-for-node-js-web-apps-bbee6bd2f3c4/
---

![](https://blog.hasura.io/content/images/downloaded_images/an-exhaustive-guide-to-writing-dockerfiles-for-node-js-web-apps-bbee6bd2f3c4/1-4KhmpXFJ_Etczs6awRnAbg.png)

## TL;DR

This post is filled with examples ranging from a simple Dockerfile to multistage production builds for Node.js web apps. Here’s a quick summary of what this guide covers:

- Using an appropriate base image (carbon for dev, alpine for production).
- Using `nodemon` for hot reloading during development.
- Optimising for Docker cache layers — placing commands in the right order so that `npm install` is executed only when necessary.
- Serving static files (bundles generated via React/Vue/Angular) using `serve` package.
- Using multi-stage `alpine` build to reduce final image size for production.
- #ProTips — 1) Using COPY over ADD 2) Handling CTRL-C Kernel Signals using `init` flag.

If you’d like to jump right ahead to the code, check out the [GitHub repo](https://github.com/praveenweb/node-docker).

## **Contents**

1. Simple Dockerfile and .dockerignore
2. Hot Reloading with nodemon
3. Optimisations
4. Serving Static Files
5. Single Stage Production Build
6. Multi Stage Production Build

Let’s assume a simple directory structure. The application is called node-app. The top level directory has a `Dockerfile`and `package.json` The source code of your node app will be in `src` folder. For brevity, let’s assume that server.js defines a node express server running on port 8080.

```
node-app
├── Dockerfile
├── package.json
└── src
    └── server.js
```

## **1. Simple Dockerfile Example**
{% gist https://gist.github.com/praveenweb/5a1b3a7ba52d160361bb1830fc43b27d.js %}

For the base image, we have used the latest LTS version`node:carbon`

During image build, docker takes all files in the `context` directory. To increase the docker build’s performance, exclude files and directories by adding a `.dockerignore` file to the context directory.

Typically, your `.dockerignore` file should be:

```
.git
node_modules
npm-debug
```

Build and run this image:

```
$ cd node-docker
$ docker build -t node-docker-dev .
$ docker run --rm -it -p 8080:8080 node-docker-dev
```

The app will be available at [`http://localhost:8080`](http://localhost:8080.). Use `Ctrl+C` to quit.

Now let’s say you want this to work every time you change your code. i.e local development. Then you would mount the source code files into the container for starting and stopping the node server.

```
$ docker run --rm -it -p 8080:8080 -v $(pwd):/app \
             node-docker-dev bash
root@id:/app# node src/server.js
```

## 2. Hot Reloading with Nodemon

[nodemon](https://www.npmjs.com/package/nodemon) is a popular package which will watch the files in the directory in which it was started. If any files change, nodemon will automatically restart your node application.

{% gist https://gist.github.com/praveenweb/92517e26f244260d26569908f8cd8227.js %}

We’ll build the image and run nodemon so that the code is rebuilt whenever there is any change inside the `app` directory.

```
$ cd node-docker
$ docker build -t node-hot-reload-docker .
$ docker run --rm -it -p 8080:8080 -v $(pwd):/app \
             node-hot-reload-docker bash
root@id:/app# nodemon src/server.js
```

All edits in the`app`directory will trigger a rebuild and changes will be available live at [`http://localhost:8080`](http://localhost:8080.). Note that we have mounted the files into the container so that nodemon can actually work.

## 3. Optimisations

In your Dockerfile, prefer COPY over ADD unless you are trying to add auto-extracting tar files, according to [Docker’s best practices](https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#add-or-copy).

Bypass `package.json` ‘s `start` command and bake it directly into the image itself. So instead of

```
$ CMD ["npm","start"]
```

you would use something like

```
$ CMD ["node","server.js"]
```

in your Dockerfile CMD. This reduces the number of processes running inside the container and it also causes exit signals such as `SIGTERM` and `SIGINT` to be received by the Node.js process instead of npm swallowing them. (Reference — [Node.js Docker Best Practices](https://github.com/nodejs/docker-node/blob/master/docs/BestPractices.md#cmd))

You can also use the `--init` flag to wrap your Node.js process with a [lightweight init system](https://github.com/krallin/tini), which will respond to Kernel Signals like `SIGTERM` (`CTRL-C`) etc. For example, you can do:

```
$ docker run --rm -it --init -p 8080:8080 -v $(pwd):/app \
             node-docker-dev bash
```

## 4. Serving Static Files

The above Dockerfile assumed that you are running an API server with Node.js. Let’s say you want to serve your React.js/Vue.js/Angular.js app using Node.js.

{% gist https://gist.github.com/praveenweb/d6665015cee658cc7b883c4bb95d3e29.js %}

As you can see above, we are using the npm package [`serve`](https://www.npmjs.com/package/serve) to serve static files. Assuming you are building a UI app using React/Vue/Angular, you would ideally build your final `bundle` using `npm run build` which would generate a minified JS and CSS file.

The other alternative is to either 1) build the files locally and use an nginx docker to serve these static files or 2) via a CI/CD pipleline.

## 5. Single Stage Production Build
{% gist https://gist.github.com/praveenweb/1c2f7f7182babc5da18869e19c38cad2.js %}

Build and run the all-in-one image:

```
$ cd node-docker
$ docker build -t node-docker-prod .
$ docker run --rm -it -p 8080:8080 node-docker-prod
```

The image built will be ~700MB (depending on your source code), due to the underlying Debian layer. Let’s see how we can cut this down.

## 6. Multi Stage Production Build

With multi stage builds, you use multiple `FROM` statements in your Dockerfile but the final build stage will be the one used, which will ideally be a tiny production image with only the exact dependencies required for a production server.

{% gist https://gist.github.com/praveenweb/acd27014e6d244c3194d3e9d7da04e17.js %}

With the above, the image built with Alpine comes to around ~70MB, a 10X reduction in size. The `alpine` variant is usually a very safe choice to reduce image sizes.

Any suggestions to improve the ideas above? Any other use-cases that you’d like to see? Do let me know in the comments.

Join the discussion on [Reddit](https://www.reddit.com/r/node/comments/7vw6gj/an_exhaustive_guide_to_writing_dockerfiles_for/) / [HackerNews](https://news.ycombinator.com/item?id=16330793) :)

---

This article was originally published on the [Hasura blog](https://blog.hasura.io/) and was authored by Praveen Durairaj.