Skip to main content

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

ConceptWhat It IsAnalogy
ImageA read-only template with the app and its dependenciesA class definition
ContainerA running (or stopped) instance of an imageAn object instantiated from a class
LayerA cached filesystem diff created by each Dockerfile instructionA 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 createcreated (filesystem ready, process not started)
  • docker startrunning
  • docker pause / docker unpausepaused / back to running
  • docker stopstopped (process terminated, filesystem still exists)
  • docker rmremoved (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 version shows both Client and Server. You can run Docker commands without sudo.


3. Run Your First Container

3.1 Hello World

docker run hello-world

What happens behind the scenes:

  1. The CLI asks the daemon for the hello-world image.
  2. The image is not found locally, so Docker pulls it from Docker Hub.
  3. Docker creates a container from the image and runs its entrypoint.
  4. 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 latest if 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:

~/docker-hello/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

~/docker-hello/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
REPOSITORYTAGSIZE
hello-dockerv1~15 MB
golang1.22-alpine~250 MB
alpine3.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.Statusrunning, exited, etc.
  • NetworkSettings.IPAddress — the container's IP on the Docker bridge network
  • Mounts — 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 -a removes 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

ProblemCauseFix
Cannot connect to the Docker daemonDaemon not running or user not in docker groupsudo systemctl start docker and verify groups includes docker
port is already allocatedAnother process is using that host portsudo 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 directoryMake sure you run docker build from the directory containing main.go
permission denied while trying to connectHaven't re-logged after usermod -aG dockerRun newgrp docker or log out and back in
curl: Connection refused after docker runForgot the -p flag — EXPOSE alone does not publish portsAdd -p hostPort:containerPort to the docker run command

8. What You Have Now

CapabilityVerification Command
Docker Engine installed and runningdocker version
Run containers without sudodocker run hello-world
Pull and run images from Docker Hubdocker run -d -p 8080:80 nginx
Build custom images from a Dockerfiledocker build -t myapp:v1 .
Inspect, log, and exec into containersdocker inspect, docker logs, docker exec
Clean up containers, images, and cachedocker 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.