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:
Get ready to patch Dec 2nd
CVE-2015-8027 Denial of Service & CVE-2015-6764 V8 Out-of-bounds Access Vulnerabilities
https://t.co/tuhG004XJF
—
Node.js Security (@nodesecurity) November 26, 2015
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 |
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 |
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! ' ); | |
| }); |
Orchestration
There's a few things to note here:
- I'm using an Alpine Linux based nodejs image
- Since we added
node_modulesto the.dockerignorewe will need to make sure tonpm install --productionbefore we run the nodejs application - The nodejs image is built to be slim, so if your application has any node modules that need native compilation, you may need to install some packages before
npm install
| 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" |
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.