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_modules
to the.dockerignore
we will need to make sure tonpm install --production
before 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.