Skip to main content

Command Palette

Search for a command to run...

How Containers Actually Work

Updated
4 min readView as Markdown
How Containers Actually Work
A
I am a cloud student, engineer, husband, father. Just documenting my journey as I learn cloud technologies. Want to connect or potentially hire?! connect with me on Linkedin

I've been using containers for a while now without fully understanding what's going on under the hood. This week I went back to the fundamentals; what problem do containers solve, what do namespaces and cgroups actually do, and whats the difference between docker run, docker exec, and docker attach. Here is what I found.

History/Context

Applications used to run directly on bare metal servers(some still do). One app, one server, no isolation. This caused problems you would expect; dependency conflicts between applications sharing the same machine, and a single point of failure if that one server went down.

The hypervisor solved part of this. Virtual machines let you run multiple isolated operating systems on the same physical hardware, which fixed the dependency problem and made things more fault tolerant. However every VM needed its own operating system, its own resource provisioning, and boots slowly compared to containers.

Containers were the next logical step. Lightweight, fast to start, efficient with resources, and friendly to work with as a developer. They are not magic and they do not solve every problem, but they solved enough of the right problems to become foundational infrastructure.

What Actually Makes a Container a Container

Two Linux kernel features do the most of the work: "namespaces" and "cgroups". These are not the only things that make a container work e.g. "runc", but for the sake of simplicity these 2 are the most important. Remember the 80/20 rule.

Namespaces handle isolation. Basically tricks a process into believing it is the only thing running on the system. A process inside a container sees its own process list, its own network stack, its own filesystem, even though it is sharing a kernel with everything else on the host.

Cgroups handle resource limits. Control groups constrain what a container is allowed to use: CPU, memory, IO, and more. Without cgroups, one noisy container could starve everything else on the host.

I followed along w/this video building a container with nothing but Go, no Docker involved. It was one of those exercises where you understand enough to see how the pieces fit, without fully understanding every detail underneath. I will be coming back to this one.

https://www.youtube.com/live/2e0dhoPeKfI?si=JGA0FuWOvCajojZ0

docker run, exec, and attach

These three commands look similar but get used for different tasks.

docker run starts a new container from an image.

docker exec starts a new process inside a container that is already running, usually a shell, so you can look around. Exit that shell and the container keeps running like nothing happened.

docker attach connects your terminal directly to the container's existing process, usually PID 1. If that container is running something like nginx, attaching shows you its live output. The one thing to be careful of; hitting Ctrl+C here can send a signal to that process and stop the container. Detach safely with Ctrl+P then Ctrl+Q instead.

TL;DR: run to start something new, exec to open a side door and poke around, attach to watch or interact with main process.

Building My First Docker Container

A simple exercise, but a good one for making a Dockerfile stop feeling like a template you copy without reading.

mkdir myapp && cd myapp

A one-line script, greet:

#!/bin/bash
echo "Hello from my container!"

make executable:

chmod +x greet

and the Dockerfile:

FROM ubuntu:24.04

WORKDIR /app

COPY greet .

CMD ["./greet"]

build with:

docker build -t myapp:1.0.0 .

-t myapp:1.0.0 tags the image with a name and version. The trailing . tells Docker to use the current directory as the build context.

The Dockerfile Instructions, One at a Time

  • FROM sets the base image.

  • RUN executes commands during the build. Each RUN creates a new layer, so related commands should be bundled together rather than spread across multiple lines.

  • COPY brings files from the build context into the image.

  • WORKDIR sets the working directory for every instruction that follows it.

  • ENV sets environment variables available at runtime.

  • EXPOSE documents which ports the application uses. It does not actually publish them, that still requires -p at runtime.

  • CMD is the default command the container runs on startup, and it can be overridden at runtime.

  • ENTRYPOINT works like CMD but is harder to override, which makes it the better choice for a container that should always run one specific executable.

None of this is complicated once it is written down like this. It just was not obvious to me until I built it myself.

Next Time: backing up my Production applications data and possibly migrating Linkding to Postgres with CNPG.