Docker Fundamentals¶
Docker provides a way to package and run an application in an isolated environment called a container. The isolation and security allow you to run many containers simultaneously on a given host. Unlike virtual machines, containers share the host kernel and start in milliseconds rather than minutes - making them the standard unit of deployment for modern applications.
Understanding the Architecture¶
Docker uses a client-server architecture. The Docker client talks to the Docker daemon, which does the heavy lifting of building, running, and distributing your containers.
- Docker Daemon (
dockerd): Listens for Docker API requests and manages Docker objects such as images, containers, networks, and volumes. It runs as a background service on the host. - Docker Client (
docker): The primary way users interact with Docker. When you run commands likedocker run, the client sends these commands todockerdvia the Docker API. - Docker Registries: Stores and distributes Docker images. Docker Hub is the default public registry. Organizations often run private registries using Harbor or cloud-provider registries (ECR, GCR, ACR).
graph LR
A[Docker Client] -->|REST API| B[Docker Daemon]
B --> C[Images]
B --> D[Containers]
B --> E[Networks]
B --> F[Volumes]
B -->|pull/push| G[Registry]
Images and Containers¶
Images¶
An image is a read-only template with instructions for creating a Docker container. Images are built in layers - each instruction in a Dockerfile creates a new layer stacked on top of the previous ones. When you change a layer, only that layer and everything above it gets rebuilt.
# List images on your system
docker images
# Pull an image from Docker Hub without running it
docker pull nginx:1.25
# Remove an image
docker rmi nginx:1.25
Image layer caching
Docker caches each layer independently. If nothing changed in a layer, Docker reuses the cached version. This is why Dockerfiles copy dependency files (like package.json or requirements.txt) before copying application code - dependencies change less frequently, so that layer stays cached across most builds.
Containers¶
A container is a runnable instance of an image. You can create, start, stop, and delete containers using the Docker CLI. Each container gets its own filesystem, networking, and process tree - isolated from the host and from other containers.
Containers are ephemeral by default. When you remove a container, any data written inside it is gone. This is by design - it forces you to think about persistence explicitly through volumes.
Container Lifecycle¶
A container moves through several states during its life:
graph LR
A[Created] -->|start| B[Running]
B -->|stop| C[Stopped]
C -->|start| B
C -->|rm| D[Removed]
B -->|kill| C
B -->|rm -f| D
# Create a container without starting it
docker create --name my-app nginx
# Start a stopped or created container
docker start my-app
# Stop gracefully (sends SIGTERM, then SIGKILL after timeout)
docker stop my-app
# Kill immediately (sends SIGKILL)
docker kill my-app
# Remove a stopped container
docker rm my-app
# Force-remove a running container (stop + rm in one step)
docker rm -f my-app
Running Containers¶
The docker run command combines create and start into a single step. It pulls the image if not present, creates a container, and starts it.
# Run Nginx in the background, map port 8080 on host to 80 in container
docker run -d --name my-web -p 8080:80 nginx
# Run an interactive Ubuntu shell (removed on exit)
docker run -it --rm ubuntu bash
# Run with environment variables
docker run -d --name my-db \
-e POSTGRES_USER=admin \
-e POSTGRES_PASSWORD=secret \
-p 5432:5432 \
postgres:16
-d: Detached mode - runs in the background and prints the container ID.-it: Interactive terminal - allocates a pseudo-TTY and keeps STDIN open. Use this for shells.--rm: Automatically remove the container when it exits. Good for throwaway tasks.--name: Assigns a human-readable name instead of Docker's random names.-p HOST:CONTAINER: Maps a host port to a container port.-e KEY=VALUE: Sets an environment variable inside the container.
Inspecting Running Containers¶
# List running containers
docker ps
# List all containers (including stopped)
docker ps -a
# View detailed container metadata (networking, mounts, config)
docker inspect my-web
# See resource usage (CPU, memory, network I/O)
docker stats
Working Inside Containers¶
You frequently need to run commands inside a running container for debugging, database administration, or one-off tasks.
# Open a shell in a running container
docker exec -it my-web bash
# Run a single command without an interactive shell
docker exec my-db psql -U admin -c "SELECT version();"
# View container logs (stdout/stderr)
docker logs my-web
# Follow logs in real time (like tail -f)
docker logs -f --tail 100 my-web
Running as root inside containers
By default, processes inside a container run as root. This means a container breakout vulnerability gives the attacker root access on the host. Always add a USER directive in your Dockerfile to run as a non-root user, and use --read-only to mount the container filesystem as read-only where possible.
Dockerfile: Building Your Own Image¶
A Dockerfile is a text file with instructions for assembling an image. Each instruction creates a layer.
Basic Dockerfile¶
# Start from an official Python image
FROM python:3.12-slim
# Set the working directory
WORKDIR /app
# Copy dependency file first (layer caching)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY . .
# Expose the port the app runs on
EXPOSE 8000
# Run the application
CMD ["python", "app.py"]
Key Instructions¶
| Instruction | Purpose |
|---|---|
FROM |
Sets the base image. Every Dockerfile starts with this. |
WORKDIR |
Sets the working directory for subsequent instructions. |
COPY |
Copies files from the build context into the image. |
RUN |
Executes a command during the build (installing packages, compiling code). |
EXPOSE |
Documents which port the container listens on. Does not actually publish it. |
CMD |
Default command when the container starts. Only the last CMD takes effect. |
ENTRYPOINT |
Like CMD, but arguments passed to docker run are appended rather than replacing it. |
ENV |
Sets environment variables available during build and at runtime. |
USER |
Switches to a non-root user for subsequent instructions and at runtime. |
ARG |
Defines build-time variables (not available at runtime). |
The .dockerignore File¶
Just like .gitignore, a .dockerignore file prevents unnecessary files from being sent to the Docker daemon during builds. This speeds up builds and prevents secrets from leaking into images.
Building and Tagging¶
# Build from the current directory
docker build -t my-app .
# Build with a specific tag (version)
docker build -t my-app:1.0.0 .
# Build and specify a different Dockerfile
docker build -f Dockerfile.prod -t my-app:prod .
Multi-Stage Builds¶
Multi-stage builds let you use multiple FROM statements in a single Dockerfile. This is the standard pattern for producing small, secure production images - you compile or build in one stage, then copy only the artifacts into a minimal final stage.
# Stage 1: Build
FROM golang:1.22 AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /app
# Stage 2: Production
FROM alpine:3.19
RUN apk --no-cache add ca-certificates
COPY --from=builder /app /app
USER nobody
ENTRYPOINT ["/app"]
The final image contains only the compiled binary and ca-certificates - no Go toolchain, no source code. A Go application image built this way is typically 10-20 MB instead of 800+ MB.
Volumes and Persistence¶
Containers are ephemeral - when you remove one, its filesystem is gone. Volumes are Docker's mechanism for persistent data that outlives any individual container.
Volume Types¶
| Type | Syntax | Use Case |
|---|---|---|
| Named volume | -v mydata:/var/lib/data |
Production data (databases, uploads). Docker manages the storage location. |
| Bind mount | -v /host/path:/container/path |
Development (mount source code for live reloading). |
| tmpfs mount | --tmpfs /tmp |
Scratch space that should never touch disk (secrets, session data). |
# Create and use a named volume
docker volume create pgdata
docker run -d --name db -v pgdata:/var/lib/postgresql/data postgres:16
# Bind mount for development
docker run -d --name dev-app -v $(pwd):/app -p 3000:3000 node:20
# List volumes
docker volume ls
# Remove unused volumes
docker volume prune
Bind mounts expose the host filesystem
Bind mounts give the container direct access to host directories. A misconfigured bind mount (e.g., -v /:/host) can expose the entire host filesystem. In production, prefer named volumes. Reserve bind mounts for development workflows where you need live code reloading.
Networking¶
Docker creates isolated networks so containers can communicate with each other without exposing ports to the host.
Network Types¶
| Driver | Description |
|---|---|
bridge |
Default. Containers on the same bridge network can reach each other by container name. |
host |
Removes network isolation - the container shares the host's network stack. |
none |
Disables networking entirely. |
overlay |
Spans multiple Docker hosts. Used with Docker Swarm for multi-node clusters. |
# Create a custom bridge network
docker network create my-net
# Run containers on the same network
docker run -d --name api --network my-net my-api-image
docker run -d --name db --network my-net postgres:16
# The api container can now reach the database at hostname "db"
# No port publishing needed for container-to-container communication
Containers on the default bridge network cannot resolve each other by name - they can only communicate by IP address. Custom bridge networks enable DNS-based service discovery, which is why you should always create a named network for multi-container setups.
Resource Limits¶
Without limits, a single container can consume all available CPU and memory on the host, starving other containers and system processes.
# Limit memory to 512MB and CPU to 1.5 cores
docker run -d --name api \
--memory=512m \
--cpus=1.5 \
my-api-image
# Set a memory reservation (soft limit) and hard limit
docker run -d --name worker \
--memory=1g \
--memory-reservation=512m \
my-worker-image
If a container exceeds its memory limit, Docker kills it with an out-of-memory (OOM) error. You can see this in docker inspect:
Debugging Containers¶
When a container crashes or behaves unexpectedly, these commands help you diagnose the problem.
# Check why a container stopped
docker inspect my-app --format='{{.State.ExitCode}} {{.State.Error}}'
# View the last 200 lines of logs
docker logs --tail 200 my-app
# See what changed in the container's filesystem since it started
docker diff my-app
# Export a stopped container's filesystem for inspection
docker export my-app | tar -tf - | head -50
# Start a fresh container from the same image for comparison
docker run -it --rm my-app-image sh
Common exit codes:
| Code | Meaning |
|---|---|
0 |
Clean exit |
1 |
Application error |
137 |
Killed by OOM or docker kill (128 + SIGKILL=9) |
139 |
Segmentation fault (128 + SIGSEGV=11) |
143 |
Graceful stop via docker stop (128 + SIGTERM=15) |
Putting It All Together¶
Interactive Quizzes¶
Further Reading¶
- Docker Documentation - official guides covering installation, configuration, and advanced features
- Dockerfile Best Practices - official recommendations for writing efficient, secure Dockerfiles
- Docker Hub - the default public registry with official and community images
- Dive - a tool for exploring Docker image layers and finding wasted space
Next: Docker Compose | Back to Index