Docker Full Walkthrough
What is Docker?
Docker is a containerisation platform. Instead of shipping your app + a full VM, you ship a container — a lightweight, isolated process that packages your app and its dependencies but shares the host OS kernel.
Key concepts:
| Term | What it is |
|---|
| Image | Read-only blueprint (like a class) |
| Container | Running instance of an image (like an object) |
| Dockerfile | Recipe to build an image |
| Registry | Remote store for images (Docker Hub, GHCR, ECR) |
| Volume | Persistent storage that outlives containers |
| Network | Virtual network connecting containers |
Installation
Linux (Ubuntu/Debian)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| # Remove old versions
sudo apt remove docker docker-engine docker.io containerd runc
# Install via convenience script
curl -fsSL https://get.docker.com | sh
# Add your user to docker group (no sudo needed after logout/login)
sudo usermod -aG docker $USER
# Start and enable the daemon
sudo systemctl enable --now docker
# Verify
docker --version
docker run hello-world
|
Check installation
1
2
3
| docker version # client + server version
docker info # system-wide info (storage driver, network, etc.)
docker system df # disk usage by images, containers, volumes
|
Images
Pulling images
1
2
3
4
| docker pull ubuntu # latest tag
docker pull ubuntu:22.04 # specific version
docker pull nginx:alpine # alpine variant (smaller)
docker pull ghcr.io/user/myapp:v1.2 # from GitHub Container Registry
|
Listing and inspecting images
1
2
3
4
5
6
| docker images # list local images
docker images -a # include intermediate layers
docker image ls --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}"
docker inspect ubuntu:22.04 # full JSON metadata
docker history nginx:alpine # layers that built the image
|
Removing images
1
2
3
4
| docker rmi nginx # remove by name
docker rmi abc123 # remove by image ID
docker image prune # remove dangling (untagged) images
docker image prune -a # remove all unused images
|
Containers
Running containers
1
2
3
4
5
6
7
8
9
| docker run nginx # foreground, blocking
docker run -d nginx # detached (background)
docker run -d -p 8080:80 nginx # map host:8080 → container:80
docker run -d --name webserver nginx # give it a name
docker run -it ubuntu:22.04 bash # interactive terminal
docker run --rm alpine echo "hello" # remove container after it exits
docker run -e MY_VAR=hello nginx # set environment variable
docker run -v /host/path:/container/path nginx # bind mount a volume
docker run --memory="256m" --cpus="1.0" nginx # resource limits
|
Listing containers
1
2
3
4
| docker ps # running containers
docker ps -a # all containers (including stopped)
docker ps -q # only container IDs (useful for scripting)
docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
|
Managing container lifecycle
1
2
3
4
5
6
7
8
| docker stop webserver # graceful stop (SIGTERM → wait → SIGKILL)
docker start webserver # start a stopped container
docker restart webserver # stop + start
docker kill webserver # immediate kill (SIGKILL)
docker pause webserver # freeze (SIGSTOP)
docker unpause webserver # resume
docker rm webserver # delete stopped container
docker rm -f webserver # force delete (even if running)
|
Interacting with running containers
1
2
3
4
5
6
7
8
9
| docker exec -it webserver bash # open a shell inside a running container
docker exec webserver cat /etc/nginx.conf # run a single command
docker logs webserver # stdout/stderr logs
docker logs -f webserver # follow logs (like tail -f)
docker logs --tail 50 webserver # last 50 lines
docker logs --since 1h webserver # logs from the last hour
docker top webserver # processes running inside container
docker stats # live resource usage (CPU, RAM, net, disk)
docker stats --no-stream # one-shot snapshot
|
Copying files
1
2
| docker cp file.txt webserver:/tmp/file.txt # host → container
docker cp webserver:/etc/nginx.conf ./ # container → host
|
Dockerfile
A Dockerfile is a script that builds an image layer by layer.
Minimal example
1
2
3
| FROM ubuntu:22.04
RUN apt-get update && apt-get install -y curl
CMD ["bash"]
|
Full example with best practices
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| # --- Build stage ---
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
# --- Runtime stage ---
FROM node:20-alpine
WORKDIR /app
# Create non-root user
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
COPY --from=builder /app/node_modules ./node_modules
COPY . .
# Set ownership
RUN chown -R appuser:appgroup /app
USER appuser
EXPOSE 3000
ENV NODE_ENV=production
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD wget -qO- http://localhost:3000/health || exit 1
CMD ["node", "server.js"]
|
Dockerfile instructions
| Instruction | Purpose |
|---|
FROM | Base image (every Dockerfile starts with this) |
RUN | Execute a command at build time |
COPY | Copy files from host into the image |
ADD | Like COPY but also handles URLs and tar extraction |
WORKDIR | Set working directory for subsequent instructions |
ENV | Set environment variables |
ARG | Build-time variable (not available at runtime) |
EXPOSE | Document which port the container listens on |
CMD | Default command when container starts (can be overridden) |
ENTRYPOINT | Main command (CMD becomes arguments to it) |
USER | Switch to a non-root user |
VOLUME | Declare a mount point for volumes |
HEALTHCHECK | Define how Docker checks if the container is healthy |
LABEL | Add metadata key-value pairs |
CMD vs ENTRYPOINT
1
2
3
4
5
6
7
8
9
| # CMD alone — fully overridden by docker run args
CMD ["nginx", "-g", "daemon off;"]
# ENTRYPOINT alone — not overridden, docker run args appended
ENTRYPOINT ["nginx"]
# Combined — ENTRYPOINT is the executable, CMD is default args
ENTRYPOINT ["nginx"]
CMD ["-g", "daemon off;"]
|
Build an image
1
2
3
4
| docker build -t myapp:latest . # build from current directory
docker build -t myapp:v1.0 -f Dockerfile.prod . # specify Dockerfile
docker build --no-cache -t myapp:latest . # ignore layer cache
docker build --build-arg VERSION=1.2 -t myapp . # pass build args
|
Multi-stage builds
Multi-stage builds keep the final image small by not including build tools:
1
2
3
4
5
6
7
8
| FROM golang:1.22 AS builder
WORKDIR /src
COPY . .
RUN go build -o /bin/app
FROM scratch # empty base image
COPY --from=builder /bin/app /app
CMD ["/app"]
|
The final image contains only the compiled binary — no Go toolchain.
Volumes
Volumes persist data beyond the container’s lifecycle and are managed by Docker.
Types of storage
| Type | Description |
|---|
| Volume | Managed by Docker, stored in /var/lib/docker/volumes/ |
| Bind mount | Maps a host path directly into the container |
| tmpfs mount | In-memory only, not persisted |
Volume commands
1
2
3
4
5
| docker volume create mydata # create a named volume
docker volume ls # list volumes
docker volume inspect mydata # details (mount point, driver)
docker volume rm mydata # delete a volume
docker volume prune # delete all unused volumes
|
Using volumes
1
2
3
4
5
6
7
8
9
10
11
| # Named volume (recommended)
docker run -d -v mydata:/var/lib/postgresql/data postgres
# Bind mount (good for dev — live code reload)
docker run -d -v $(pwd)/src:/app/src myapp
# Read-only bind mount
docker run -d -v $(pwd)/config:/etc/app:ro myapp
# tmpfs (in-memory, for secrets or temp data)
docker run --tmpfs /tmp:size=100m myapp
|
Networking
Default networks
1
2
| docker network ls # list all networks
docker network inspect bridge # inspect the default bridge network
|
| Network | Description |
|---|
bridge | Default. Containers talk via IP, isolated from host |
host | Container shares host’s network stack (no isolation) |
none | No networking |
| Custom bridge | Like bridge but with automatic DNS by container name |
Custom networks (always use these)
1
2
3
4
| docker network create mynet # create a bridge network
docker network create --driver bridge --subnet 172.20.0.0/16 mynet
docker network rm mynet # delete
docker network prune # remove unused networks
|
Connect containers to a network:
1
2
| docker run -d --network mynet --name db postgres
docker run -d --network mynet --name app myapp
|
On a custom bridge network, containers can reach each other by name (db, app) — no need for IPs.
1
2
| docker network connect mynet webserver # add a running container to a network
docker network disconnect mynet webserver # remove it
|
Port publishing
1
2
3
4
| docker run -p 8080:80 nginx # host:8080 → container:80
docker run -p 127.0.0.1:8080:80 nginx # bind only to localhost
docker run -P nginx # auto-assign host ports for all EXPOSE'd ports
docker port webserver # show port mappings
|
Docker Compose
Docker Compose manages multi-container applications with a YAML file.
docker-compose.yml structure
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
| services:
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: appuser
POSTGRES_PASSWORD: secret
POSTGRES_DB: myapp
volumes:
- pgdata:/var/lib/postgresql/data
restart: unless-stopped
backend:
build: ./backend
environment:
DATABASE_URL: postgresql://appuser:secret@db:5432/myapp
ports:
- "8000:8000"
depends_on:
db:
condition: service_healthy
restart: unless-stopped
nginx:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
depends_on:
- backend
volumes:
pgdata:
|
Docker Compose commands
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| docker compose up # start all services (foreground)
docker compose up -d # start in background
docker compose up --build # force rebuild images
docker compose down # stop and remove containers
docker compose down -v # also remove volumes
docker compose ps # status of all services
docker compose logs # all service logs
docker compose logs -f backend # follow a specific service
docker compose exec backend bash # open shell in a service
docker compose run backend pytest # run a one-off command
docker compose pull # pull latest images
docker compose restart backend # restart a specific service
docker compose stop # stop without removing
docker compose start # start stopped services
|
Environment variables in Compose
1
2
3
4
5
6
7
8
9
10
11
12
| # Inline
environment:
DEBUG: "true"
PORT: "8000"
# From .env file (auto-loaded from same directory)
env_file:
- .env
# Substitute from shell
environment:
API_KEY: ${API_KEY}
|
Healthchecks in Compose
1
2
3
4
5
6
7
8
9
| services:
db:
image: postgres:16
healthcheck:
test: ["CMD-SHELL", "pg_isready -U appuser"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
|
Docker Registry
Docker Hub
1
2
3
4
5
| docker login # login to Docker Hub
docker login ghcr.io # login to GitHub Container Registry
docker tag myapp:latest username/myapp:latest
docker push username/myapp:latest # push to Docker Hub
docker pull username/myapp:latest # pull from Docker Hub
|
Run a local registry
1
2
3
4
5
6
7
8
| docker run -d -p 5000:5000 --name registry registry:2
# Tag and push to local registry
docker tag myapp localhost:5000/myapp:latest
docker push localhost:5000/myapp:latest
# Pull from local registry
docker pull localhost:5000/myapp:latest
|
System Cleanup
Docker accumulates a lot of disk space over time.
1
2
3
4
5
6
7
8
9
| docker system df # show disk usage breakdown
docker system prune # remove stopped containers, unused networks, dangling images
docker system prune -a # also remove unused images (not just dangling)
docker system prune -a --volumes # everything including volumes (destructive!)
docker container prune # remove all stopped containers
docker image prune -a # remove all unused images
docker volume prune # remove unused volumes
docker network prune # remove unused networks
|
Dockerfile Best Practices
Layer caching — Docker caches each layer. Put things that change least at the top:
1
2
3
4
5
6
7
8
| # Good — dependencies change less often than source code
COPY package.json ./
RUN npm install
COPY . . # source code copied last
# Bad — cache busted on every source change
COPY . .
RUN npm install
|
Combine RUN commands to reduce layers:
1
2
3
4
5
6
7
8
9
| # Bad — 3 layers
RUN apt-get update
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*
# Good — 1 layer
RUN apt-get update \
&& apt-get install -y curl \
&& rm -rf /var/lib/apt/lists/*
|
Use .dockerignore to exclude files from build context:
1
2
3
4
5
| node_modules
.git
.env
*.log
dist
|
Never run as root in production — always add and switch to a non-root user.
Use specific tags — never FROM ubuntu:latest in production. Pin to ubuntu:22.04.
Quick Reference
| Task | Command |
|---|
| Pull an image | docker pull nginx:alpine |
| Run a container | docker run -d -p 8080:80 nginx |
| Open a shell | docker exec -it name bash |
| View logs | docker logs -f name |
| Stop container | docker stop name |
| Remove container | docker rm name |
| List images | docker images |
| Remove image | docker rmi nginx |
| Build image | docker build -t myapp . |
| Push image | docker push user/myapp |
| Create volume | docker volume create mydata |
| Create network | docker network create mynet |
| Compose up | docker compose up -d |
| Compose logs | docker compose logs -f |
| Compose down | docker compose down |
| Clean everything | docker system prune -a |
| Disk usage | docker system df |