Module 01 — Docker Fundamentals
In this module you install Docker Engine on the existing app-server from the Fundamentals track, learn how Docker works under the hood, and build your first custom image.
No new VMs are needed — everything runs on app-server (192.168.56.12).
Before you proceed, take a moment and watch how Docker was introduced in 2010.
1. How Docker Works
Before you touch any commands, understand the three layers that make Docker tick.
1.1 Architecture
When you type docker run, here is what happens behind the scenes:
docker CLI ──▶ dockerd (daemon) ──▶ containerd ──▶ runc
(client) (API server) (container (spawns the
runtime) container)
- docker CLI — the command-line tool you interact with. It sends REST API calls to the daemon.
- dockerd — the Docker daemon. It manages images, containers, networks, and volumes.
- containerd — a lower-level container runtime that manages the container lifecycle (create, start, stop, delete).
- runc — the OCI-compliant runtime that actually creates the Linux container using namespaces and cgroups.
1.2 Images, Containers, and Layers
| Concept | What It Is | Analogy |
|---|---|---|
| Image | A read-only template with the app and its dependencies | A class definition |
| Container | A running (or stopped) instance of an image | An object instantiated from a class |
| Layer | A cached filesystem diff created by each Dockerfile instruction | A git commit |
An image is built from stacked layers. When you run a container, Docker adds a thin writable layer on top — that is where your runtime changes go. The image layers underneath stay read-only and are shared across containers.
1.3 Container Lifecycle
created ──▶ running ──▶ paused
│ │
▼ ▼
stopped ◀── (unpause)
│
▼
removed
docker create→ created (filesystem ready, process not started)docker start→ runningdocker pause/docker unpause→ paused / back to runningdocker stop→ stopped (process terminated, filesystem still exists)docker rm→ removed (container deleted)
2. Install Docker Engine
SSH into app-server:
ssh app-server
2.1 Remove old packages
Ubuntu may ship outdated Docker packages. Remove them first:
sudo apt-get remove -y docker.io docker-doc docker-compose docker-compose-v2 \
containerd runc 2>/dev/null || true
2.2 Add the Docker official apt repository
# Install prerequisites
sudo apt-get update
sudo apt-get install -y ca-certificates curl
# Add Docker's official GPG key
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
# Add the repository to apt 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
2.3 Install Docker packages
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io \
docker-buildx-plugin docker-compose-plugin
2.4 Start and enable Docker
sudo systemctl enable --now docker
2.5 Run Docker without sudo
Add your user to the docker group so you don't need sudo for every command:
sudo usermod -aG docker $USER
newgrp docker
2.6 Verify the installation
docker version
Expected output (key lines):
Client: Docker Engine - Community
Version: ...
API version: ...
Server: Docker Engine - Community
Engine:
Version: ...
You should see both Client and Server sections. If the Server section shows an error, the daemon is not running — check sudo systemctl status docker.
Checkpoint:
docker versionshows both Client and Server. You can run Docker commands withoutsudo.
3. Run Your First Container
3.1 Hello World
docker run hello-world
What happens behind the scenes:
- The CLI asks the daemon for the
hello-worldimage. - The image is not found locally, so Docker pulls it from Docker Hub.
- Docker creates a container from the image and runs its entrypoint.
- The program prints a confirmation message and exits.
Expected output (key lines):
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
...
Hello from Docker!
This message shows that your installation appears to be working correctly.
3.2 List containers
docker ps
Nothing shows up — the hello-world container already exited. Add -a to see all containers, including stopped ones:
docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS NAMES
a1b2c3d4e5f6 hello-world "/hello" 30 seconds ago Exited (0) 29 seconds ago ...
3.3 Run Nginx in the background
docker run -d -p 8080:80 --name my-nginx nginx
-d— detached mode (runs in the background)-p 8080:80— map host port 8080 to container port 80--name my-nginx— give the container a human-readable name
3.4 Verify Nginx is serving
curl http://localhost:8080
You should see the Nginx welcome HTML page.
3.5 Read container logs
docker logs my-nginx
You will see the Nginx access log entry from your curl request.
3.6 Shell into the running container
docker exec -it my-nginx bash
-i— interactive (keep STDIN open)-t— allocate a pseudo-TTY
You are now inside the container. Look around:
ls /usr/share/nginx/html/
cat /etc/nginx/nginx.conf
exit
3.7 Stop and remove the container
docker stop my-nginx
docker rm my-nginx
Or in one shot: docker rm -f my-nginx (force-removes even if running).
Checkpoint: You can pull, run, inspect logs, exec into, stop, and remove containers.
4. Images and Layers
4.1 List local images
docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
nginx latest ... ... 187MB
hello-world latest ... ... 13.3kB
- TAG — defaults to
latestif you don't specify one. In production, always pin a specific tag. - SIZE — the uncompressed size of all layers combined.
4.2 Inspect the layer stack
docker history nginx
Each row is one layer. The CREATED BY column shows the Dockerfile instruction that produced it. Layers with 0B size are metadata-only (e.g., CMD, EXPOSE).
4.3 Why layer order matters
Docker caches layers. When you rebuild an image, Docker reuses every layer above the first changed instruction. This means:
- Put instructions that rarely change (OS packages, dependencies) near the top of your Dockerfile.
- Put instructions that change often (your source code) near the bottom.
You will see this in action in the next section.
Note: Layers are read-only. When a container runs, Docker adds a thin writable layer on top. Changes made inside the container (new files, modified files) live only in that writable layer and disappear when the container is removed.
5. Build a Custom Image
You will create a tiny Go HTTP server, package it in a multi-stage Dockerfile, and run it.
5.1 Create the project directory
mkdir -p ~/docker-hello && cd ~/docker-hello
5.2 Write the Go server
Create main.go:
package main
import (
"fmt"
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello from Docker!")
})
fmt.Println("Server listening on :8080")
http.ListenAndServe(":8080", nil)
}
5.3 Write the Dockerfile
# Stage 1: Build
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY main.go .
RUN go build -o hello main.go
# Stage 2: Runtime
FROM alpine:3.19
COPY --from=builder /app/hello /hello
EXPOSE 8080
CMD ["/hello"]
Why multi-stage? The golang image is ~250 MB — it contains the entire Go toolchain. You only need the compiled binary at runtime. The second stage starts from a minimal alpine image (~7 MB) and copies just the binary. Everything from the builder stage is discarded.
5.4 Build the image
docker build -t hello-docker:v1 .
Watch the output — Docker executes each instruction as a separate layer:
Step 1/7 : FROM golang:1.22-alpine AS builder
---> ...
Step 4/7 : RUN go build -o hello main.go
---> Running in ...
Step 5/7 : FROM alpine:3.19
---> ...
Step 7/7 : CMD ["/hello"]
---> ...
Successfully tagged hello-docker:v1
5.5 Run it
docker run -d -p 8080:8080 --name hello hello-docker:v1
curl http://localhost:8080
Expected output:
Hello from Docker!
5.6 Compare image sizes
docker images
| REPOSITORY | TAG | SIZE |
|---|---|---|
| hello-docker | v1 | ~15 MB |
| golang | 1.22-alpine | ~250 MB |
| alpine | 3.19 | ~7 MB |
The multi-stage build kept your final image at ~15 MB instead of ~250 MB. This means faster pulls, less disk usage, and a smaller attack surface.
Checkpoint: You can write a Dockerfile, build a custom image with
docker build, and run it. You understand why multi-stage builds produce smaller images.
6. Inspect and Clean Up
6.1 Inspect a container
docker inspect hello
This returns a large JSON blob. Key fields to look at:
State.Status—running,exited, etc.NetworkSettings.IPAddress— the container's IP on the Docker bridge networkMounts— any volumes attached to the container
You can extract specific fields with Go templates:
docker inspect --format '{{.State.Status}}' hello
docker inspect --format '{{.NetworkSettings.IPAddress}}' hello
6.2 Stop and remove your containers
docker stop hello
docker rm hello
6.3 Remove images you no longer need
docker rmi hello-docker:v1 nginx hello-world
6.4 Clean up dangling resources
docker system prune
This removes:
- Stopped containers
- Networks not used by any container
- Dangling images (untagged layers left from builds)
- Build cache
Tip:
docker system prune -aremoves all unused images, not just dangling ones. Use this when you want to reclaim disk space aggressively, but be aware it will re-download images on next use.
7. Troubleshooting
| Problem | Cause | Fix |
|---|---|---|
Cannot connect to the Docker daemon | Daemon not running or user not in docker group | sudo systemctl start docker and verify groups includes docker |
port is already allocated | Another process is using that host port | sudo lsof -i :8080 to find it, then stop the process or pick a different port |
Build fails on COPY main.go . | main.go not in the build context directory | Make sure you run docker build from the directory containing main.go |
permission denied while trying to connect | Haven't re-logged after usermod -aG docker | Run newgrp docker or log out and back in |
curl: Connection refused after docker run | Forgot the -p flag — EXPOSE alone does not publish ports | Add -p hostPort:containerPort to the docker run command |
8. What You Have Now
| Capability | Verification Command |
|---|---|
| Docker Engine installed and running | docker version |
| Run containers without sudo | docker run hello-world |
| Pull and run images from Docker Hub | docker run -d -p 8080:80 nginx |
| Build custom images from a Dockerfile | docker build -t myapp:v1 . |
| Inspect, log, and exec into containers | docker inspect, docker logs, docker exec |
| Clean up containers, images, and cache | docker system prune |
Next up: Module 02 — Containerize the App — make the Go backend container-ready with environment variables, write a multi-stage Dockerfile, and run it.