# 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.

```shell
mkdir myapp && cd myapp
```

A one-line script, `greet`:

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

make executable:

```bash
chmod +x greet
```

and the Dockerfile:

```dockerfile
FROM ubuntu:24.04

WORKDIR /app

COPY greet .

CMD ["./greet"]
```

build with:

```bash
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.
