Building nodejs with npm dependencies into a docker container without using a Dockerfile

In this article we will look at a very simple node application that uses Express with three examples for running the application in a docker container using the node:latest image:

  • using a Dockerfile
  • without a Dockerfile with the docker run -v option
  • without a Dockerfile with the docker cp option

When running node applications in docker containers, you typically use a Dockerfile to manage both the source code and any npm dependencies for the application. This is useful for cases where you want to build an image and then docker push it to a repository. There are cases where you want to quickly run the node application with dependencies without also creating and maintaining a Dockerfile. This is especially the case where you are making fast iterations when exploring new ideas in your application code.

Step 1: The node application

Create a new directory called npmdeps

> mkdir npmdeps
> cd npmdeps

We will create three files in the npmdeps directory:

app.js
package.json
Dockerfile

First, create a file called app.js with the following content:

'use strict';

const express = require('express');

// Constants
const PORT = 8888;

// App
const app = express();

app.get('/', function (req, res) {
    res.statusCode = 200;
    res.setHeader('Content-Type', 'text/plain');
    res.end("hello" + '\n');

});

app.listen(PORT);

console.log('Running on http://localhost:' + PORT);

This is a very simple application that uses the Express module to provide http routing. It will run on port 8888.

Next, create a file called package.json with the following contents:

{
    "name": "my-application",
    "version": "0.1.0",
    "description": "A sample Node.js app using Express 4",
    "author": "First Last <first.last@example.com>",
    "main": "app.js",
    "scripts": {
    "prestart": "npm install",
    "start": "node app.js"
    },
    "dependencies": {
    "express": "^4.13.3"
    },
    "license" : "MIT"
}

This package file makes app.js the main application, and specifies both a start and a prestart script. The start script uses the command node app.js to run app.js. The prestart script uses the command npm install to install the dependencies for the application. The dependencies element lists at least version 4.13.3 of the Express module. This is the dependency that is installed by the npm install command.

Step 2: Create the Dockerfile

Create a file called Dockerfile with the following contents:

FROM node:latest

RUN mkdir -p /usr/src/app

WORKDIR /usr/src/app

COPY package.json /usr/src/app/

COPY app.js /usr/src/app/

EXPOSE 8888

This Dockerfile starts with node:latest, copies the sourcecode and exposes port 8888.

Step 3: Build and run the application using a Dockerfile

Use the following commands to build and then run the node application in a docker container, make sure that you are in the npmdeps directory:

> docker build -t npmdepsdf .

> docker run --name npmdepsdf --expose 8888 -w /usr/src/app --net=host -d npmdepsdf npm start

The docker build command builds an image tagged npmdepsdf. The docker run command starts a container based on the npmdepsdf image you just built and exposes the application to port 8888 on the host network. The working directory of /usr/src/app is specified using the -w argument. When the container starts it executes the npm start command. In package.json we have specified both a presart and start script so that npm install is run first by the prestart script and then node app.js is executed by the start script.

On the host running the container, you can test with the command:

> curl localhost:8888
> hello

You can also check the logs for the container in the event something went wrong using the command:

> docker logs npmdepsdf

Step 4: Run the application without a Dockerfile using docker run -v

Stop the npmdepsdf container using the command:

> docker stop npmdepsdf

Make sure that you are still in the npmdeps directory and use the command:

> docker run --name npmdepsvol --expose 8888 -w /usr/src/app --net=host -v $PWD:/usr/src/app -d node:latest npm start

This command will run a container called npmdepsvol with all the same arguments as the docker run command from step 3, with one addition:

-v $PWD:/usr/src/app

This command maps the npmdeps directory on the host to the /usr/src/app directory inside the container. This makes the files app.js and package.json available to the container.

This command is more convenient; especially when you are in testing and development of the application. This works fine so long as your source code is avaiable on the host. However, this is less a secure option, since your applications source code is sitting in a writable directory on the host filesystem.

Step 5: Create and Start the application without a Dockerfile using docker cp

A more secure way to achieving the same result as in step 4 is to use the following sequence of commands:

> docker stop npmdepsvol

> docker create --name npmdepscp --expose 8888 -w /usr/src/app --net=host node:latest npm start

> docker cp . npmdepscp:/usr/src/app

> docker start npmdepscp

This series of commands:

  • creates a container called npsdepscp with the same arguments as from step 3.
  • copies the source code in the current directory (npmdeps) to /usr/scr/app in the container's file system.
  • starts the container

You can now remove the source code from the host file system as the source code is safely tucked away inside the container.

Discussion

While the technique described in Step 5 provides greater security by letting you remove the source code from the host file system, the file system of the container is still read-write.

You can achieve the greatest security by removing the prestart script from package.json:

{
    "name": "my-application",
    "version": "0.1.0",
    "description": "A sample Node.js app using Express 4",
    "author": "First Last <first.last@example.com>",
    "main": "app.js",
    "scripts": {
    "start": "node app.js"
    },
    "dependencies": {
    "express": "^4.13.3"
    },
    "license" : "MIT"
}

and adding RUN npm install to the Dockerfile:

FROM node:latest

RUN mkdir -p /usr/src/app

WORKDIR /usr/src/app

COPY package.json /usr/src/app/

COPY app.js /usr/src/app/

RUN npm install

EXPOSE 8888

Now you can use the following sequence of commands:

> docker build -t npmdepsdfro .

> docker run --name npmdepsdfro --expose 8888 -w /usr/src/app --net=host -d --read-only npmdepsdfro npm start

Now, the container's file system is read only and cannot be modified, ensuring the source code's file integrity of your running application.

You can test this out with the following commands:

> docker exec -it npmdepsdfro bash

> touch test.txt
> touch: cannot touch 'test.txt': Read-only file system

Conclusion

The techniques described in steps 4 and 5 are suitable for development and testing your node application. In production, you may wish to use a dockerfile. This allows you to create an image for your container and make the file system of your running application container read-only. However, for greatest flexibility, the ability to use the pre and post scripts in the package.json gives you very fine-grained control over the lifecycle events of your node application.