DEV Community

Andrew Welch
Andrew Welch

Posted on • Originally published at nystudio107.com on

Running Node.js in Docker for local development

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

Run your node js apps buildchains via docker

Devops folks who use Dock­er often have no desire to use JavaScript, and JavaScript devel­op­ers often have no desire to do devops.

How­ev­er, Node.js + Dock­er real­ly is a match made in heaven.

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 / Gulp / Mix / web­pack / NPM 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.

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 itself.

If you’re the TL;DR type, you can check out the exam­ple project we used eleven­ty-blog-base feature/​docker branch, and look at the master..feature/docker diff.

Why in the world would I use Docker?

I think this tweet from Adam Wathan is a per­fect exam­ple of why you would want to use Docker:

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

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.

Is it quick­er to just start installing stuff via Home­brew 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 article.

I also high­ly rec­om­mend the Dock­er Mas­tery 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

We’re using make with a Make­file 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 that con­tains the infor­ma­tion need­ed to build & run our Dock­er container.

We then lever­age NPM scripts in the scripts sec­tion of our package.json to run our build­chain / application:

Docker package scripts

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 build script that’s in the scripts sec­tion of our package.json.

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

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 sta­t­ic site generator.

So what we’ll do is make a clone of the eleven­ty-base-blog repo:


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

Then we’ll make just one change to the package.json that comes from the repos­i­to­ry, adding an install 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 Makefile 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 make works is that if you type make, it looks for a Makefile 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 make as a short­cut to run much more com­pli­cat­ed com­mands that aren’t fun to type:

  • make docker — 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
  • make npm xxx — once built, this will run our Dock­er con­tain­er, and exe­cute the NPM script named xxx as list­ed in the package.json. For instance, make npm build will run the build script

The TAG?=12-alpine line pro­vides a default Node.js tag 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 TAG?=14-alpine and do a make docker or we could pass it in via the com­mand line for a quick tem­po­rary change: make docker TAG=14.alpine

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

The </kbd> you see in the Makefile 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.

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

We do the port map­ping to allow 11ty’s hot reload­ing 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 Dockerfile is pret­ty small, but let’s break down what it’s doing:

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

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

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

CMD ["build"] — Set the default com­mand to build

ENTRYPOINT ["npm", "run"] — When the con­tain­er is spun up, it’ll exe­cute npm run xxx where xxx is an argu­ment passed in via the com­mand line, or it’ll fall back on the default build 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 make docker:


❯ 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 install script we added to our package.json via make npm install. This runs an npm install, which we only need to do once to get our node_module 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 make npm serve. 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 http://localhost:8080 and we’ll see our web­site up and running:

Hot reloaded eleventy blog base

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

Real­ize that with the Makefile and Dockerfile 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. No more n. No more has­sles switch­ing Node.js ver­sions.

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 on Twitter.

Copyright ©2020 nystudio107. Designed by nystudio107

Top comments (1)

Collapse
 
edo78 profile image
Federico "Edo" Granata

Really interesting article. I can't believe it received so little love.