Decoupling the Node Runtime From Your App with docker-compose

The situation is this: you have a nodejs app/service, it's deployed in various environments (private and public), and you read something like this on twitter:

Decoupling Node Application Code From the Runtime

If you are creating build artifacts that include the nodejs binary, you are going to have to re-build, re-tag, bump-version and re-release/deploy.

The idea here is to decouple the nodejs application code from the nodejs runtime. With orchestration tools like docker-compose you can then orchestrate a volume container that will mount the code, then in another container re-install or re-build node_modules dependencies and run the application.

Use a Code-only Docker Image

Make sure you create a .dockerignore so that the docker context doesn't pick up large directories like node_modules or .git.

node_modules
.git
view raw .dockerignore hosted with ❤ by GitHub

Next, all you need are your source files and any assets. Since we're just using this as a data volume, we'll use tinan/true to immediately exit but keep the data volume exposed to other containers who want to mount it.

FROM tianon/true
ADD . /app
VOLUME /app
view raw Dockerfile hosted with ❤ by GitHub

For this example we'll create a super simple expressjs app that prints out the nodejs version:

var express = require('express ' );
var app = express();
app.get('/', function (req, res) {
res.send('I am running nodejs version: ' + process.versions.node );
});
app.listen(3000, function () {
console.log('Example app listening on port 3000! ' );
});
view raw index.js hosted with ❤ by GitHub

Orchestration

There's a few things to note here:

code:
build: .
app:
image: quay.io/oddnetworks/alpine-nodejs:4.2.2
volumes_from:
- code
ports:
- "8080:3000"
working_dir: /app
command: /bin/ash -c "npm install --production && npm run start"
view raw docker-compose.yml hosted with ❤ by GitHub

Let's run this with docker-compose up:

Building code
Step 1 : FROM tianon/true
latest: Pulling from tianon/true
e3d3859e28f4: Pull complete
4da49b657714: Pull complete
Digest: sha256:0c678029118314264306b49c931d6dce678c8c88143252342a8614210bea4129
Status: Downloaded newer image for tianon/true:latest
 ---> 4da49b657714
Step 2 : ADD . /app
 ---> ae963587d4f7
Removing intermediate container 94aeb2da0e77
Step 3 : VOLUME /app
 ---> Running in f4915f2bb377
 ---> b9052ca7c135
Removing intermediate container f4915f2bb377
Successfully built b9052ca7c135
Creating tmp_code_1
Creating tmp_app_1
Attaching to tmp_code_1, tmp_app_1
tmp_code_1 exited with code 0
app_1  | sample@1.0.0 /app
app_1  | `-- express@4.13.3
app_1  |   +-- accepts@1.2.13
app_1  |   | +-- mime-types@2.1.9
app_1  |   | | `-- mime-db@1.21.0
app_1  |   | `-- negotiator@0.5.3
app_1  |   +-- array-flatten@1.1.1
app_1  |   +-- content-disposition@0.5.0
app_1  |   +-- content-type@1.0.1
app_1  |   +-- cookie@0.1.3
app_1  |   +-- cookie-signature@1.0.6
app_1  |   +-- debug@2.2.0
app_1  |   | `-- ms@0.7.1
app_1  |   +-- depd@1.0.1
app_1  |   +-- escape-html@1.0.2
app_1  |   +-- etag@1.7.0
app_1  |   +-- finalhandler@0.4.0
app_1  |   | `-- unpipe@1.0.0
app_1  |   +-- fresh@0.3.0
app_1  |   +-- merge-descriptors@1.0.0
app_1  |   +-- methods@1.1.1
app_1  |   +-- on-finished@2.3.0
app_1  |   | `-- ee-first@1.1.1
app_1  |   +-- parseurl@1.3.0
app_1  |   +-- path-to-regexp@0.1.7
app_1  |   +-- proxy-addr@1.0.10
app_1  |   | +-- forwarded@0.1.0
app_1  |   | `-- ipaddr.js@1.0.5
app_1  |   +-- qs@4.0.0
app_1  |   +-- range-parser@1.0.3
app_1  |   +-- send@0.13.0
app_1  |   | +-- destroy@1.0.3
app_1  |   | +-- http-errors@1.3.1
app_1  |   | | `-- inherits@2.0.1
app_1  |   | +-- mime@1.3.4
app_1  |   | `-- statuses@1.2.1
app_1  |   +-- serve-static@1.10.0
app_1  |   +-- type-is@1.6.10
app_1  |   | `-- media-typer@0.3.0
app_1  |   +-- utils-merge@1.0.0
app_1  |   `-- vary@1.0.1
app_1  |
app_1  | npm WARN EPACKAGEJSON sample@1.0.0 No repository field.
app_1  | npm WARN EPACKAGEJSON sample@1.0.0 No license field.
app_1  |
app_1  | > sample@1.0.0 start /app
app_1  | > node /app/index.js
app_1  |
app_1  | Example app listening on port 3000!
❯ curl -XGET http://docker.local:8080
I am running nodejs version: 4.2.2

To upgrade the nodejs version, all we need to do is create a new docker image with the patched node binary, update the docker-compose.yml accordingly and issue docker-compose up which should detect the changes and restart the app service. Note the new nodejs version 4.2.3 below:

code:
build: .
app:
image: quay.io/oddnetworks/alpine-nodejs:4.2.3
volumes_from:
- code
ports:
- "8080:3000"
working_dir: /app
command: /bin/ash -c "npm install --production && npm run start"

❯ curl -XGET http://docker.local:8080
I am running nodejs version: 4.2.3

So What's the Point?

Obviously you will need to test your code against the new version of nodejs in CI, and create a new release, but decoupling the code from the runtime prevents your build artifact from mutating between deploys / releases.

back