Docker, From Zero: What It Actually Solves, and How to Actually Use It
Part 1: The Real Problem Docker Solves (It's Not Just "Works on My Machine")
👦 Nephew: Uncle, everyone says Docker fixes "it works on my machine" problems. But what does that actually mean, in detail?
👨🦳 Uncle: That phrase is the symptom people quote, but the real disease underneath it is bigger, and worth understanding properly. Let's list the actual, concrete problems:
Problem 1 - Dependency conflicts on one machine. Say Project A needs Node.js version 16, and Project B, on the exact same laptop, needs Node.js version 20. Without Docker, you'd have to juggle multiple Node versions manually (using something like nvm), and it only gets messier once you add databases, system libraries, and specific OS-level tools into the mix.
Problem 2 - Setup instructions that rot over time. "Install Postgres 14, set this config flag, install these three system packages, make sure your OS is Ubuntu 22.04..." - these instructions are correct today and quietly wrong six months from now, once package versions move on. New team members lose entire days just getting a project running locally.
Problem 3 - "It ran on my laptop" vs. "it needs to run on a server." Your laptop might be macOS, the production server is Linux, and subtle differences between them (file paths, installed library versions, default configurations) cause things to behave differently in ways that are genuinely hard to track down.
👦 Nephew: So the real problem is: an application never travels alone. It always drags its entire environment along with it - and that environment is fragile and hard to reproduce.
👨🦳 Uncle: Exactly. Docker's actual job is: package your application together with its entire environment - the exact runtime, exact dependencies, exact configuration - into one single, self-contained unit that behaves identically no matter where it's run. Not "hopefully behaves the same." Identically. That's the real problem, and that's the real fix.
Part 2: What Docker Actually Is, Under the Hood
👨🦳 Uncle: Let's open the hood before we run a single command, because typing commands blindly is how people end up confused six months in. Docker has a few key pieces working together:
You type a command (e.g., docker run redis)
│
v
DOCKER CLIENT - the "docker" command itself, just a messenger
│
v
DOCKER DAEMON (dockerd) - a background process that does the REAL work:
building images, starting containers, managing networks and storage.
This is the actual engine.
│
v
containerd + runc - lower-level components that actually create and run
containers using Linux kernel features (namespaces for isolation,
cgroups for resource limits)
│
v
Your Linux kernel - the real operating system underneath everything
👦 Nephew: So when I type docker run, I'm not directly running anything - I'm just sending a request to this background daemon?
👨🦳 Uncle: Exactly. The docker command you type is just a thin messenger. The daemon (dockerd) is the actual worker that receives that request and does everything - pulling images, allocating resources, starting the container. This distinction matters, because "is Docker running" really means "is the daemon running," which we'll check properly in Part 4.
Images vs. Containers - The One Distinction That Clears Up Most Confusion
👦 Nephew: People say "image" and "container" almost interchangeably. Are they the same thing?
👨🦳 Uncle: No, and this distinction alone clears up a huge amount of confusion.
- An image is a read-only blueprint - a packaged snapshot of your application, its dependencies, and its configuration, sitting on disk, not running.
- A container is a running instance created from that image - the actual live process, with its own writable layer on top.
IMAGE (blueprint, not running)
│
│ "docker run" creates a running instance from it
v
CONTAINER (a live, running process, based on that image)
👦 Nephew: So I could start five containers from the same one image?
👨🦳 Uncle: Exactly - five independent, running instances, all built from the same underlying blueprint, each with their own isolated running state, but sharing the same read-only image layers underneath for efficiency.
Part 3: Installing Docker on Ubuntu
👨🦳 Uncle: Let's actually get it installed. On Ubuntu, the officially recommended path looks like this:
# 1. Update your package list and install a few prerequisites
sudo apt-get update
sudo apt-get install ca-certificates curl
# 2. Add Docker's official GPG key (verifies packages are genuinely from Docker)
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
# 3. Add Docker's official repository to your package sources
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] \
https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# 4. Update again, now that Docker's repo is added, then install
sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
👦 Nephew: That's a lot of steps just for an install.
👨🦳 Uncle: It is, and it's worth it precisely because it verifies you're installing genuine Docker packages, kept up to date through your normal package manager going forward. One more step that trips up almost every beginner:
# By default, Docker commands need "sudo" every time - annoying.
# Add your user to the "docker" group to fix that:
sudo usermod -aG docker $USER
# Then log out and back in (or restart your terminal) for it to take effect
👦 Nephew: And after that, docker run just works without sudo every time?
👨🦳 Uncle: Exactly - your user is now allowed to talk to the daemon directly, without elevated permissions for every single command.
Part 4: The Daemon - Checking If Docker Is Actually Running
👦 Nephew: You mentioned the daemon earlier. How do I actually check if it's running, and what happens if it's not?
👨🦳 Uncle: The daemon (dockerd) is a background service - much like any other system service on Linux (think of how your web server or database might run continuously in the background). It needs to be running before any Docker command will actually work.
# Check whether the Docker service (the daemon) is active
$ sudo systemctl status docker
● docker.service - Docker Application Container Engine
Loaded: loaded
Active: active (running) since ...
# If it's not running, start it
$ sudo systemctl start docker
# And to make sure it starts automatically every time you boot your machine
$ sudo systemctl enable docker
👦 Nephew: What if I run a Docker command while the daemon is stopped?
👨🦳 Uncle: You'll get a clear complaint, something like:
Cannot connect to the Docker daemon at unix:///var/run/docker.sock.
Is the docker daemon running?
That error is precisely confirming the architecture from Part 2 - the CLI is just a messenger; if there's no daemon listening on the other end, the message has nowhere to go.
A quick general sanity check, once it's running:
$ docker version # shows client AND daemon (server) versions
$ docker info # detailed info about the running daemon itself
$ docker ps # lists currently RUNNING containers (empty if none yet)
Part 5: Running Your First Real Thing - A Database in Docker
👨🦳 Uncle: Theory's done. Let's actually run something useful - a Redis database, without installing Redis on your machine at all.
$ docker run -d --name my-redis -p 6379:6379 redis
👦 Nephew: Let's break that down piece by piece.
👨🦳 Uncle: Gladly:
| Part | Meaning |
|---|---|
docker run |
Create and start a new container |
redis |
The image to use - Docker automatically downloads ("pulls") it from Docker Hub if you don't already have it locally |
-d |
"Detached" - run in the background, don't block your terminal |
--name my-redis |
Give this container a friendly name, instead of a random generated one |
-p 6379:6379 |
Map port 6379 on your machine to port 6379 inside the container (we'll go deep on this in Part 6) |
# Confirm it's actually running
$ docker ps
CONTAINER ID IMAGE STATUS PORTS NAMES
a1b2c3d4e5f6 redis Up 5 seconds 0.0.0.0:6379->6379/tcp my-redis
# See what the container itself is printing (its logs)
$ docker logs my-redis
# Actually get a shell INSIDE the running container, to poke around
$ docker exec -it my-redis redis-cli
127.0.0.1:6379>
👦 Nephew: So my actual machine never had Redis "installed" in the traditional sense - it's living entirely inside that container?
👨🦳 Uncle: Entirely - and if you docker stop my-redis and docker rm my-redis, every trace of it vanishes from your system, cleanly, with nothing left behind to manually uninstall. That disposability is a genuine feature, not a limitation - though it does raise an important question we need to solve next: what happens to your actual data when the container disappears?
Part 6: Ports - Why a Container Only Exposes What You Explicitly Allow
👨🦳 Uncle: Let's slow down on that -p 6379:6379 flag, because understanding it properly avoids a lot of beginner confusion later.
A running container has its own isolated network space, completely separate from your actual machine's network. Redis, running inside the container, is listening on port 6379 - but that's port 6379 inside the container's own private network, not automatically reachable from your machine at all.
Your Machine (host) Container (isolated network)
Port 6379 on YOUR machine? -----X NOT connected to anything
Redis listening on port 6379,
but INSIDE the container only
The -p hostPort:containerPort flag is what explicitly creates a bridge between the two:
-p 6379:6379
│ │
│ └── Port INSIDE the container that the app is actually listening on
└── Port on YOUR actual machine that gets forwarded to it
👦 Nephew: So if I don't add -p, the container runs, Redis works fine internally, but I genuinely can't reach it from my own machine at all?
👨🦳 Uncle: Exactly - and that's a deliberate, useful safety default, not an oversight. A container only exposes exactly what you explicitly tell it to. If a container is running an internal tool that only other containers need to talk to, and never your actual host machine directly, you simply don't map a port for it at all - reducing what's reachable to the bare minimum needed.
# You can also map to a DIFFERENT port on your host, if 6379 is
# already taken by something else on your machine
$ docker run -d -p 7000:6379 redis
# Now YOUR machine's port 7000 reaches the container's internal 6379
Part 7: Networks - How Containers Talk to Each Other
👦 Nephew: What if I have two containers - my Node app and Redis - and I want them to talk to each other, not just me talking to Redis from outside?
👨🦳 Uncle: This is exactly what a Docker network solves. By default, containers exist somewhat isolated from each other. But you can create a shared network and attach multiple containers to it - once they're on the same network, they can find and talk to each other by container name, automatically, without you manually tracking IP addresses.
# Create a custom network
$ docker network create my-app-network
# Run Redis attached to that network
$ docker run -d --name my-redis --network my-app-network redis
# Run your Node app attached to the SAME network
$ docker run -d --name my-app --network my-app-network -p 3000:3000 my-node-image
Now, inside your Node app's code, you can connect to Redis using the container's name as if it were a hostname:
// Inside the Node app container, this just works -
// Docker's internal DNS resolves "my-redis" to the right container
const redis = require('redis');
const client = redis.createClient({ url: 'redis://my-redis:6379' });
👦 Nephew: So my-redis isn't a real internet domain - it's a name Docker itself understands, only within that shared network?
👨🦳 Uncle: Exactly - Docker runs its own small internal DNS system for containers on the same custom network, resolving container names to their internal addresses automatically. This is precisely why multi-container setups (which we'll simplify further in Part 9) don't need you to hunt down and hardcode IP addresses anywhere.
Part 8: Volumes - Making Data Actually Survive
👦 Nephew: Back to the question from Part 5 - if I remove my Redis container, what happens to any data that was stored in it?
👨🦳 Uncle: Gone, completely, unless you've explicitly set up a volume. By default, anything a container writes lives in its own temporary, disposable writable layer - the moment the container is removed, that layer, and everything in it, is deleted along with it.
This is intentional; containers are meant to be disposable, replaceable at any moment. Your actual data, however, usually shouldn't be.
A volume is a piece of storage managed by Docker itself, existing outside any single container's own disposable layer. You mount it into a container at a specific folder path - and even if that container is deleted and a brand-new one is started in its place, the volume, and everything inside it, is still there.
# Create a named volume
$ docker volume create my-redis-data
# Run Redis, mounting that volume to the folder where Redis actually
# stores its data internally
$ docker run -d --name my-redis -v my-redis-data:/data redis
WITHOUT a volume: Container removed → all data inside it is gone forever
WITH a volume: Container removed → volume (and its data) SURVIVES independently
New container started, same volume attached → picks up right
where the old one left off, data intact
👦 Nephew: So the volume is basically stored outside the container's own lifecycle entirely?
👨🦳 Uncle: Precisely - it's managed by Docker, but lives independently of any single container's lifespan.
Comments
No comments yet. Start the discussion.