Running Node.js in Docker for local development
Tags: javascript, node, webdev, docker
Published: 2020-09-14 12:43:29

---
title: Running Node.js in Docker for local development
published: true
date: 2020-09-14 04:00:00 UTC
tags: javascript,nodejs,webdev,docker
canonical_url: https://nystudio107.com/blog/run-your-node-js-apps-buildchains-via-docker
---

# Running Node.js in Docker for local development

### You don’t need to know Dock­er to ben­e­fit from run­ning local dev Node.js build­chains & apps inside of Dock­er con­tain­ers. You get easy onboard­ing, and less hassle.

Andrew Welch / [nystudio107](https://nystudio107.com)

![Run your node js apps buildchains via docker](https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x675_crop_center-center_82_line/run-your-node-js-apps-buildchains-via-docker.jpg)

Devops folks who use [Dock­er](https://www.docker.com/) often have no desire to use JavaScript, and JavaScript devel­op­ers often have no desire to do devops.

How­ev­er, [Node.js](https://nodejs.org/en/) + [Dock­er](https://www.docker.com/) real­ly is a match made in heaven.

<aside>
                                Hear me out.
                            </aside>

You don’t have to learn Dock­er in depth to reap the ben­e­fits from using it.

Whether you’re just using Node.js as a way to run a build­chain to gen­er­ate fron­tend assets that uses [Grunt](https://gruntjs.com/) / [Gulp](https://gulpjs.com/) / [Mix](https://laravel.com/docs/7.x/mix) / [web­pack](https://webpack.js.org/) / [NPM scripts](https://docs.npmjs.com/misc/scripts), or you’re devel­op­ing full blown Node.js apps, you can ben­e­fit from run­ning Node.js in Docker.

In this arti­cle, we’ll show you how you can uti­lize Dock­er to run your Node.js build­chains & apps & in local dev with­out need­ing to know a whole lot about how Dock­er works.

<aside>
                                Unless you install every <span>NPM</span> pack­age you use glob­al­ly, you already under­stand the need for containerization
                            </aside>

We’ll be run­ning Node.js on-demand in Dock­er con­tain­ers that run in local dev only when you’re or build­ing assets with your build­chain or devel­op­ing your application.

All you’ll need to have [installed is Dock­er](https://docs.docker.com/get-docker/) itself.

If you’re the <kbd>TL;DR</kbd> type, you can check out the exam­ple project we used [eleven­ty-blog-base feature/​docker branch](https://github.com/nystudio107/eleventy-base-blog/tree/feature/docker), and look at the [master..feature/docker diff](https://github.com/nystudio107/eleventy-base-blog/compare/master..feature/docker).

## Why in the world would I use Docker?

I think [this tweet from Adam Wathan](https://twitter.com/adamwathan/status/1128329090828832769) is a per­fect exam­ple of why you would want to use Docker:

<aside>
                                <span>“</span>Upgrad­ed Yarn, which broke <span>PHP</span>, which needs Python to rein­stall, which needs a new ver­sion of Xcode, which needs the lat­est ver­sion of Mojave, which means I need a beer and it’s only noon.” —Adam Wathan
                            </aside>

Adam’s cer­tain­ly not alone, this type of ​“depen­den­cy hell” is some­thing that most devel­op­ers have descend­ed down into at some point or another.

![Dependency hell](https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x854_crop_center-center_100_line/dependency-hell.png)

And hav­ing one glob­al install for your entire devel­op­ment envi­ron­ment only gets worse from here:

- Updat­ing a depen­den­cy like the Node.js ver­sion for one app may break oth­er apps
- You end up using the old­est pos­si­ble ver­sion of every­thing to keep the tee­ter­ing devel­op­ment envi­ron­ment running
- Try­ing new tech­nolo­gies is cost­ly, because your whole devel­op­ment envi­ron­ment is at risk
- Updat­ing oper­at­ing sys­tem ver­sions often means putting aside a day (or more) to rebuild your devel­op­ment environment
- Get­ting a new com­put­er sim­i­lar­ly means putting aside a day (or more) to rebuild your devel­op­ment environment

Instead of hav­ing one mono­lith­ic local devel­op­ment envi­ron­ment, using Dock­er adds a lay­er of con­tainer­iza­tion that gives each app you are work­ing on exact­ly what it needs to run.

<aside>
                                Your com­put­er isn’t dis­pos­able, but Dock­er con­tain­ers are
                            </aside>

Is it quick­er to just start installing stuff via [Home­brew](https://brew.sh/) on your com­put­er? Sure.

But peo­ple often con­fuse get­ting start­ed quick­ly with speed. What mat­ters more is the speed (and san­i­ty) with which you finish.

So let’s give Dock­er a whirl.

## Dock­er set­up overview

We’re not going to teach you the ins and outs of Dock­er here; if you want that, check out the [An Anno­tat­ed Dock­er Con­fig for Fron­tend Web Devel­op­ment](https://dev.to/gaijinity/an-annotated-docker-config-for-frontend-web-development-3kin) article.

I also high­ly rec­om­mend the [Dock­er Mas­tery](https://www.udemy.com/course/docker-mastery/) course (if it’s not on sale now, don’t wor­ry, it will be at some point).

Instead, we’re just going to put Dock­er to work for us. Here’s an overview of how this is going to work:

![Makefile docker container](https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x267_crop_center-center_100_line/makefile-docker-container.png)

We’re using [make](https://en.wikipedia.org/wiki/Make_(software)) with a [Make­file](https://opensource.com/article/18/8/what-how-makefile) to pro­vide a nice easy way to type our ter­mi­nal com­mands (yes, Vir­ginia, depen­den­cy man­ag­ing build sys­tems have been around since 1976).

Then we’re also using a [Dock­er­file](https://docs.docker.com/engine/reference/builder/) that con­tains the infor­ma­tion need­ed to build & run our Dock­er container.

We then lever­age [NPM scripts](https://docs.npmjs.com/misc/scripts) in the <kbd>scripts</kbd> sec­tion of our <kbd>package.json</kbd> to run our build­chain / application:

![Docker package scripts](https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x269_crop_center-center_100_line/docker-package-scripts.png)

So we’ll type some­thing like:

```

make npm build

```

And it will spin up our Node.js Dock­er con­tain­er, and run the <kbd>build</kbd> script that’s in the <kbd>scripts</kbd> sec­tion of our <kbd>package.json</kbd>.

Since we can put what­ev­er we want in the <kbd>scripts</kbd> sec­tion of our <kbd>package.json</kbd>, we can run what­ev­er we want.

<aside>
                                It may seem com­pli­cat­ed, but it’s actu­al­ly rel­a­tive­ly sim­ple how it all works
                            </aside>

So let’s have a look at how this all works in detail.

## Dock­er set­up detail

So as to have a real-world exam­ple, what we’re going to do is cre­ate a Dock­er con­tain­er that builds a web­site using the pop­u­lar [11ty](https://www.11ty.dev/) sta­t­ic site generator.

<aside>
                                Keep in mind that this is just an exam­ple, we could be con­tainer­iz­ing any Node.js build­chain or app
                            </aside>

So what we’ll do is make a clone of the [eleven­ty-base-blog](https://github.com/11ty/eleventy-base-blog) repo:

```

git clone https://github.com/11ty/eleventy-base-blog

```

Then we’ll make just one change to the <kbd>package.json</kbd> that comes from the repos­i­to­ry, adding an <kbd>install</kbd> npm script:

```

{
  "name": "eleventy-base-blog",
  "version": "5.0.2",
  "description": "A starter repository for a blog web site using the Eleventy static site generator.",
  "scripts": {
    "install": "npm install",
    "build": "eleventy",
    "watch": "eleventy --watch",
    "serve": "eleventy --serve",
    "start": "eleventy --serve",
    "debug": "DEBUG=* eleventy"
  },

```

**MAKE­FILE**

Next we’ll cre­ate a <kbd>Makefile</kbd> in the project direc­to­ry that looks like this:

```

TAG?=12-alpine

docker:
	docker build \
		. \
		-t nystudio107/node:${TAG} \
		--build-arg TAG=${TAG} \
		--no-cache
npm:
	docker container run \
		--name 11ty \
		--rm \
		-t \
		-p 8080:8080 \
		-p 3001:3001 \
		-v `pwd`:/app \
		nystudio107/node:${TAG} \
		$(filter-out $@,$(MAKECMDGOALS))
%:
	@:
# ref: https://stackoverflow.com/questions/6273608/how-to-pass-argument-to-makefile-from-command-line

```

The way <kbd>make</kbd> works is that if you type <kbd>make</kbd>, it looks for a <kbd>Makefile</kbd> in the cur­rent direc­to­ry for the recipe to make. In our case, we’re just using it as a con­ve­nient way to cre­ate alias­es that are local to a spe­cif­ic project.

So we can use <kbd>make</kbd> as a short­cut to run much more com­pli­cat­ed com­mands that aren’t fun to type:

- <kbd>make docker</kbd> — this will build our Node.js Dock­er image for us. You need to build a Dock­er image from a Dock­er­file before you can run it as a container
- <kbd>make npm xxx</kbd> — once built, this will run our Dock­er con­tain­er, and exe­cute the NPM script named <kbd>xxx</kbd> as list­ed in the <kbd>package.json</kbd>. For instance, <kbd>make npm build</kbd> will run the <kbd>build</kbd> script

The <kbd>TAG?=12-alpine</kbd> line pro­vides a default [Node.js tag](https://hub.docker.com/_/node) to use when build­ing the image, with the num­ber part of it being the Node.js ver­sion (“alpine” is just a very slimmed down Lin­ux distro).

If we want­ed, say, Node.js 14, we could just change that to be <kbd>TAG?=14-alpine</kbd> and do a <kbd>make docker</kbd> or we could pass it in via the com­mand line for a quick tem­po­rary change: <kbd>make docker TAG=14.alpine</kbd>

<aside>
                                It’s just that easy to switch the Node.js version
                            </aside>

While it’s not impor­tant that you learn the syn­tax of <kbd>make</kbd>, let’s have a look at the two com­mands we have in our <kbd>Makefile</kbd>.

The <kbd>\</kbd> you see in the <kbd>Makefile</kbd> is just a way to allow you to con­tin­ue a shell com­mand on the next line, for read­abil­i­ty reasons.

- <kbd>docker:</kbd> # the com­mand alias, so we run it via <kbd>make docker</kbd>
  - <kbd>docker build \</kbd> # [Build a Dock­er](https://docs.docker.com/engine/reference/commandline/build/) con­tain­er from a Dockerfile
  - <kbd>. \</kbd> # …in the cur­rent directory
  - <kbd>-t nystudio107/node:${TAG} \</kbd> # tag the image with <kbd>nystudio107/node:12-alpine</kbd> (or what­ev­er <kbd>${TAG}</kbd> is)
  - <kbd>--build-arg TAG=${TAG} \</kbd> # pass in our <kbd>${TAG}</kbd> vari­able as an argu­ment to the Dockerfile
  - <kbd>--no-cache</kbd> # Do not use cache when build­ing the image
- <kbd>npm:</kbd> # the com­mand alias, so we run it via <kbd>make npm xxx</kbd>, where <kbd>xxx</kbd> is the npm script to run
  - <kbd>docker container run \</kbd> # [Run a Dock­er](https://docs.docker.com/engine/reference/commandline/container_run/) con­tain­er from an image
  - <kbd>--name 11ty \</kbd> # name the con­tain­er instance ​“11ty”
  - <kbd>--rm \</kbd> # remove the con­tain­er when it exits
  - <kbd>-t \</kbd> # pro­vide a ter­mi­nal, so we can have pret­ty col­ored text
  - <kbd>-p 8080:8080 \</kbd> # map port 8080 from inside of the con­tain­er to port 8080 to serve our hot reloaded files from <kbd>http://localhost:8080</kbd>
  - <kbd>-p 3001:3001 \</kbd> # map port 3001 from inside of the con­tain­er to port 3001 to serve the [Browser­Sync UI](https://www.browsersync.io/) from <kbd>http://localhost:3001</kbd>
  - <kbd>-v `pwd`:/app \</kbd> # mount a vol­ume from the cur­rent work­ing direc­to­ry to <kbd>/app</kbd> inside of the Dock­er container
  - <kbd>nystudio107/node:${TAG} \</kbd> # use the Dock­er image tagged with <kbd>nystudio107/node:12-alpine</kbd> (or what­ev­er <kbd>${TAG}</kbd> is)
  - <kbd>$(filter-out $@,$(MAKECMDGOALS))</kbd> # a fan­cy way to [pass any addi­tion­al argu­ments](https://stackoverflow.com/questions/6273608/how-to-pass-argument-to-makefile-from-command-line) from the com­mand line down to Docker

We do the port map­ping to allow [11ty’s hot reload­ing](https://www.11ty.dev/docs/usage/#re-run-eleventy-when-you-save) to work dur­ing development.

**DOCK­ER­FILE**

Now we’ll cre­ate a Dock­er­file in the project root directory:

```

ARG TAG=12-alpine
FROM node:$TAG

WORKDIR /app

CMD ["build"]

ENTRYPOINT ["npm", "run"]

```

Our <kbd>Dockerfile</kbd> is pret­ty small, but let’s break down what it’s doing:

<kbd>ARG TAG=12-alpine</kbd> — Set the build argu­ment <kbd>TAG</kbd> to default to <kbd>12-alpine</kbd>. If a <kbd>--build-arg</kbd> is pro­vid­ed, it’ll over­ride this so you can spec­i­fy oth­er Node.js version

<kbd>FROM node:$TAG</kbd> — Des­ig­nate which base image our con­tain­er will be built from 

<kbd>WORKDIR /app</kbd> — Set the direc­to­ry where the com­mands in the Dock­er­file are run to <kbd>/app</kbd>

<kbd>CMD ["build"]</kbd> — Set the default com­mand to build

<kbd>ENTRYPOINT ["npm", "run"]</kbd> — When the con­tain­er is spun up, it’ll exe­cute <kbd>npm run xxx</kbd> where <kbd>xxx</kbd> is an argu­ment passed in via the com­mand line, or it’ll fall back on the default <kbd>build</kbd> command

## Tak­ing Dock­er for a spin

So let’s take Dock­er for a spin on this project. First we’ll make sure we’re in the project root direc­to­ry, and build our Dock­er con­tain­er with <kbd>make docker</kbd>:

```

❯ make docker
docker build \
		. \
		-t nystudio107/node:12-alpine \
		--build-arg TAG=12-alpine \
		--no-cache
Sending build context to Docker daemon 438.8kB
Step 1/5 : ARG TAG=12-alpine
Step 2/5 : FROM node:$TAG
 ---> 18f4bc975732
Step 3/5 : WORKDIR /app
 ---> Running in 6f5191fe0128
Removing intermediate container 6f5191fe0128
 ---> 29e9346463f9
Step 4/5 : CMD ["build"]
 ---> Running in 38fb3db1e3a3
Removing intermediate container 38fb3db1e3a3
 ---> 22806cd1f11e
Step 5/5 : ENTRYPOINT ["npm", "run"]
 ---> Running in cea25ee21477
Removing intermediate container cea25ee21477
 ---> 29758f87c56c
Successfully built 29758f87c56c
Successfully tagged nystudio107/node:12-alpine

```

Next let’s exe­cute the <kbd>install</kbd> script we added to our <kbd>package.json</kbd> via <kbd>make npm install</kbd>. This runs an <kbd>npm install</kbd>, which we only need to do once to get our <kbd>node_module</kbd> depen­den­cies installed:

```

❯ make npm install
docker container run \
		--name 11ty \
		--rm \
		-t \
		-p 8080:8080 \
		-p 3001:3001 \
		-v `pwd`:/app \
		nystudio107/node:12-alpine \
		install

> eleventy-base-blog@5.0.2 install /app
> npm install

npm WARN deprecated core-js@2.6.11: core-js@<3 is no longer maintained and not recommended for usage due to the number of issues. Please, upgrade your dependencies to the actual version of core-js@3.

> core-js@2.6.11 postinstall /app/node_modules/core-js
> node -e "try{require('./postinstall')}catch(e){}"

> ejs@2.7.4 postinstall /app/node_modules/ejs
> node ./postinstall.js

Thank you for installing EJS: built with the Jake JavaScript build tool (https://jakejs.com/)

npm WARN lifecycle eleventy-base-blog@5.0.2~install: cannot run in wd eleventy-base-blog@5.0.2 npm install (wd=/app)
npm notice created a lockfile as package-lock.json. You should commit this file.
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@~2.1.2 (node_modules/chokidar/node_modules/fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@2.1.3: wanted {"os":"darwin","arch":"any"} (current: {"os":"linux","arch":"x64"})

added 437 packages from 397 contributors and audited 439 packages in 30.004s

15 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

```

Final­ly, let’s fire up a hot reload­ing devel­op­ment serv­er, and build our site via <kbd>make npm serve</kbd>. This is the only step you’ll nor­mal­ly need to do in order to work on your site:

```

❯ make npm serve
docker container run \
		--name 11ty \
		--rm \
		-t \
		-p 8080:8080 \
		-p 3001:3001 \
		-v `pwd`:/app \
		nystudio107/node:12-alpine \
		serve

> eleventy-base-blog@5.0.2 serve /app
> eleventy --serve

Writing _site/feed/feed.xml from ./feed/feed.njk.
Writing _site/sitemap.xml from ./sitemap.xml.njk.
Writing _site/feed/.htaccess from ./feed/htaccess.njk.
Writing _site/feed/feed.json from ./feed/json.njk.
Writing _site/posts/fourthpost/index.html from ./posts/fourthpost.md.
Writing _site/posts/thirdpost/index.html from ./posts/thirdpost.md.
Writing _site/posts/firstpost/index.html from ./posts/firstpost.md.
Writing _site/404.html from ./404.md.
Writing _site/posts/index.html from ./archive.njk.
Writing _site/posts/secondpost/index.html from ./posts/secondpost.md.
Writing _site/page-list/index.html from ./page-list.njk.
Writing _site/tags/second-tag/index.html from ./tags.njk.
Writing _site/index.html from ./index.njk.
Writing _site/tags/index.html from ./tags-list.njk.
Writing _site/about/index.html from ./about/index.md.
Writing _site/tags/another-tag/index.html from ./tags.njk.
Writing _site/tags/number-2/index.html from ./tags.njk.
Copied 3 files / Wrote 17 files in 0.74 seconds (43.5ms each, v0.11.0)
Watching…
[Browsersync] Access URLs:
 -----------------------------------
       Local: http://localhost:8080
    External: http://172.17.0.2:8080
 -----------------------------------
          UI: http://localhost:3001
 UI External: http://localhost:3001
 -----------------------------------
[Browsersync] Serving files from: _site

```

We can just point our web brows­er at <kbd>http://localhost:8080</kbd> and we’ll see our web­site up and running:

![Hot reloaded eleventy blog base](https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x1032_crop_center-center_100_line/hot-reloaded-eleventy-blog-base.png)

If we make any changes, they’ll auto­mat­i­cal­ly be hot reloaded in the brows­er, so away we go!

<aside>
                                <span>“</span>Yeah, so what?” you say?
                            </aside>

Real­ize that with the <kbd>Makefile</kbd> and <kbd>Dockerfile</kbd> in place, we can hand our project off to some­one else and onboard­ing becomes bliss:

- We won’t need to care what ver­sion of Node.js they have installed
- They don’t even have to have Node.js installed at all, in fact

Addi­tion­al­ly, we can come back to the project at any time and:

- The project is guar­an­teed to work, since the devops need­ed to run it is ​“shrink wrapped” around it
- We can eas­i­ly switch Node.js ver­sions with­out affect­ing any­thing else

No more [nvm](https://github.com/nvm-sh/nvm). No more [n](https://www.npmjs.com/package/n). No more has­sles [switch­ing Node.js ver­sions](https://dev.to/bnevilleoneill/switching-between-node-versions-during-development-3c2l).

## Con­tainer­iza­tion as a way forward

Next time you have the oppor­tu­ni­ty to start fresh with a new com­put­er or a new oper­at­ing sys­tem, con­sid­er tak­ing it.

Don’t install Homebrew.

Don’t install Node.js.

Don’t install dozens of packages.

Instead, take the con­tainer­iza­tion chal­lenge and _just_ install Dock­er, and run every­thing you need from containers

I think you may be pleas­ant­ly sur­prised at how it’ll make your life easier.

## Further Reading

_If you want to be notified about new articles, follow [nystudio107](https://twitter.com/nystudio107) on Twitter._

<small>Copyright ©2020 nystudio107. Designed by nystudio107</small>