Docker

Docker is a containerization platform that enables developers to package applications and their dependencies into lightweight, portable containers. Containers provide a consistent runtime environment, ensuring that applications run the same way across different systems. Docker simplifies application deployment, scaling, and management, making it easier to build, ship, and run applications in various environments, from local development machines to cloud infrastructure. Several components make up the Docker ecosystem, including the Docker Engine (the runtime), Docker CLI (command-line interface), Docker Hub (a cloud-based registry for sharing images), and Docker Compose (a tool for defining and running multi-container applications). See https://www.docker.com/ for more information.

Alternatively, containerd is an industry-standard core container runtime that provides the essential functionalities needed to run containers. It is designed to be a lightweight, high-performance, and extensible runtime that can be integrated into various container orchestration systems, such as Kubernetes. Containerd handles tasks such as image management, container lifecycle management, and low-level storage and networking. It is often used as the underlying runtime for Docker, but it can also be used independently in other container ecosystems. See https://containerd.io/ for more information.

Environment

1docker version
2docker info                     # quick peek of your environment
3docker run --rm hello-world     # confirm basic run works

If any command fails, ensure the Docker daemon is running and your user has permission to access the Docker socket.

Conventions

  • Shell blocks assume Linux/macOS Bash.

  • Replace $USER, $REGISTRY or similar placeholders with values for your environment.

  • Run commands from a writable working directory (e.g., a separate project folder).

Note

Many examples use small images like alpine or busybox to keep builds fast.

Using Containers

Objectives

  • Pull, run, stop, start, and remove containers

  • Inspect logs, processes, and metadata

  • Map ports, pass environment variables, and manage restart policies

  • Use resource controls

First Runs

 1# Pull an image explicitly
 2docker pull alpine:latest
 3
 4# Run a one-time command
 5docker run --rm alpine:latest echo "Hello from Alpine"
 6
 7# Start an interactive shell
 8docker run -it --name demo-alpine alpine:latest sh
 9
10# Inside container, try a few commands
11uname -a
12cat /etc/os-release
13exit

Validation and Cleanup:

1docker ps -a --filter name=demo-alpine
2docker logs demo-alpine
3docker rm demo-alpine

Ports, Environment, and Restart Policies

Run a tiny HTTP server in BusyBox.

 1# Start a background web server publishing container port 8080 to host port 8080
 2docker run -d --name web1 \
 3    -p 8080:8080 \
 4    -e APP_MESSAGE="Hello from web1" \
 5    busybox:latest sh -c 'echo "$APP_MESSAGE" > index.html && httpd -f -p 8080'
 6
 7# Verify port binding
 8curl -s localhost:8080
 9
10# Restart policy: restart unless stopped
11docker update --restart unless-stopped web1
12docker inspect -f '{{.HostConfig.RestartPolicy.Name}}' web1
13
14# Stop and start again
15docker stop web1 && docker start web1
16curl -s localhost:8080

Validation and Cleanup:

docker port web1
docker rm -f web1

Exec, Copy, Inspect, Resource Limits

 1docker run -d --name cpu-mem-demo alpine:latest sh -c "sleep 3600"
 2
 3# Exec into running container
 4docker exec -it cpu-mem-demo sh -lc "echo inside && ls -la /"
 5
 6# Copy a file into the container
 7echo "sample file" > local.txt
 8docker cp local.txt cpu-mem-demo:/root/in-container.txt
 9docker exec -it cpu-mem-demo sh -lc "cat /root/in-container.txt"
10
11# Inspect metadata
12docker inspect cpu-mem-demo | head -n 30
13
14# Apply resource limits (recreate container)
15docker rm -f cpu-mem-demo
16docker run -d --name cpu-mem-demo \
17    --cpus="0.50" --memory="128m" --pids-limit=128 alpine:latest sleep 3600

Validation and Cleanup:

1# Validation
2docker stats --no-stream cpu-mem-demo
3docker inspect -f '{{.HostConfig.NanoCpus}} {{.HostConfig.Memory}} {{.HostConfig.PidsLimit}}' cpu-mem-demo
4
5# Cleanup resources
6rm -f local.txt
7docker rm -f cpu-mem-demo

Create, Build, and Maintain Images

Objectives

  • Write Dockerfiles (CMD vs ENTRYPOINT, layers, caching, .dockerignore)

  • Build single- and multi-stage images with BuildKit

  • Tag, run, and test images; manage versions and labels

  • Operate a local registry to push/pull

Working with Dockerfile

A typical Docker project structure may look like this:

app/
├─ Dockerfile
├─ .dockerignore
└─ server.py

Create files:

mkdir -p app && cd app
server.py
 1from http.server import BaseHTTPRequestHandler, HTTPServer
 2import os
 3
 4class Handler(BaseHTTPRequestHandler):
 5    def do_GET(self):
 6        msg = os.getenv("APP_MESSAGE", "Hello, Docker!")
 7        self.send_response(200)
 8        self.end_headers()
 9        self.wfile.write(msg.encode("utf-8"))
10
11if __name__ == "__main__":
12    port = int(os.getenv("PORT", "8000"))
13    server = HTTPServer(("0.0.0.0", port), Handler)
14    print(f"Serving on :{port}")
15    server.serve_forever()
Dockerfile
 1FROM python:3.13-slim
 2LABEL org.opencontainers.image.title="simple-http" \
 3        org.opencontainers.image.source="https://example.invalid/demo" \
 4        org.opencontainers.image.description="A tiny HTTP server demo"
 5WORKDIR /app
 6COPY server.py .
 7EXPOSE 8000
 8ENV PORT=8000
 9# Use the exec form to preserve signals and avoid shell quirks
10CMD ["python", "server.py"]
.dockerignore
1__pycache__/
2*.pyc
3.git
4.env
5.DS_Store

Build & run:

1# Enable BuildKit (Optional. Enabled by default on new Docker installs.)
2export DOCKER_BUILDKIT=1
3
4docker build -t simple-http:1.0.0 .
5docker run -d --name simple-http -p 8000:8000 -e APP_MESSAGE="Hello from 1.0.0" simple-http:1.0.0
6curl -s localhost:8000

Validation:

docker image ls simple-http
docker inspect -f '{{.Config.Env}}' simple-http:1.0.0

Cleanup:

docker rm -f simple-http

Multi‑Stage Build and Image Slimming

Refactor to separate build and runtime:

Dockerfile (multi-stage)
 1# syntax=docker/dockerfile:1.19.0
 2FROM python:3.13-slim AS base
 3WORKDIR /app
 4COPY server.py .
 5
 6FROM gcr.io/distroless/python3-debian12 AS runtime
 7WORKDIR /app
 8COPY --from=base /app/server.py /app/server.py
 9ENV PORT=8000
10EXPOSE 8000
11ENTRYPOINT ["/usr/bin/python3", "/app/server.py"]

Build & compare sizes:

docker build -t simple-http:2.0.0 -f Dockerfile .
docker image ls simple-http

Run and validate:

docker run -d --name simple-http -p 8000:8000 -e APP_MESSAGE="Hello, Multi-stage" simple-http:2.0.0
curl -s localhost:8000
docker rm -f simple-http

Tagging, Labels, Digests, Healthcheck

Dockerfile (add healthcheck)
1FROM python:3.13-slim
2WORKDIR /app
3COPY server.py .
4ENV PORT=8000
5EXPOSE 8000
6HEALTHCHECK --interval=10s --timeout=2s --retries=3 \
7    CMD python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000', timeout=1).read() or exit(1)"
8CMD ["python", "server.py"]
1docker build -t simple-http:2.1.0 .
2docker tag simple-http:2.1.0 simple-http:latest
3docker run -d --name simple-http-healthcheck -p 8000:8000 simple-http:latest
4sleep 2
5docker inspect -f '{{.State.Health.Status}}' simple-http-healthcheck
6docker inspect --format='{{index .RepoDigests 0}}' simple-http:2.1.0
7docker rm -f simple-http-healthcheck

Local Registry for Push/Pull

Start a local registry:

docker run -d --name registry -p 5000:5000 registry:2

Tag & push:

docker tag simple-http:2.1.0 localhost:5000/simple-http:2.1.0
docker push localhost:5000/simple-http:2.1.0

Test pull:

docker rmi simple-http:2.1.0
docker pull localhost:5000/simple-http:2.1.0

Cleanup:

docker rm -f registry

Note

Pushing to Docker Hub or another remote registry follows the same pattern after authenticating with docker login and tagging with the registry hostname.

Advanced Storage, Networking, and Security

Objectives

  • Persist data with volumes, use bind mounts, tmpfs, and backups

  • Build user‑defined bridge networks, DNS-based service discovery, and port publishing

  • Apply security hardening: non‑root users, capabilities, read‑only rootfs, seccomp/AppArmor/SELinux basics, and no-new-privileges

Volumes, Bind Mounts, Backups

Create and use a named volume:

1docker volume create appdata
2docker run -d --name vol-demo -v appdata:/data alpine:latest sh -c 'echo "persist me" > /data/file.txt; sleep 3600'
3docker exec vol-demo cat /data/file.txt
4docker rm -f vol-demo
5
6# Re-attach the same volume to verify persistence
7docker run --rm -v appdata:/data alpine:latest cat /data/file.txt

Bind mounts (host path):

1mkdir -p "$(pwd)/hostdir"
2echo "hello from host" > "$(pwd)/hostdir/host.txt"
3docker run --rm \
4    --mount type=bind,src="$(pwd)/hostdir",dst=/mnt,readonly \
5    alpine:latest sh -lc 'echo "Listing:" && ls -la /mnt && echo "Try write:" && (echo hi > /mnt/x || echo "write blocked (as expected)")'

Tmpfs (in‑memory):

docker run --rm --tmpfs /tmp:rw,size=64m alpine:latest sh -lc 'mount | grep /tmp && echo data > /tmp/inmem && ls -la /tmp'

Backup/restore a volume (using a throwaway helper):

1# Backup appdata to a tar stream into host file
2docker run --rm -v appdata:/data -v "$(pwd)":/backup alpine:latest \
3    sh -lc 'tar -C /data -czf /backup/appdata.tgz .'
4
5# Restore into a fresh volume
6docker volume create appdata2
7docker run --rm -v appdata2:/data -v "$(pwd)":/backup alpine:latest \
8    sh -lc 'tar -C /data -xzf /backup/appdata.tgz && ls -la /data'

Cleanup:

docker volume rm appdata appdata2
rm -rf hostdir appdata.tgz

User‑Defined Networks and DNS

Create a user-defined bridge network and connect services by name:

 1docker network create appnet
 2
 3# Service A: a tiny key-value service using BusyBox HTTPD on the appnet
 4docker run -d --name svc-a --network appnet busybox:1.36 sh -c 'echo "svc-a" > index.html && httpd -f -p 8080'
 5
 6# Service B: curl Service A by its container name (DNS from user-defined network)
 7docker run --rm --network appnet curlimages/curl:8.9.1 curl -s http://svc-a:8080
 8
 9# Publish a separate service to the host on :8081 (initially on default bridge)
10docker run -d --name svc-a-pub -p 8081:8080 busybox:1.36 sh -c 'echo "svc-a-public" > index.html && httpd -f -p 8080'
11curl -s http://127.0.0.1:8081

Explore networking metadata:

docker network inspect appnet
docker port svc-a-pub

Connect / disconnect svc-a-pub to/from appnet:

docker network connect appnet svc-a-pub
docker network disconnect appnet svc-a-pub

Cleanup:

docker rm -f svc-a svc-a-pub
docker network rm appnet

Note

host and none networks have special behavior. User-defined bridge networks provide built-in DNS-based service discovery without extra tooling.

Security Hardening Essentials

Run as non‑root (build-time) and drop capabilities:

Dockerfile (non-root user)
1FROM alpine:latest
2RUN adduser -D appuser
3USER appuser
4WORKDIR /home/appuser
5COPY --chown=appuser:appuser . /home/appuser
6CMD ["sh", "-lc", "id && sleep 3600"]
1docker build -t security-demo:nonroot .
2docker run -d --name nonroot security-demo:nonroot
3docker exec nonroot id

Drop Linux capabilities by default and selectively add:

1# Try to run ping (needs CAP_NET_RAW)
2docker run --rm --cap-drop ALL alpine:latest sh -lc 'apk add --no-cache iputils >/dev/null && ping -c1 1.1.1.1 || echo "No CAP_NET_RAW -> ping fails (expected)"'
3
4# Add CAP_NET_RAW back
5docker run --rm --cap-drop ALL --cap-add NET_RAW alpine:latest sh -lc 'apk add --no-cache iputils >/dev/null && ping -c1 -W1 1.1.1.1 && echo "Ping works with NET_RAW"'

Read‑only root filesystem and writable mounts:

docker run --rm --read-only --tmpfs /tmp alpine:latest sh -lc 'echo ok > /tmp/x && echo "wrote to tmpfs ok"; (echo fail > /etc/x || echo "rootfs is read-only (expected)")'

No new privileges:

docker run --rm --security-opt=no-new-privileges alpine:latest sh -lc 'echo "no-new-privileges active"'

Optional: Custom seccomp / AppArmor / SELinux

  • Default seccomp profile already blocks many risky syscalls. You can pass --security-opt seccomp=/path/profile.json for custom profiles.

  • AppArmor (Debian/Ubuntu) and SELinux (Fedora/RHEL) apply host MAC policies. You can select profiles via --security-opt apparmor=profile or --security-opt label=... (SELinux). Consult your OS documentation for appropriate profiles and labels.

Cleanup:

docker rm -f nonroot

Multi‑Architecture Builds

Objectives

  • Use docker buildx for cross‑platform builds

  • Emulate architectures via binfmt_misc (QEMU)

  • Build, push, and verify multi‑arch images

Enable Buildx and Binfmt

 1# Ensure Buildx is available
 2docker buildx version
 3
 4# Ensure a builder exists (create if needed)
 5docker buildx ls
 6docker buildx create --name multi --use || docker buildx use multi
 7docker buildx inspect --bootstrap
 8
 9# (Optional) Install QEMU emulation for cross-building
10docker run --privileged --rm tonistiigi/binfmt --install all
11docker buildx inspect --bootstrap

Build for linux/amd64 and linux/arm64

Use the app from Module 2 or create a fresh minimal sample.

 1cd app  # if not already there
 2
 3# Ensure a local registry is running for pushing the multi-arch manifest
 4docker run -d --name registry -p 5000:5000 registry:2
 5
 6# Build a multi-arch image and push to the local registry
 7docker buildx build \
 8    --platform linux/amd64,linux/arm64 \
 9    -t localhost:5000/simple-http:multi \
10    --push .

Inspect the manifest:

docker buildx imagetools inspect localhost:5000/simple-http:multi

Optionally, pull a specific platform (on a multi‑arch host):

docker pull --platform linux/arm64 localhost:5000/simple-http:multi
docker image rm localhost:5000/simple-http:multi

Cleanup:

docker rm -f registry
# keep the builder 'multi' for future use or remove with:
# docker buildx rm multi

Maintenance and Housekeeping

Objectives

  • Keep images current, manage tags, and rebuild with cache awareness

  • Export, import, and inspect images; prune unused resources

  • Diagnose disk usage and layer sharing

Upgrades, Retagging, and Rebuilds

 1# Inspect base image and digests
 2docker history simple-http:2.1.0
 3docker inspect --format '{{.RepoDigests}}' simple-http:2.1.0
 4
 5# Retag for a new release
 6docker tag simple-http:2.1.0 simple-http:2.1.1
 7
 8# Rebuild with cache (e.g., after code change)
 9touch server.py
10docker build -t simple-http:2.1.1 .
11docker image ls simple-http

Save/Load and Export/Import

 1# Save image as tarball and load elsewhere
 2docker save simple-http:2.1.1 | gzip > simple-http_2.1.1.tar.gz
 3docker image rm simple-http:2.1.1
 4gunzip -c simple-http_2.1.1.tar.gz | docker load
 5
 6# Export a container filesystem (no image metadata) and import
 7docker run --name export-demo simple-http:2.1.1 sh -lc 'echo "artifact" > /app/art.txt; sleep 1'
 8docker export export-demo | gzip > export-demo.tar.gz
 9docker rm -f export-demo
10gunzip -c export-demo.tar.gz | docker import - import:v1
11docker run --rm import:v1 sh -lc 'ls -la /app'

Disk Usage and Pruning

1docker system df
2docker image prune -f
3docker container prune -f
4docker volume prune -f
5docker builder prune -f

Warning

prune deletes unused resources. Ensure you don’t need dangling images/volumes before pruning.

Best Practices & Cheat‑Sheet

General

  • Prefer minimal bases (alpine, distroless) and multi‑stage builds to reduce image size and attack surface.

  • Pin versions (and optionally digests) for reproducible builds.

  • Use .dockerignore to keep contexts small. Avoid copying your entire repo when only a subdir is needed:

    COPY --link src/ /app/          # when using BuildKit
    
  • Order Dockerfile steps to maximize cache hits (install dependencies before copying fast‑changing app code).

  • Use exec form for CMD/ENTRYPOINT to preserve signals and avoid shell interpolation pitfalls.

  • Emit logs to stdout/stderr; don’t write logs to files inside the container.

  • Keep containers immutable and ephemeral; store state in volumes.

Security

  • Do not store passwords or security-sensitive information on containers.

  • Run as non‑root (USER), drop capabilities (--cap-drop ALL; add back only what you need).

  • Use read‑only rootfs, tmpfs for scratch paths, and no‑new‑privileges where possible.

  • Keep images patched and rebuilt regularly; avoid installing extra tools in runtime images.

  • Prefer distroless or slim runtimes and fetch debug shells only in a debug build stage.

Networking & Storage

  • Use user-defined bridge networks for isolation and DNS-based discovery.

  • Map ports deliberately; avoid exposing unnecessary ports.

  • Persist data to named volumes; use bind mounts for local dev only.

  • Use tmpfs for sensitive ephemeral data to keep it in memory.

Multi‑Arch & Build

  • Use BuildKit and buildx for parallel, multi‑platform builds and advanced features (--platform, inline cache, provenance/SBOM as supported by your version).

  • Test images on target platforms and verify manifests with docker buildx imagetools inspect.

Operational Tips

  • Label images using OCI labels (org.opencontainers.image.*) for traceability.

  • Use healthchecks for better container lifecycle signals.

  • Monitor resource limits (CPU/mem/PIDs) and adapt to workload needs.

  • Regularly prune unused artifacts and audit disk usage with docker system df.

Troubleshooting

  • Port already in use: choose another host port (e.g., -p 8082:8080) or stop conflicting process.

  • Permission denied on bind mount: check path exists and permissions; on SELinux systems, consider adding context options (e.g., :Z).

  • DNS resolution between containers works only on user-defined networks (not the default bridge with container names).

  • QEMU/binfmt not installed: run tonistiigi/binfmt helper and re-bootstrap the builder.