Containers are a type of software virtualization. Using a directory structure that contains an entire operating system (typically referred to as a chroot), containers can easily spin up and utilize system resources without the overhead of full hardware allocation. It is not possible to use separate kernels with this approach.


Docker Hub is a container registry that provides a central location to find, download, and upload container images. Here is a list of common operating system images for each family of distributions:

  • Arch Linux

    • archlinux

  • Fedora

    • centos:8

    • fedora:31

  • Debian

    • debian:10

    • ubuntu:20.04

  • openSUSE

    • opensuse/leap:15.2

    • opensuse/tumbleweed

More containers can be found here.

Bootstrap an Operating System

Using a package manager and the main operating system repositories, it is possible to bootstrap an operating system. It installs all of the operating system packages into a directory. It can then be used as a chroot or for a container image. This can be done on different operating systems but the relevant package manager has to be installed. Arch Linux is one of the few distributions that ships all of the most popular package managers.

Arch Linux

$ mkdir -p archlinux_bootstrap/var/lib/pacman
$ cd archlinux_bootstrap
$ sudo pacman -r . -Syy
$ sudo pacman -r . -S base

If not using Arch Linux with pacman installed, download the archlinux-bootstrap-<DATE>-x86_64.tar.gz tarball from one of the HTTP Direct Downloads mirror.

CentOS 8

$ sudo cat <<EOF > /etc/yum/repos.d/centos8.repo
$ mkdir ${HOME}/centos8_bootstrap
$ sudo yum install centos-release dnf @base --installroot=${HOME}/centos8_bootstrap

Debian 10

$ mkdir debian10_bootstrap
$ sudo debootstrap --arch amd64 buster ./debian10_bootstrap/

Fedora 31

$ mkdir ${HOME}/fedora31_bootstrap
$ sudo dnf install --installroot=${HOME}/fedora31_bootstrap --releasever=31 --nogpgcheck fedora-release
$ sudo dnf groupinstall --installroot=${HOME}/fedora31_bootstrap --releasever=31 --nogpgcheck minimal-environment


$ sudo mount rhel-8.0-x86_64-dvd.iso /mnt
$ sudo cat <<EOF > /etc/yum/repos.d/rhel8.repo
$ sudo yum clean all
$ mkdir ${HOME}/rhel8_bootstrap
$ sudo yum groupinstall base --installroot=${HOME}/rhel8_bootstrap

Ubuntu 20.04

$ mkdir ubuntu2004_bootstrap
$ sudo debootstrap --no-check-gpg --arch amd64 focal ./ubuntu2004_bootstrap/



A container registry stores Open Container Initiative (OCI) formatted images. These can universally be used across any modern cloud-native platform.

Here are a list of different container registiries that exist [22]:

  • Amazon Elastic Container Registry (ECR)

  • Docker Hub

  • Docker Trusted Registry (DTR)

  • Harbor

  • JFrog Artifactory

  • Nexus Repository

  • Pulp Container Registry

  • Quay

By default, the docker command manages container images on the Docker Hub registry.

$ docker login

Other registries can also be used by specifying the fully qualified domain name of the registry.

$ docker login <REGISTRY>


  • = Red Hat customer.

  • = Red Hat Quay.

It may be required to first create a new image with a name of the alternative registry.




The docker daemon strictly enforces verified certificates. If a certificate for a container registry cannot be validated, then the docker client will refuse to connect to it. These are workarounds for connecting to registries with untrusted and/or broken certificates.

Add a Certificate Authority

Create a directory in /etc/docker/certs.d/ or ~/.docker/certs.d/ named <REGISTRY_DOMAIN_OR_IP>:<REGISTRY_PORT>. Place the certificate authority certificate and public key there. Normally a “ca.crt” file would contain both of those but may also be provided separately as “ca.cert” and “ca.key” files. On Linux, a restart of the docker daemon is not required. [23]

On macOS, local certificates will be synced to from ~/.docker/certs.d/ to /etc/docker/certs.d/ in the back-end virtual machine after restarting the Docker Desktop app. [24]

$ osascript -e 'quit app Docker'
$ open -a Docker

Ignore Certificates

If a certificate has a common name of something other than the domain or IP address of the container registry then it will not work. In this case, the certificate should be ignored entirely by being listed as an insecure registry. This can also be used as an alternative to providing a certificate authority.

Edit the docker daemon configuration file and add a list of registries to ignore invalid or self-signed certificates.

  • Linux: /etc/docker/daemon.json

  • macOS: ~/.docker/daemon.json or navigate to Docker Desktop > Preferences > Docker Engine.

  "insecure-registries": [

Restart the docker daemon:

  • Linux:

    $ sudo systemctl restart docker
  • macOS:

    $ osascript -e 'quit app Docker'
    $ open -a Docker

Container Runtimes

Container runtimes handle launching, stopping, and removing containers. Typically a container runtime will be used as a library for implementing a CRI and optionally a Container Engine on-top of the CRI. End-users do not need to interact directly with a container runtime. [13]

An OCI compliant container runtime reads metadata about a container from a config.json file. This describes everything about the container. It will then handle overlay mounts, creating cgroups for process isolation, configuring AppArmor or SELinux, and starting the container process. [20]

runC and crun

runC was originally developed by Docker as one of the first modern container runtimes and is written in Go. crun is developed by Red Hat as a re-implementation of runC in the C programming language. It is twice as fast as runC. [14] Legacy container runtimes that are no longer maintained include railcar and rkt. Both runC and crun follow the Open Container Initiative (OCI) for providing a standardized container runtime. [13]

Container Runtime Interfaces (CRIs)

CRIs are wrappers around container runtimes that provide a standard API for Kubernetes and other container management platforms to interact with. [13]

containerd (docker)

containerd is a cross-platform (Linux and Windows) CRI built on-top of runC. It is what the Docker Engine uses in the back-end. [15]


Supported operating systems:

  • CentOS/RHEL >= 7

  • Debian >= 9

  • Ubuntu >= 16.04

  • Windows

Debian and Ubuntu:

  • Install the required dependencies:

    $ sudo apt-get update
    $ sudo apt-get install apt-transport-https ca-certificates curl gnupg2 software-properties-common
  • Add the repository and its GPG key.

    $ sudo add-apt-repository "deb [arch=amd64]$(lsb_release -is | awk '{print tolower($0)}') $(lsb_release -cs) stable"
    $ curl -fsSL$(lsb_release -is | awk '{print tolower($0)}')/gpg | sudo apt-key --keyring /etc/apt/trusted.gpg.d/docker.gpg add -
  • Install containerd.

    $ sudo apt-get update
    $ sudo apt-get install
  • Pick to either use containerd by itself or the Docker Engine.

    • containerd:

      • Create default configuration file and restart containerd to reload the new configuration file.

        $ sudo mkdir -p /etc/containerd
        $ containerd config default | sudo tee /etc/containerd/config.toml
        $ sudo systemctl restart containerd
    • Docker Engine:

      • Install the Docker Engine.

        $ sudo apt-get install docker-ce docker-ce-cli
      • Configure it.

        $ cat <<EOF | sudo tee /etc/docker/daemon.json
          "exec-opts": ["native.cgroupdriver=systemd"],
          "log-driver": "json-file",
          "log-opts": {
            "max-size": "100m"
          "storage-driver": "overlay2"
        $ sudo mkdir -p /etc/systemd/system/docker.service.d
        $ sudo systemctl daemon-reload
      • Restart it to load the new configuration. Also ensure it will start on boot.

        $ sudo systemctl restart docker
        $ sudo systemctl enable docker



Use crictl to manage containers that are running using the containerd or docker daemon (default). The command uses the same arguments as the docker CLI tool except it also has the ability to view Kubernetes pods via crictl pods.

There are three main ways to define which daemon to interact with. Use one of the three.

  1. Use the /etc/crictl.yaml configuration file.

  • containerd:

    runtime-endpoint: unix:///var/run/containerd.sock
    image-endpoint: unix:///var/run/containerd.sock
    timeout: 5
    debug: false
  • docker:

    runtime-endpoint: unix:///var/run/dockershim.sock
    image-endpoint: unix:///var/run/dockershim.sock
    timeout: 5
    debug: false
  1. Use CLI arguments.

  • containerd: $ sudo crictl --runtime-endpoint=/var/run/containerd/containerd.sock --image-endpoint=/var/run/containerd/containerd.sock

  • docker: $ sudo crictl --runtime-endpoint=/var/run/dockershim.sock --image-endpoint=/var/run/dockershim.sock

  1. Use environment variables.

  • containerd:

    $ export CONTAINER_RUNTIME_ENDPOINT="/var/run/containerd/containerd.sock"
    $ sudo -E crictl
  • docker:

    $ export CONTAINER_RUNTIME_ENDPOINT="/var/run/containerd/containerd.sock"
    $ sudo -E crictl



CRI-O is a lightweight CRI created by Red Hat and is specifically for Kubernetes only. It supports both runC (cgroups v1) and crun (cgroups v2). [17] In OpenShift 4, CRI-O is the default CRI. [18]


Supported operating systems:

  • CentOS >= 7

  • Debian Testing or Unstable (currently Debian 11)

  • Fedora

  • openSUSE Tumbleweed

  • Ubuntu >= 18.04

Debian and Ubuntu:

  • Install the required dependencies:

    $ sudo apt-get update
    $ sudo apt-get install apt-transport-https ca-certificates curl gnupg2 software-properties-common
  • Add the CRI-O repository and its GPG key.

    $ export OS="xUbuntu_20.04" # Or use "Debian_Testing" for Debian.
    $ cat <<EOF | sudo -E tee /etc/apt/sources.list.d/devel:kubic:libcontainers:stable.list
    deb$OS/ /
    $ cat <<EOF | sudo -E tee /etc/apt/sources.list.d/devel:kubic:libcontainers:stable:cri-o:$VERSION.list
    deb$VERSION/$OS/ /
    $ curl -L$OS/Release.key | sudo apt-key --keyring /etc/apt/trusted.gpg.d/libcontainers.gpg add -
    $ curl -L$VERSION/$OS/Release.key | sudo apt-key --keyring /etc/apt/trusted.gpg.d/libcontainers-cri-o.gpg add -
  • Install CRI-O and start the service.

    $ sudo apt-get update
    $ sudo apt-get install cri-o cri-o-runc
    $ sudo systemctl daemon-reload
    $ sudo systemctl start crio


Container Engines

A Container Engine provides a set of tools for end-users to interact with and manage containers. [13]

Docker Engine

The Docker Engine provides a single binary docker that can build and run containers as well as manage image repositories. It uses the CRI containerd which uses the container runtime runC. Legacy versions of the Docker Engine relied on the LXC kernel module.

A command is ran to start a daemon in the container. As long as that process is still running in the foreground, the container will remain active. Some processes may spawn in the background. A workaround for this is to append && tail -f /dev/null to the command. If the daemon successfully starts, then a never-ending task can be run instead (such as viewing the never ending file of /dev/null). [1]

By default, only the “root” user has access to manage docker containers. Users assigned to a “docker” group will have the necessary privileges. However, they will then have administrator access to the system. If the “docker” group is newly created then the daemon needs to be restarted for the change to load up. The docker user may also have to run the newgrp docker command to reload their groups. [2]

$ sudo groupadd docker
$ sudo usermod -a -G docker <USER>
$ sudo systemctl restart docker


docker containers are built by using a template called Dockerfile. This file contains a set of instructions on how to build and handle the container when it’s started.

Dockerfile Instructions

  • FROM <IMAGE>:<TAG> = The original container image to copy and use as a base for this new container.

  • ADD <SOURCE> <DESTINATION> = Similar in functionality to COPY. This should only be used to download URLs or extract archives.

  • CMD = The default command to run in the container, if ENTRYPOINT is not defined. If ENTRYPOINT is defined, then CMD will serve as default arguments to ENTRYPOINT that can be overridden from the docker CLI.

  • COPY <SOURCE> <DESTINATION> = Copy a file or directory to/from the container image. It is recommended to use this method instead of ADD for simple operations.

  • ENTRYPOINT = The default command to run in this container. Arguments from the docker CLI will be passed to this command and override the optional CMD arguments. Use if this container is supposed to be an executable.

  • ENV <VARIABLE>=<VALUE> = Create shell environment variables.

  • EXPOSE <PORT>/<PROTOCOL> = Connect to certain network ports.

  • FROM = The original image to create this container from.

  • LABEL = A no-operation string that helps to identify the image. One or more labels can be specified.

  • MAINTAINER (deprecated) = The name or e-mail address of the image maintainer.

    • Use LABEL maintainer=<EMAIL_ADDRESS> instead.

  • ONBUILD <INSTRUCTION> <ARGS> = Define instructions to only execute during the build process. This is specific to docker and by default does not apply to images being built with OCI tools such as Buildah.

  • RUN = A command that can be ran once in the container. Use the CMD <COMMAND> <ARG1> <ARG2> format to open a shell or CMD ['<COMMAND>', '<ARG1>', '<ARG2>'] to execute without a shell.

  • USER <UID>:<GID> = Configure a UID and/or GID to run the container as. After this instruction is defined, all CMD, ENTRYPOINT, and RUN commands use this specified user.

  • VOLUME <PATH> = A list of paths inside the container that can mount to an external persistent storagedevice (for example, for storing a database).

  • WORKDIR = The working directory where commands will be executed from.


OpenShift Instructions

Some instructions in the Dockerfile have special uses in regards to OpenShift.


    • io.openshift.tags = A comma-separated list of keywords that help categorize the usage of the image.

    • io.k8s.description = A detailed description of what the container image does.

    • io.openshift.expose-services = Syntax is <PORT>/<PROTOCOL>:<NAME>. A description of the ports defined via EXPOSE.

  • USER = This value is ignored on OpenShift as a random UID will be used instead.

Storage Space

Containers should be ephemeral where the persistent data is stored in an external location (volume) and/or a database. Almost every Dockerfile operation creates a writable/container layer ontop of the previous layer. Each layer created with ADD, COPY, and RUN takes up more space.

Lower space usage by [10]:

  • Using a small image such as alpine.

  • Combining all RUN commands into one statement. Chain them together with && to ensure that each command succeeds before moving onto the next one.

  • Cleaning package manager cache (if applicable).

    • Debian: RUN apt-get clean

    • Fedora: RUN dnf clean all

  • Using the docker image build –squash or buildah bud –squash command to consolidate all additional layers when creating a new image. Use docker-squash to consolidate an existing image.

A Dockerfile cannot ADD or COPY directories above where the docker build command is being run from. Only that directory and sub-directories can be used. Use docker build -f <PATH_TO_DOCKERFILE> to use a Dockerfile from a different directory and also use the current working directory for copying files from. [11]


Networking is automatically bridged to the public interface and set up with a NAT. This allows full communication to/from the container, provided that the necessary ports are open in the firewall and configured in the docker image.

Networking issues from within a container are commonly due to network packet size (MTU) issues. There are a few work-a-rounds.

  1. Configure the default MTU size for docker deployments by modifying the daemon’s process settings. This value should generally be below the default of 1500.

    $ sudo vim /etc/sysconfig/docker
    OPTIONS='--selinux-enabled --log-driver=journald --mtu 1400'
    $ sudo systemctl restart docker


    $ sudo vim /usr/lib/systemd/system/docker.service
    ExecStart=/usr/bin/docker-current daemon \
          --exec-opt native.cgroupdriver=systemd --mtu 1400 \
          $OPTIONS \
          $ADD_REGISTRY \
          $BLOCK_REGISTRY \
    $ sudo systemctl daemon-reload
    $ sudo systemctl restart docker
  2. Forward all packets between the docker link through the physical link.

    $ sudo iptables -I FORWARD -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu


In rare cases, the bridge networking will not be working properly. An error message similar to this may appear during creation.

ERROR: for <CONTAINER_NAME> failed to create endpoint <NETWORK_ENDPOINT> on network bridge: iptables failed: iptables --wait -t nat -A DOCKER -p tcp -d 0/0 --dport <DESTINATION_PORT_HOST> -j DNAT --to-destination <IP_ADDRESS>:<DESTINATION_PORT_CONTAINER> ! -i docker0: iptables: No chain/target/match by that name.

The solution is to delete the virtual “docker0” interface and then restart the docker service for it to be properly recreated.

$ sudo ip link delete docker0
$ sudo systemctl restart docker



Java <= 9, by default, will try to allocate a large amount of memory for the runtime and garbage collection. This can lead to resource exhaustion of RAM on a hypervisor. The maximum memory allocation should be specified to Java applications using -Xmx<SIZE_IN_MB>m. [7] This is no longer an issue in Java >= 10 as it is now aware of when it is being containerized. [8]

Example Java <=9 usage in a docker compose file that utilizes an environment variable:

CMD java -XX:+PrintFlagsFinal $JAVA_OPTS -jar app.jar

Container Tools (buildah, podman, and skopeo)

The Container Tools project bundles a set of fully-featured programs to replicate the functionality of the docker command using the OCI standard. [19] No daemon or CRI is used and instead the tools communicate directly with crun or runC. The podman codebase (previously known as libpod) is shared between the Container Tools and CRI-O projects. However, the two projects are not able to manage containers created from the other.

Container Tools:

  • buildah = Build container images.

  • podman = Run containers. Designed as a drop-in CLI replacement for docker. It has a focus on adding additional functional to replicate the Pod API from Kubernetes. Containers will run as a non-privileged user by default.

  • skopeo = Manage container image registries.


Linux Containers (LXC) utilizes the Linux kernel to natively run containers.

Debian install [5]:

$ sudo apt-get install lxc

RHEL install [6] requires the Extra Packages for Enterprise Linux (EPEL) repository:

  • RHEL:

    $ sudo yum install epel-release
    $ sudo yum install lxc lxc-templates libvirt

On RHEL family systems the lxcbr0 interface is not created or used. Alternatively, the libvirt interface virbr0 should be used.

$ sudo vim /etc/lxc/default.conf = virbr0

The required services need to be started before LXC containers will be able to run.

$ sudo systemctl start libvirtd
$ sudo systemctl start lxc

Templates that can be referenced for LXC container creation can be found in the /usr/share/lxc/templates/ directory.



Error when pulling a container image from a Harbor container registry proxy-cache project:

Using default tag: latest
Error response from daemon: unknown: artifact docker-hub-proxy-cache/mysql/mysql-router@sha256:66d5955bbf926b9ab35df6e199aa434c89c96a2b8c5a47531cf011d67b4b37f0 not found


  • View the harbor-core logs. The repository may be temporarily blocked by Docker Hub API rate limiting. Wait at least two hours before trying to pull the image again.

    2021-04-22T06:17:47Z [WARNING] [/server/middleware/repoproxy/proxy.go:139]: Artifact: <HARBOR_PROJECT_NAME>/<DOCKER_HUB_PROJECT_NAME>/<DOCKER_HUB_CONTAINER_NAME>:, digest:sha256:66d5955bbf926b9ab35df6e199aa434c89c96a2b8c5a47531cf011d67b4b37f0 is not found in proxy cache, fetch it from remote repo
    2021-04-22T06:17:47Z [DEBUG] [/server/middleware/repoproxy/proxy.go:141]: the tag is , digest is sha256:66d5955bbf926b9ab35df6e199aa434c89c96a2b8c5a47531cf011d67b4b37f0
    2021-04-22T06:17:47Z [WARNING] [/server/middleware/repoproxy/proxy.go:151]: Proxy to remote failed, fallback to local repo, error: http status code: 429, body: {
      "errors": [
          "code": "TOOMANYREQUESTS",
          "message": "You have reached your pull rate limit. You may increase the limit by authenticating and upgrading:"


  1. “Get started with Docker.” Docker. Accessed November 19, 2016.

  2. “Getting started with Docker.” Fedora Developer Portal. Accessed May 16, 2018.

  3. “containers in docker 1.11 does not get same MTU as host #22297.” Docker GitHub. September 26, 2016. Accessed November 19, 2016.

  4. “iptables failed - No chain/target/match by that name #16816.” Docker GitHub. November 10, 2016. Accessed December 17, 2016.

  5. “LXC.” Ubuntu Documentation. Accessed August 8, 2017.

  6. “How to install and setup LXC (Linux Container) on Fedora Linux 26.” nixCraft. July 13, 2017. Accessed August 8, 2017.

  7. “Java inside docker: What you must know to not FAIL.” Red Hat Developers Blog. March 14, 2017. Accessed October 2018.

  8. “Improve docker container detection and resource configuration usage.” Java Bug System. November 16, 2017. Accessed October 5, 2018.

  9. “Dockerfile reference.” Docker Documentation. 2019. Accessed April 3, 2019.

  10. “Five Ways to Slim Docker Images.” Codacy Blog. December 14, 2017. Accessed March 21, 2020.

  11. “Best practices for writing Dockerfiles.” Docker Documentation. Accessed March 21, 2020.

  12. “How to Bootstrap different Linux Distribution Under Arch Linux.” September 6, 2015. Accessed May 30, 2020.

  13. “A Comprehensive Container Runtime Comparison.” Capital One Tech Cloud. June 10, 2020. Accessed November 22, 2020.

  14. “containers/crun.” GitHub. November 16, 2020. Accessed November 22, 2020.

  15. “containerd.” containerd. 2020. Accessed November 22, 2020.

  16. “Container runtimes.” Kubernetes Documentation. October 28, 2020. Accessed November 22, 2020.

  17. “cri-o.” cri-o. Accessed November 22, 2020.

  18. “The OpenShift Container Platform control plane.” OpenShift Container Platform 4.6 Documentation. Accessed November 22, 2020.

  19. “podman.” podman. November 13, 2020. Accessed November 22, 2020.

  20. “A Practical Introduction to Container Terminology.” Red Hat Developer. February 22, 2018. Accessed November 22, 2020.

  21. “docker push.” Docker Documentation. Accessed March 2, 2021.

  22. “Episode 147: CoreDNS.” GitHub vmware-tanzu/tgik. April 3, 2021. Accessed April 13, 2021.

  23. “Test an insecure registry.” Docker Documentation. Accessed April 21, 2021.

  24. “Docker Desktop for Mac user manual.” Docker Documentation. Accessed April 21, 2021.

  25. “Debugging Kubernetes nodes with crictl.” Kubernetes Documentation. December 10, 2020. Accessed July 20, 2021.