How Containers Actually Work

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
FROMsets the base image.RUNexecutes commands during the build. EachRUNcreates a new layer, so related commands should be bundled together rather than spread across multiple lines.COPYbrings files from the build context into the image.WORKDIRsets the working directory for every instruction that follows it.ENVsets environment variables available at runtime.EXPOSEdocuments which ports the application uses. It does not actually publish them, that still requires-pat runtime.CMDis the default command the container runs on startup, and it can be overridden at runtime.ENTRYPOINTworks likeCMDbut 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.



