Containers Basics

What is a Container?

Containers are lightweight packages of your application code together with dependencies such as specific versions of programming language runtimes and libraries required to run your software services. A container is a sandboxed process running on a host machine that is isolated from all other processes running on that host machine. That isolation leverages kernel namespaces and cgroups, features that have been in Linux for a long time. Docker makes these capabilities approachable and easy to use.

Containers are also an important part of IT security. By building security into the container pipeline and defending infrastructure, containers stay reliable, scalable, and trusted. You can also easily move the containerized application between public, private and hybrid cloud environments and data centers (or on-premises) with consistent behavior and functionality.

To summarize, a container:

  • Is a runnable instance of an image. You can create, start, stop, move, or delete a container using the Docker API or CLI.
  • Can be run on local machines, virtual machines, or deployed to the cloud.
  • Is portable (and can be run on any OS).
  • Is isolated from other containers and runs its own software, binaries, configurations, etc.

What is an image?

A running container uses an isolated filesystem. This isolated filesystem is provided by an image, and the image must contain everything needed to run an application – all dependencies, configurations, scripts, binaries, etc. The image also contains other configurations for the container, such as environment variables, a default command to run, and other metadata.

Let’s say we want to run 20 containers that share a bunch of files (shared libraries like libc, Ruby gems, a base operating system, etc.). It would be nice if I could load all those files into memory just once, instead of 20 times.

If I did this I could save disk space on my machine (by just storing the files once), but more importantly, I could save memory!

If I’m running 20 containers I don’t want to have 20 copies of all my shared libraries in memory. That’s why we invented dynamic linking!

If you’re running just 2-3 containers, maybe you don’t care about a little bit of copying. That’s for you to decide!

It turns out that the way Docker solves this is with “overlay filesystems” or “graphdrivers”. (why are they called graphdrivers? Maybe because different layers depend on each other like in a directed graph?) These let you stack filesystems – you start with a base filesystem (like Ubuntu 22.04) and then you can start adding more files on top of it one step at a time.

Reasons to use containers

packaging. Let’s imagine you want to run your application on a computer. Your application depends on many different libraries being installed. Installing stuff on computers so you can run your program on them really sucks. It’s easy to get wrong with different versioning and compatibility issues!

Containers are nice, because you can install all the stuff your program needs to run in the container inside the container. packaging is a huge deal and it is probably the thing that is most important thing about containers.

scheduling. If you use containers, you can treat computers all the same whether it’s running in private data centre or on public cloud! Then you can more easily pack your programs onto computers in a more reasonable way. Systems like Kubernetes do this automagically.

better developer environment. If you can make your application run in a container, then maybe you can also develop it on your laptop in the same container. Refer devcontainers.

security. You can use seccomp-bpf or something to restrict which system calls your program runs.

Getting Started

Most popular choice to run containers is docker but there are other alternatives as well:

  • Podman
    • Unlike docker Podman is a daemonless, open source, Linux native tool designed to make it easy to find, run, build, share and deploy applications using Open Containers Initiative (OCI) Containers and Container Images.
    • Can be used to run docker containers
  • LXC (Linux)
  • runC
    • It can run containers, but it doesn’t have overlayfs
  • containerd
  • There’s a live demo of how to run a container with 0 tools (no docker, no rkt, no runC) at this point in this video which is super super interesting.
LXCWindows Hyper-VPodmanrunCcontainerd
Solution typeAll-in-oneAll-in-oneContainer engineContainer runtimeInterface/daemon
ProsNo daemon. Better for traditional application design.Higher level of isolation and portability.More secure.No daemon. Familiar CLI commands.Standardized interoperable container runtime.Easier to manage container lifecycles.
ConsLimited portability.More technical implementation.Larger infrastructure footprint. Windows only.Container engine only.Container runtime only.Container interface only.
Open sourceYesNo, but compatible with open sourceYesYesYes
Pros & Cons

In this article, we are going to use Docker. However, there are few cons with docker due to which people started using other alternatives:

  • Docker’s architecture is fundamentally flawed.
    • At the heart of Docker is a daemon process that is the starting point of everything Docker does. The docker executable is merely a REST client that requests the Docker daemon to do its work. Critics of Docker say this is not very Linux-like.
    • Where it starts hurting is if you use an init system like systemd. Since systemd was not designed for Docker specifically, when you try to start a Docker process, you actually start a Docker client process that in turn requests the Docker daemon to start the actual Docker container. There is a risk that the Docker client fails while the actual Docker container keeps running. In such a situation, systemd concludes that the process has stopped and restarts the Docker client — in turn possibly creating a second container (yes, you can work around this but that is besides the point
  • Docker announced its inclusion of Swarm into Docker Engine 1.12. So eventhough we don’t want to use it we are bound to run the whole engine.

Installation

Before following rest of the article it’s essential to install docker. Docker can be installed by referring to below link:

https://docs.docker.com/engine/install

You can test your installation by running a simple hello world container as below:

docker run --rm hello-world

This simple container prints “Hello from Docker!” and exits. We can run a container which keeps running a server.

docker run -d -p 8080:80 docker/getting-started

-d option is used to run container in the backgroud in daemon mode.
-p is used to map host port to container port.

To access this http service point your browser to http://localhost:8080

docker/getting-started is a docker image hosted on docker hub. When we run above command docker client on our local system downloads the image from docker hub locally and then spawns the container.

To understand what’s happening inside this container we can go to docker hub and refer the dockerfile for this image. For example docker file for above container is:

https://github.com/docker/getting-started/blob/master/Dockerfile

Building your own image

Clone demo applications from below github url.

https://github.com/vivekprm/docker-demo-apps

Change directory to the app you want to run. e.g. node-app in this case.

cd ./node-app

Build the image using below docker build command

docker build -t node-app .
-t flag tags your image
node-app is the image name

Starting the container using docker run command:

docker run -dp 3030:3000 node-app

-d flag (short for --detach) runs the container in the background.
-p flag (short for --publish) creates a port mapping between the host and the container. The -p flag takes a string value in the format of HOST:CONTAINER, where HOST is the address on the host, and CONTAINER is the port on the container.

Point your browser to http://localhost:3030 to see your app running.

Understand Dockerfile

Docker builds images automatically by reading the instructions from a Dockerfile which is a text file that contains all commands, in order, needed to build a given image. A Dockerfile adheres to a specific format and set of instructions which you can find at Dockerfile reference.

A Docker image consists of read-only layers each of which represents a Dockerfile instruction. The layers are stacked and each one is a delta of the changes from the previous layer. So the less number of instructions lesser the image size.

Now let’s understand the content of Dockerfile for node-app:

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

In the example above, each instruction creates one layer:

  • FROM creates a layer from the node:18-alpine Docker image.
  • WORKDIR Changes working directory.
  • COPY Copy files and directories. In this case everything in current directory.
  • RUN Executes build commands.
  • CMD specifies what command to run within the container.
  • EXPOSE Describe which ports your application is listening on.

Please refer Dockerfile reference to understand different instructions and their use.

References

Series Navigation<< Introduction to Docker

Leave a Reply