Containers - JessicaOPRD/docs GitHub Wiki

Benefits

  • Bundle together application, configuration, possibly data
  • Do not need to install application tooling manually
  • Do not need to worry about version conflicts between application stacks (which version of a language is installed locally, whether there's a native way to manage language version, etc) — although applications generated with your desktop version of a tool may need to be coordinated with container choices/targets (Node.js version for example)
  • More consistent environment and portability between machines
  • Sandboxing (no knowledge of other applications) without one VM per application
  • Can use official images to help manage patching
  • Popular support — although Windows is still catching up on a good developer experience (see Windows 11 notes)

Gotchas

  • Steep learning curve — the primary image repositories can be difficult to search as a beginner, it's not always clear how images should be mixed ("Do I need the language image or ...?"), and there are some big differences between the Linux and Windows container ecosystems, with Windows having less widespread support overall
  • Connection to host machine persists — containers are not as clean as VMs in this way, and this can be confusing in setup, particularly on Windows where the host version needs to be coordinated with image selection, and Windows versioning/tag names are not intuitive unless you are already very familiar with Windows versioning
  • Any CLI code generators are likely tied to the host machine's tool versions, unless built inside a container and copied to the host — this means it is possible to generate code on a version different from the container, which can lead to compatibility problems and unexpected issues if the developer's version does not match

Important note about app generation/scaffolding

This topic took me awhile to wrap my head around. While a container image ultimately instructs an application for deployment to any environment that supports a container infrastructure, it is often NOT used to generate the original app in the first place! This step typically happens on the original developer's local machine, using their installed scaffolding tools (language, CLI, etc). I believe it is easy to overlook this when someone is learning from public repository sample images. It is therefore important that a developer think carefully about their own language and CLI versions when generating an app that they intended to containerize. They will want to audit their machine's stack, or defer to a dedicated VM if one exists.

For instance, I once scaffolded a Vue/Nuxt app on an older version of Node. When I attempted to containerize it, I pulled in the latest Node image. The app would not build in these conditions and I needed to check my system version of Node. This was not what I intended, as I would have wanted a new app to use a recent Node image.

What this ultimately means is that while it is possible for receiving developers to spin up an already containerized app without installing separate tools, as Developer Zero scaffolding falls to you. This is a unique responsibility.

There is a way around this — you can scaffold the application inside a container and copy it to the host machine. By doing so, you never need to install a specific version of the target stack on your machine, and become a regular developer just like everyone else. However, I have felt this may be "going against the grain." It would also make continued use of CLI generators (say to create a new component) more cumbersome than the design of such generators intend, although all devs who work with the container and don't install CLI scaffolding tools will experience the same problem.

Using interactive/terminal mode to generate scaffolding, etc.

Run from base image directly

This works well for testing what installs might look like and verifying behavior in "clean" environments. The basic idea is to run your latest pull of Node, do installs via the command line, and copy files to the host. This works, but I find copying the files a little messy. I think it is easier to do minimal pre-scaffolding (see next header).

Run latest Node image in interactive mode, with terminal, and in this mode check the tooling versions:

docker run -i -t node bash

node --version

npm --version

Press ctrl + d to exit.

Note that in this case node refers to the latest pull of the official image via docker pull node:latest. I have greatly confused my system with multiple Node installs however. I believe problems can occur if you pull in administrator mode? To fix the problem, I removed the node image with docker rmi node from administrator mode. It did not appear in Docker Desktop and I believe this signaled a user ownership issue.

You can also install and try out a package through this method. To copy to the host machine, use the docker cp command from the host in administrator mode.

Light pre-scaffolding with mount volume

A good example of a "light" scaffold is setting up the basic structure outlined by Bootstrap. But you could also simply start with an empty folder — it will all depend on the project.

Then created a simple Dockerfile with a pinned version of Node:

FROM node:20.5.0
RUN mkdir -p /app
WORKDIR /app

COPY . .

Next build the image:

docker build -t storybook-test .

Next run the image with an imperatively declared mount volume (${PWD} is a placeholder for the current terminal location on the host):

docker run -v ${PWD}:/app -it storybook-test bash

Now when you run npm install --save-dev, package.json will be created and updated on the host as well. Note this will also created node_modules, even with a .dockerignore file.

You can probably do all commands in this step in a single command line, but eventually you'll probably want a Dockerfile anyway.

Searching artifact repositories

I might have come in at a difficult time, but learning how to search for images in repositories has been rather unpleasant and confusing. Some general tips:

Look for officially published images only

These will be specially marked, unless you are are Microsoft on Docker Hub. In the case of Microsoft images, use Microsoft Artifact Registry instead of Docker Hub.

Make use of manifest lists

Searching container image tags for the image that matches your exact host architecture and OS version can be exhausting. When a manifest is pulled, Docker will attempt to find the most appropriate image based on the host system and platform. Many people probably use manifests without realizing it.

docker pull mcr.microsoft.com/windows/nanoserver:ltsc2022
ltsc2022: Pulling from windows/nanoserver
no matching manifest for windows/amd64 10.0.xxxxx in the manifest list entries

Microsoft Artifact Registry's browser version provides a "Type" of "Manifest List." Manifest lists are typically generically named and will not include details pertaining to the host OS architecture or version in the name.

To inspect a manifest:

docker manifest inspect mcr.microsoft.com/windows/nanoserver:ltsc2022

Things to know when choosing a container engine

Container engines (or runtimes) run atop the host architecture (infrastructure/hardware) and operating system. They make possible OS-level virtualization. Note that Docker Engine runs on Linux only. I'm unsure how Docker Windows containers run in production.

OS Docker Desktop runs on Docker Engine runs on
Windows 🟢 Windows containers — native? 🟢 Linux containers — Microsoft WSL2 Linux distro 🟡 Need to install/manage own Linux VM — can you configure to point to Linux subsystem?
Linux 🟢 Native? 🟢 Native?
Mac 🟡 Included/packaged Linux VM 🟡 Need to install/manage own Linux VM

Difference between running directly on the OS and running in a container

A computer program running on an ordinary operating system can see all resources (connected devices, files and folders, network shares, CPU power, quantifiable hardware capabilities) of that computer. However, programs running inside of a container can only see the container's contents and devices assigned to the container.

Source

Things to know when choosing an official image

Containers are very host-dependent (the host OS and container share the kernel). They are even more host-dependent than VM images, which only need to know underlying architecture.

🔴 = Decision-making limited to host, you will need to find a matching image (you can use a Linux VM, but this is not ideal on a production box)

🟢 = Can virtualize and switch between images if other host dependencies are met

Consideration Such as Windows container Linux container
Architecture of host machine 32- vs. 64-bit variants* 🔴 Host dependent 🔴 Host dependent
Operating System of host machine 🔴 Host dependent 🔴 Host dependent
Operating System Version of host machine 20H2, 21H2 🔴 Host dependent — with Hyper-V (not Home Edition) ✖️ Does not matter if virtualized? Kernel version?
Distro Version ✖️ No distributions 🟢 Image dependent — Alpine, Debian versions (Stretch, Buster, Bullseye, etc)
Software Version Node.js, Python, etc** 🟢 Image dependent — Many versions 🟢 Image dependent — Many versions
Registry Microsoft Artifact Registry Docker Hub

*Docker supports a specific list

**Note that some languages run best on Linux — even Microsoft discourages running Node.js on Windows and encourages users to use the Linux subsystem for development and actual Linux for production

What containers can do

🟢 Use the underlying host kernel to run many apps

🟢 In case of Linux OS, use different distributions

🟢 Mostly sandbox apps from language versions and peer services/networks that might otherwise collide if developing multiple apps in the same raw OS

🟡 Lightweight security sandbox solution — Microsoft describes the isolation from the host as "lightweight" and recommends a VM where a strong security boundary is necessary, or invoking Hyper-V isolation mode (which runs the container in a VM) (source)

What containers can't do

🔴 Use an OS different from the host OS (unless VM installed)

🔴 Completely standardize a dev and production environment where key host machine differences exist

Windows Host

Note that for Windows you will need Hyper-V to run Docker. Hyper-V is not included with the Home Edition.

You will be limited by your host version of Windows in a way that I do not typically experience when working off the Linux Subsystem. For example, if you attempt to use a base image that does not match:

7.0-nanoserver-ltsc2022: Pulling from dotnet/sdk
a Windows version 10.0.20348-based image is incompatible with a 10.0.xxxxx host

This was very confusing to me after playing with many Linux images. This is because the host and container share Windows resources. More about this here. The user experience here will likely improve with Windows 11 based on that article.

I have not discovered a crystal clear way to find matching images. Some tips from tinkering with the Microsoft Artifact Registry:

  • When searching tags, use a starting hyphen if no matches are found, such as -nanoserver.
  • If a mismatch is indicated, try the next oldest image.
  • If you find a matching "Manifest List" without a specific version, this indicates many variations for specific platforms and architectures. This is useful in cases where multi-architecture environments are expected for a single image. When the manifest is pulled, Docker will attempt to find the most appropriate image based on the host system and platform. Not all tags are guaranteed a manifest, but they are a "fluid" way to work.

Windows Server

❓ How to account for Windows Server, Windows Server Core, Windows Nano?

The following flavors of Windows Server are available:

Name Approximate Size Description
Windows Server ? ⚪ Full-featured ⚪ Includes GUI ⚪ Supports virtualization
Windows Server Core 7.0-windowsservercore-ltsc2022 = 46.8 MB ⚪ Designed for use by command line or PowerShell ⚪ Often used for running containerized apps ⚪ Lighter weight than Windows Server ⚪ No GUI
Windows Nano Server aspnet:7.0-nanoserver-ltsc2022 = 45.2 MB ⚪ Design for containerization ⚪ Lighter weight than Windows Server Core ⚪ Optimized for microservices ⚪ No GUI or 32-bit support ⚪ Supports runtimes and development frameworks for .NET Core, Java, and Node.js

Conceptual Checks

Check OS version from container terminal

  1. Open terminal via Docker Desktop
  2. Type uname -a

Layer Caching

You can decrease the build time by practicing layer caching. When an image is changed, a direct copy of all local files will include dependencies such as those in node_modules. You can decrease the copy and install time (I think?) by copying the dependency manifest files instead.

Recreates dependencies every time

FROM node:12-alpine
WORKDIR /app
COPY . .
RUN yarn install --production
CMD ["node", "src/index.js"]

Caches dependencies

FROM node:12-alpine
WORKDIR /app
COPY package.json yarn.lock ./
RUN yarn install --production
COPY . .
CMD ["node", "src/index.js"]

Docker CLI

List of commands for running Docker imperatively. These commands will run relative to a Dockerfile.

Action Syntax Description
Build docker build -t <tag-name> . Build image from current app state — notice the dot!
Run docker run -dp <host-port>:<container-port> <tag-name> Start an app container from an already built image ⚪ -d = detached (gives terminal access back) ⚪ -p = port (or publish?) ⚪ -i = interactive
List images docker image ls
List running containers docker ps

Dockerfile Instructions

Command Syntax Description
FROM FROM node:18.14.2-alpine3.17 Parent or base image to use for your application — usually you will target your application's server-side language version
RUN RUN npm install Execute arbitrary commands inside the container, such as install and/or a bash command
COPY COPY package*.json ./ Copy local files and directories into the image
ADD Add can copy remote URLs into the image
CMD CMD [ "npm", "start" ] Similar to a constructor or main function — there can only be one and it serves as the default command when a container runs

Docker Compose

List of commands for running multiple containers via Docker Compose. These commands will run relative to docker-compose.yml and one or more Dockerfiles, depending on the configuration.

Action Syntax Description
Start docker-compose up -d Start up from root docker-compose.yml
Stop docker-compose down Tear down running containers (will not include volumes)
Stop including volumes docker-compose down --volumes Tear down running containers and associated(?) volumes
Build from Dockerfile docker-compose build Build any targeted Dockerfile images specified in the docker-compose.yml file

Tutorials

⚠️ **GitHub.com Fallback** ⚠️