Research

Introduction to Docker

Historical Docker lab note covering container images, registries, service deployment, port publishing, persistence, troubleshooting, and container security basics.

  • Docker
  • Linux
  • Containers
On this page

Docker logo

What Docker is

Docker is containerisation software used to package and run applications in isolated user-space environments called containers. A container includes the application and the libraries, dependencies, configuration, and runtime pieces it needs, without requiring a full guest operating system for every service.

In my lab, I used Docker to run small Linux environments and services on an Ubuntu Server VM. The host provided the kernel and Docker runtime; the containers provided the isolated application environments. This let me start, test, stop, remove, and rebuild services without rebuilding the whole virtual machine.

The original lab VM was built in VMware Workstation Pro 16 with Ubuntu Server 18.04, 4 CPU cores, 8 GB of RAM, 60 GB of storage, and a NAT network adapter. I enabled SSH so I could manage the machine from another VM on the same subnet.

The first setup step was updating the Ubuntu packages:

sudo apt update && sudo apt upgrade -y

I then installed Docker from the Ubuntu package repository:

sudo apt install docker.io

The older lab also experimented with the Snap package. I would not present that as the preferred install path now; for a current build, I would check Docker’s own installation guidance for the target distribution and keep the install method consistent.

Containers, images, and registries

An image is the template Docker uses to create containers. It contains the filesystem layers and instructions needed for the container environment. A container is the running instance created from an image.

Images are usually built from Dockerfiles. A Dockerfile defines the base image, dependencies, libraries, configuration, and the command or application that should run when the container starts.

Docker Hub is a public registry for container images. Pulling an image downloads it from the registry so it can be used locally:

sudo docker pull centos

In this lab, I pulled CentOS first and used it to create a basic container:

sudo docker run -d -t --name mycentoscontainer centos

The flags are straightforward:

  • docker run creates and starts a new container.
  • -d runs it in detached mode, so it starts in the background.
  • -t allocates a pseudo-terminal.
  • --name mycentoscontainer gives the container a readable name.
  • centos tells Docker which image to use.

To list running containers, I used:

docker ps

To open a shell inside the running CentOS container:

sudo docker exec -it mycentoscontainer bash

Here, docker exec runs a command inside an existing container. The -i flag keeps STDIN open, and -t allocates a pseudo-terminal, so the shell behaves like an interactive session. Typing exit returns to the host shell.

Containers versus virtual machines

Virtual machines and containers solve different problems. A virtual machine virtualises hardware. It runs a full guest operating system with its own kernel on top of a hypervisor.

Virtual machine hypervisor diagram from the original Docker post

A container uses operating-system-level virtualisation. Docker runs on the host operating system and starts containers that share the host kernel while isolating the application environment.

Docker container architecture diagram

That shared-kernel design is why containers are lightweight compared with full virtual machines. They do not need to boot a separate kernel for each service, and they do not need a full OS installation per workload. They are usually faster to start, lighter on disk, and easier to recreate.

The trade-off is isolation. A VM gives a stronger boundary because it has a separate guest OS and kernel. A container is more convenient for packaging and running services, but it is not the same isolation boundary as a VM.

The compatibility point is tied to the kernel. Linux containers depend on a Linux kernel. Windows containers depend on Windows container support. When Linux containers run on Windows or macOS, Docker uses a Linux environment behind the scenes so the containers still have the kernel behaviour they need.

Running services in containers

After CentOS, I pulled Alpine Linux:

sudo docker pull alpine

Alpine is a small Linux distribution based on musl and BusyBox. It is commonly used for compact container images because it keeps the base environment small.

I started an Alpine container in the same style as the CentOS one:

sudo docker run -d -t --name myalpinecontainer alpine

Running docker ps then showed both containers on the same host:

Docker ps output showing Alpine and CentOS containers running together

That is the basic Docker workflow: pull an image, run a container from it, check the running containers, and execute commands inside the container when needed.

The lab then moved from operating-system-style containers to an application container with WordPress. I pulled the image:

sudo docker pull wordpress

Then I ran it as a service:

sudo docker run -t -d -p 80:80 --name mywebsite wordpress

The new option there is -p 80:80. This publishes a container port to the host. The syntax is:

HOST_PORT:CONTAINER_PORT

In this example, traffic to port 80 on the host is forwarded to port 80 inside the WordPress container. Without publishing the port, the service may be running inside the container but not reachable from outside in the way I expected.

To stop a running container:

sudo docker stop <name>

To remove a stopped container:

sudo docker rm <name>

Removing a container removes that container instance. It does not remove the original image, and it does not preserve files written only inside the container unless that data is stored somewhere persistent.

Ports, volumes, and persistence

Port publishing controls how a container service becomes reachable from the host or network. A container can run an application internally, but the host needs an explicit mapping if the service should be accessed through a host port.

That is why the WordPress example used:

-p 80:80

The left side is the host port. The right side is the container port. Changing the host side would allow the same container port to be exposed on a different host port, which is useful when testing multiple services on one machine.

Persistence is the other half of service deployment. Containers are designed to be replaceable. If a container writes data only into its own writable layer, that data is tied to the lifecycle of that container. Remove the container and that internal state is gone.

For services that need durable data, such as websites, databases, logs, uploads, or configuration, the data should be stored through a Docker volume or a carefully chosen bind mount. That separates the service runtime from the data that should survive rebuilds.

For later lab work, I separated disposable container state from data that needed to persist.

Docker in lab environments

Docker works well for lab environments because it makes service deployment quick and repeatable. A container can be started from a known image, configured, tested, stopped, removed, and recreated without rebuilding an entire VM.

For security and infrastructure labs, that supports workflows such as:

  • run a web service and expose only the required port;
  • test how a configuration change affects the service;
  • keep throwaway services separate from the host;
  • run multiple small services on one lab machine;
  • rebuild a service quickly after breaking it;
  • document the image, port mapping, and persistent storage decisions.

Docker also supports virtual networks, which lets containers communicate with each other without placing every service directly onto the host network. That becomes useful once a lab moves beyond a single container and starts modelling small application stacks.

Troubleshooting note: AppArmor and stuck containers

One issue I hit in the lab was a container that would not stop cleanly. The error looked like this:

Error response from daemon: Cannot kill container: 981a44d4c79: Cannot kill container xxxx: unknown error after kill: runc did not terminate successfully: container_linux.go:392: signalling init process cause "permission denied"

At the time, I traced this to AppArmor on Linux. AppArmor is a Linux kernel security module that restricts program behaviour through per-program profiles. The workaround I used was:

sudo aa-remove-unknown

After that, I retried the container stop. I would treat this as a historical troubleshooting note rather than a blanket recommendation. AppArmor is a security control, so removing unknown profiles should not be done casually on a real system.

Security considerations

Containers are useful, but they are not automatically secure. The main security considerations I keep in mind are:

  • Containers share the host kernel, so they are not the same isolation boundary as virtual machines.
  • Avoid running containers as root where possible.
  • Be careful with mounted host paths; a risky bind mount can expose sensitive host files to a container.
  • Do not expose unnecessary ports.
  • Store and inject secrets properly rather than baking them into images or command history.
  • Keep images updated and rebuild containers when base images change.
  • Prefer trusted images and understand what is inside them.
  • Treat container escape and container misconfiguration as real risks.
  • Keep logging and monitoring in mind, especially for services exposed outside a lab host.

Docker is strong for repeatable lab services, but the host still needs normal system hardening. If the Docker host is weak, overexposed, or running containers with excessive privileges and broad mounts, the container layer will not fix the underlying risk.

Currentness note

This page comes from a 2021 Docker lab. The core Docker model is still the same: images create containers, containers share the host kernel, port mappings expose services, and persistent data should live outside the disposable container layer.

Some details change over time. Docker installation methods, image names, default tags, CentOS image availability, and recommended hardening settings should be checked against current documentation before using this as a build guide.