NodeJS on Windows NanoServer using Docker

Posted in software by Christopher R. Wirz on Tue Sep 11 2018



Docker containers are a consistent way of setting up a runtime environment for execution of a headless application. This offers an improvement in Devops and deployment. In actuality, is always a balance between the size of the resulting image and the capabilities of the image. Using a multi-stage build, however, you can take certain benefits of a larger image and use select features on the image which ultimately gets deployed.

Note: When distributing a Dockerfile, it is good practice (from my experience) to include third party install files locally. This will allow the image to be built on an air-gapped networks. Also, third party hosted URLs are sometimes removed to reduce their costs.

Let's assume you have a simple NodeJS app that has a package.js file


{
  "name": "node-nano",
  "version": "0.0.1",
  "description": "Running node.js in a Windows NanoServer docker container",
  "main": "index.js",
  "scripts": {
    "start": "node index.js",
    "test": "echo \"Error: no test specified\" & exit 1"
  },
  "engines": {
    "node": "8.12.0"
  },
  "dependencies": {
    "express": "^4.16.3",
    "file": "^0.2.2",
    "http": "^0.0.0",
    "ip": "^1.1.5",
    "path": "^0.12.7"
  },
  "keywords": [
    "node",
    "docker",
    "express"
  ],
  "author": "Christopher Wirz",
  "license": "MIT"
}

and an index file


// index.js
var express = require('express');
var ip = require('ip');
var app = express();
app.set('port', (process.env.PORT || 8675));
app.use(express.static(__dirname + '/public'));
app.get('/', function(request, response) {
	response.send("Hello World! (from " + ip.address() + ":" + app.get('port') + ")");
});
app.get('/*', function(request, response) {
	var fullUrl = req.protocol + '://' + req.get('host') + req.originalUrl;
	response.send("Hello "+fullUrl+"! (from " + ip.address() + ":" + app.get('port') + ")");
});
app.listen(app.get('port'), function() {
	console.log("Node app is running at " + ip.address() +":" + app.get('port'));
});

The index.js and package.json files are in a directory called "app". In the "app" directory is a folder called "public", that can be used for static files, but not code. It would be nice to have this project run in a Docker container with Node, Node Package Manager (npm), and Git. (fortunately, npm is packaged with node these days) This will allows us to update the node modules if needed.

In the base directory (just above the app directory), is the Dockerfile. The following Dockerfile is an example of how to use the robust windowsservercore image to build capabilities for the light-weight nanoserver target image.


# Dockerfile
# specify the builder ("builder" can be any tag)
FROM microsoft/windowsservercore as builder
# set the environment information
ENV NPM_CONFIG_LOGLEVEL info
ENV NODE_VERSION 8.12.0
ENV GIT_VERSION 2.19.0

# create and set the working directory
RUN mkdir "C:/app"
WORKDIR "C:/app"

# install git and delete the install file
COPY "Git-2.19.0-64-bit.exe" git.exe
RUN git.exe /VERYSILENT /NORESTART /NOCANCEL /SP- /CLOSEAPPLICATIONS /RESTARTAPPLICATIONS /COMPONENTS="icons,ext\reg\shellhere,assoc,assoc_sh"
RUN del git.exe

# install node and delete the install file
COPY "node-v8.12.0-x64.msi" node.msi
### Optionally, the installer could be downloaded as follows (not recommended)
# Invoke-WebRequest $('https://nodejs.org/dist/v{0}/node-v{0}-x64.msi' -f $env:NODE_VERSION) -OutFile 'node.msi'
RUN msiexec.exe /q /i node.msi
RUN del node.msi

# here is where the app directory (including package.json and index.js files) is copied
COPY app "C:/app"
    
### Optionally, you can globally set the proxy server
# ENV http_proxy "http://proxy:port"
# ENV https_proxy "http://proxy:port"

# falling back to the npm http registry overcomes some issues with proxy servers
RUN npm config set strict-ssl false
RUN npm config set registry "http://registry.npmjs.org/"
# if the proxy is not globally specified, npm can accept the declaration directly
RUN npm config set proxy "http://proxy:port"
RUN npm config set https-proxy "http://proxy:port"
RUN npm install
# Optionally, npm can be used to install packages without package.json
# RUN npm install express
# RUN npm install ip

### Optionally, you can remove the proxy declaration
# ENV http_proxy ""
# ENV https_proxy ""

# now make the target image
FROM microsoft/nanoserver
COPY --from=builder ["C:/Program Files/nodejs", "C:/Program Files/nodejs"]
COPY --from=builder ["C:/Program Files/Git", "C:/Program Files/Git"]
COPY --from=builder ["C:/app", "C:/app"]

# Add the command and bin directories to the environment path
ENV PATH "C:\\Windows\\system32;C:\\Windows;C:\\Windows\\System32\\Wbem;C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\;C:\\Program Files\\nodejs;C:\\Users\\ContainerAdministrator\\AppData\\Roaming\\npm;C:\\Program Files\\Git\\cmd"
### Optionally, the path could have been set referencing the previous path
# ARG SETX=/M
# RUN setx %SETX% PATH "%PATH%;C:\\Program Files\\nodejs;C:\\Users\\ContainerAdministrator\\AppData\\Roaming\\npm;C:\\Program Files\\Git\\cmd"

### If the image is not referenced, node can be started in this image, otherwise comment this out
ENV PORT 8675
EXPOSE ${PORT}
CMD ["C:/Program Files/nodejs/node.exe", "C:/app/index.js"]

Clearly there are a lot of ways to create a nodejs Windows NanoServer image. But first, let's discuss the steps to built it...

To begin with, open Powershell and go to the directory with the "Dockerfile". Then issue the following build commands to the Docker daemon:


docker pull microsoft/windowsservercore
docker pull microsoft/nanoserver
docker build -t node-nano .

This will build the image. Now, you must run the image, mapping the exposed port (8675) to the host's HTTP port (80) (the name is "nsn" for this example):


docker run -dit -p 80:8675 --name nsn node-nano

Though the express webserver is running, you can interact with the container (such as command line node) and make modifications from the console


docker exec -ti nsn cmd

or powershell.


docker exec -ti nsn powershell

While not recommended, changes made to the image can be committed


docker commit -m "Commit message" nsn node-nano:version2

Such that in a new Dockerfile, the resulting image can be referenced:


FROM node-nano:version2

# copy the newer app source from the local app directory
COPY app "C:/app"

ENV PORT 8675
CMD ["node", "C:/app/index.js"]