Docker Fundamentals: Images, Containers and the Docker Daemon (2026)
Docker solves the "works on my machine" problem by packaging an application and everything it needs — runtime, libraries, config — into a single portable unit called a container. Containers share the host OS kernel but are isolated from each other via Linux namespaces and cgroups, giving you near-native performance with VM-like isolation. This phase covers the core mental model: what images and containers are, how the Docker daemon works, and the fundamental commands every developer needs daily.
Table of Contents
Architecture Overview
# Docker has three components:
#
# 1. Docker Daemon (dockerd)
# — Background service that manages images, containers, networks, volumes
# — Listens on a Unix socket: /var/run/docker.sock
# — Does the actual work: pulling images, starting containers, etc.
#
# 2. Docker Client (docker CLI)
# — The command you type: docker run, docker build, docker ps
# — Sends API calls to the daemon over the socket
# — Client and daemon can be on different machines (Docker contexts)
#
# 3. Docker Registry
# — Stores and distributes images
# — Docker Hub is the default public registry
# — Private registries: GHCR, ECR, GCR, your own Harbor instance
#
# Flow: docker run nginx
# Client → API request → Daemon → checks local image cache
# If not found → pulls from Docker Hub → starts container
# Verify your installation
docker version # Shows client + daemon versions
docker info # Full daemon config: storage driver, cgroup, total containers
docker system df # Disk usage: images, containers, volumes, build cache
Images and Layers
# An image is a read-only template made of stacked layers.
# Each instruction in a Dockerfile creates one layer.
# Layers are content-addressed (SHA256) and shared across images —
# if two images use the same Ubuntu base, that layer is stored once on disk.
#
# Image naming: [registry/][namespace/]name[:tag]
# nginx → docker.io/library/nginx:latest
# node:20-alpine → docker.io/library/node:20-alpine
# ghcr.io/org/app:v1.2.3
# Pull an image without running it
docker pull node:20-alpine
# List local images
docker images
# REPOSITORY TAG IMAGE ID CREATED SIZE
# node 20-alpine 3f9a0c123456 2 weeks ago 133MB
# Inspect image layers
docker history node:20-alpine
# IMAGE CREATED CREATED BY SIZE
# 3f9a0c123456 2 weeks ago CMD ["node"] 0B
# <missing> 2 weeks ago ENV NODE_VERSION=20.15.0 0B
# ...
# See the full image manifest and config
docker inspect node:20-alpine
# Image ID vs Digest
# ID: short hash of the image config (changes with rebuild)
# Digest: sha256 of the manifest — immutable, use this for production pins
docker pull nginx@sha256:a4c4106... # Pin to exact digest
Container Lifecycle
# Container states:
# created → allocated but never started
# running → process is active
# paused → SIGSTOP sent, process frozen
# stopped → process exited or killed, filesystem still on disk
# removed → gone (--rm flag or docker rm)
#
# A container = image layers (read-only) + writable layer (container-specific)
# When a container writes files, they go in the writable layer (copy-on-write)
# When the container is removed, the writable layer is deleted
# Full lifecycle
docker create --name myapp nginx # Create, don't start
docker start myapp # Start it
docker pause myapp # Freeze
docker unpause myapp # Unfreeze
docker stop myapp # SIGTERM → wait 10s → SIGKILL
docker kill myapp # SIGKILL immediately
docker rm myapp # Delete stopped container
docker rm -f myapp # Force-remove running container
# run = create + start (most common)
docker run nginx
# List containers
docker ps # Running only
docker ps -a # All (including stopped)
docker ps -q # Just IDs (useful in scripts)
Running Containers
# Key flags for docker run:
# -d / --detach: run in background
docker run -d nginx
# --name: give it a name (otherwise Docker assigns a random adjective_noun)
docker run -d --name webserver nginx
# -it: interactive terminal (for shells, REPLs)
docker run -it ubuntu bash
docker run -it node:20-alpine node # Drop into Node REPL
# --rm: auto-remove when stopped (great for one-off tasks)
docker run --rm alpine echo "Hello from Alpine"
# --restart: restart policy
docker run -d --restart unless-stopped nginx
# Policies: no (default), on-failure[:N], always, unless-stopped
# Execute a command in a running container
docker exec -it webserver bash # Open a shell
docker exec webserver nginx -t # Test nginx config without shell
docker exec -e DEBUG=1 webserver env # Pass env var to exec command
# Copy files between host and container
docker cp ./config.json webserver:/etc/app/config.json
docker cp webserver:/var/log/nginx/error.log ./error.log
Port Mapping
# Containers have their own network namespace — ports are not accessible from
# the host unless explicitly mapped.
#
# -p hostPort:containerPort
docker run -d -p 8080:80 nginx
# Now: http://localhost:8080 → container port 80
# Map to a specific host IP (e.g., only localhost, not external interfaces)
docker run -d -p 127.0.0.1:8080:80 nginx
# Ephemeral port (Docker picks a free host port)
docker run -d -p 80 nginx
docker port container_name 80 # See which host port was assigned
# Multiple ports
docker run -d \
-p 8080:80 \
-p 8443:443 \
nginx
# UDP port
docker run -d -p 53:53/udp dns-server
# See all port mappings for a container
docker port webserver
# 80/tcp -> 0.0.0.0:8080
# 443/tcp -> 0.0.0.0:8443
# EXPOSE in Dockerfile is documentation only — it doesn't publish ports.
# You still need -p at runtime to access them from the host.
Environment Variables
# Pass configuration to containers via environment variables (12-factor app)
# Single variable
docker run -d -e NODE_ENV=production -e PORT=3000 myapp
# From a file (one KEY=VALUE per line, no quotes)
# .env file:
# NODE_ENV=production
# DATABASE_URL=postgres://...
docker run -d --env-file .env myapp
# Inherit from host shell (no = means: use the value from your shell)
export DB_PASSWORD=secret
docker run -d -e DB_PASSWORD myapp # Passes $DB_PASSWORD from shell
# View env vars in a running container
docker exec myapp env
docker inspect myapp --format='{{json .Config.Env}}'
# Never bake secrets into images — always inject at runtime via env vars or
# Docker secrets (Swarm) / Kubernetes secrets.
Inspecting Containers
# docker inspect: full JSON metadata about a container or image
docker inspect webserver
# Extract specific fields with --format (Go template)
docker inspect webserver --format='{{.State.Status}}' # running
docker inspect webserver --format='{{.NetworkSettings.IPAddress}}' # 172.17.0.2
docker inspect webserver --format='{{json .HostConfig.PortBindings}}'
# Logs
docker logs webserver # All logs so far
docker logs -f webserver # Follow (like tail -f)
docker logs --tail 50 webserver # Last 50 lines
docker logs --since 10m webserver # Last 10 minutes
docker logs -t webserver # With timestamps
# Resource usage (live)
docker stats # All running containers
docker stats webserver # Single container
# Shows: CPU%, MEM USAGE/LIMIT, MEM%, NET I/O, BLOCK I/O, PIDS
# Top-like process list inside a container
docker top webserver
Cleanup
# Stopped containers, unused images and dangling layers accumulate fast.
# Remove a stopped container
docker rm container_id_or_name
# Remove all stopped containers
docker container prune
# Remove an image
docker rmi nginx:latest
docker rmi image_id
# Remove unused images (not referenced by any container)
docker image prune # Dangling images only (untagged)
docker image prune -a # All unused images
# Remove unused volumes
docker volume prune
# Remove unused networks
docker network prune
# Nuclear option — remove everything unused at once
docker system prune
docker system prune -a --volumes # Also removes unused images and volumes
# Check what you'd free before running prune
docker system df
# TYPE TOTAL ACTIVE SIZE RECLAIMABLE
# Images 12 3 4.2GB 3.1GB (73%)
# Containers 5 2 124MB 89MB (71%)
# Local Volumes 8 3 2.3GB 1.8GB (78%)
# Build Cache - - 890MB 890MB
Next: Phase 2 — Docker CLI Mastery covers the full command toolkit: filtering, formatting output, image tagging, pushing to registries and scripting with docker commands.