Building and Using Utility Containers

I first heard the phrase, "utility containers" from Maximilian Schwarzmuller from Academind, via his excellent course on Udemy, Docker and Kubernetes: The Practical Guide.  But, the goal of building "utility containers" has been something I have worked to  integrate into my software development workflow for a couple of years.  The promise of the perfect containerized environment, to eliminate interactions between software installed on the bare metal hosts (e.g. past frustrations of python 2 and python 3) has been alluring.

What Max did for me was introduce the idea of a "utility container".  I then considered using them to be the primary tool to bootstrap a project such as "npm init" or "ng new project".

I then realized that this is very similar to what the AWS CLI team had produced with their Dockerized AWS CLI tool.  Their simple idea of using an Alias with their dockerized AWS CLI tool solidified what I had wanted to achieve all along, which was to be able to simply type: "npm init" from the command line and have a dockerized Node to run.

Before the Dockerized version of AWS CLI tool was available, installing the AWS CLI was a lengthy process of ensuring we met the pre-requisites, using "curl" to download the code, unzipping, running an install script which also installed Python because it is dependent on it.  Similarly, in order to update to a newer version, we had to uninstall, unlink software, etc, etc.  

Using Docker containers is so much cleaner to just about everything!

Prerequisites:

  • Docker 19.03.13 (or newer) installed

"Installing" The Software:

  • Consider, this new process to install an Angular development environment, e.g. Angular utility container.
FROM node:14.15.0

RUN npm install -g @angular/cli@10.2.0

USER node
WORKDIR /app
ENV PATH /app/node_modules/.bin:$PATH
Dockerfile
$ build docker -t angular-util:10.2.0 . # don't forget the dot (.)
Build the Docker image
$ alias ng="docker run -it --rm -v ~/.gitconfig:/home/node/.gitconfig \
  -v $(pwd):/app angular-util:10.2.0 ng"
$ alias ts-node="docker run -it --rm -v $(pwd):/app  \
  angular-util:10.2.0 ts-node"
$ alias node="docker run -it --rm -v $(pwd):/app \
  angular-util:10.2.0 node"
Create a couple of aliases (put these in your shell startup script)

Usage:

Create ("initialize", "bootstrap") a cool new Angular Project, without having Node and Angular installed on your bare metal system:

$ ng new <proejct name>
...
$ cd <project name>

Other examples:

$ node --version
v14.15.0
$ npm --version
6.14.8
$ yarn --version
1.22.5

Initialize a NodeJS backend:

$ mkdir backend
$ npm init
This utility will walk you through creating a package.json file.
It only covers the most common items, and tries to guess sensible defaults.

See `npm help init` for definitive documentation on these fields
and exactly what they do.

Use `npm install <pkg>` afterwards to install a package and
save it as a dependency in the package.json file.

Press ^C at any time to quit.
package name: (app) backend
version: (1.0.0) 
description: Testing docker
entry point: (index.js) 
test command: 
git repository: 
keywords: 
author: 
license: (ISC) 
About to write to /app/package.json:

{
  "name": "backend",
  "version": "1.0.0",
  "description": "Testing docker",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}


Is this OK? (yes)

$ ll
total 12
drwxr-xr-x 2 scott scott 4096 Nov  5 08:18 ./
drwxr-xr-x 3 scott scott 4096 Nov  5 08:18 ../
-rw-r--r-- 1 scott scott  217 Nov  5 08:17 package.json

Keep in mind that the intent of the use of this "Utility Container" is merely to Bootstrap the application.  It is not enough to containerize the application itself.  That should be done in a Dockerfile or Docker Compose yml file that resides within the application itself.  This separates the functions of the containers.

Benefits:

Notice that from this "installation", we get the following software (all containerized into a nice, neat little package that's not polluting our bare metal system):

  • Node 14.15.0
  • NPM (packaged with Node)
  • Yarn (packaged with Node)
  • Angular CLI 10.2.0 development environment
  • TS-NODE (packaged with Angular CLI)
  • (many others...)

Also note the other advantages:

  • It's possible to quickly change to another Node or Angular version by simply changing out the :tag that's used
  • Changing the version does not mean that an entirely new Node or Angular image needs to be downloaded because of magic of Docker image layers which are provided via a Union File System.  Only the differences between the versions of Node need to be downloaded, not an entirely new image.

"Using Union filesystems is super cool because they merge all the files for each image layer together and presents them as one single read-only directory at the union mount point. If there are duplicate files in different layers, the file on the higher level layer is what is displayed."

  • As a best practice, it's best to take advantage of the Union File System capabilities (e.g. docker layers) if you can favor one base image for multiple environments.  For example, using Node:14.15.0 for both the Node and Angular environments rather than mixing base images like Node:14.15.0 for node (backend api development) and Node:12.0.0 for Angular.  Docker will still union what it can together to be efficient, but we can help it if we try to stick to something consistent.

Troubleshooting:

If you're on a Linux / Unix environment and you run into file ownership issues when you do things like "npm init" or "ng new <project>", here are some steps you can use to overcome them:

The problem is most likely related to your Linux User not having User and Group Ids of 1000:1000.  The Node team had to assume something when they built the image, so, that's what they went with.  But, your user is likely not 1000:1000 if you are one of several people logged into a Linux server, rather than being the one and only user on a personal Linux system.

The fix is pretty simple; just a couple of modifications to the Dockerfile and the Docker build command - everything else stays the same.

FROM node:14.15.0

RUN userdel -r node

ARG USER_ID

ARG GROUP_ID

RUN addgroup --gid $GROUP_ID user

RUN adduser --disabled-password --gecos '' --uid $USER_ID --gid $GROUP_ID user

USER user

WORKDIR /app
Dockerfile modifications
$ docker build -t node-util:cliuser --build-arg USER_ID=$(id -u) --build-arg GROUP_ID=$(id -g) .