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,$REGISTRYor 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 (
CMDvsENTRYPOINT, 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
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()
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"]
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:
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
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:
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.jsonfor custom profiles.AppArmor (Debian/Ubuntu) and SELinux (Fedora/RHEL) apply host MAC policies. You can select profiles via
--security-opt apparmor=profileor--security-opt label=...(SELinux). Consult your OS documentation for appropriate profiles and labels.
Cleanup:
docker rm -f nonroot
Multi‑Architecture Builds
Objectives
Use
docker buildxfor cross‑platform buildsEmulate 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/ENTRYPOINTto 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
bridgewith container names).QEMU/binfmt not installed: run
tonistiigi/binfmthelper and re-bootstrap the builder.