Jump straight into practice questions
Scenario-based DCA questions covering every exam domain — free, no signup required.
DCA Exam Snapshot
Exam Domain Weights
Key Concept: Containers ≠ Lightweight VMs
The single biggest mental-model fix the DCA exam pushes is that containers are processes, not tiny machines. There is no guest kernel, no hypervisor, no boot sequence — a container is one or more host processes wrapped in Linux namespaces (isolation) and cgroups (resource limits), reading from a layered read-only filesystem (OverlayFS). Every DCA security, networking, and storage decision flows from this. If you ever catch yourself thinking "the container's OS" — stop. The host kernel is the container's kernel.
Learn Docker on the go
Tune in to multi-stage build walkthroughs, Swarm vs Kubernetes comparisons, DCA exam strategy, and container security deep-dives. New episodes every week — perfect for commutes and gym sessions.
Listen on SpotifyCourse content
6 modules · ~25 hours
Each module maps to one DCA exam domain. Work through them in order — Module 1 (Architecture) and Module 2 (Images) build the foundation every later module assumes. Module 5 (Swarm, 25%) is the heaviest single domain on the exam and the most common place candidates lose points on the clock.
Container Fundamentals & Docker Architecture3 lessons
Before any CLI flag or Dockerfile instruction, you need the mental model: what a container actually is, who runs it, and which daemon talks to which runtime. Roughly 15% of DCA questions live here under "Installation & Configuration", and the wrong mental model causes mistakes in every later domain.
📖 Read in-depth chapter ▾
A VM virtualises hardware: the hypervisor lies to a guest OS about CPUs, NICs, and disks, and that guest boots a full kernel. A container virtualises only what userland sees of the host — a process and its children get their own filesystem, network interface, PID list, hostname, and user IDs while still running on the host's single kernel. Faster startup, smaller footprint, but a thinner blast wall: a container escape is a host escape.
- Linux namespaces — kernel feature that isolates a process's view of the system. Docker uses six:
pid(process IDs),net(network interfaces, routes, iptables),mnt(mount points),uts(hostname, domain),ipc(System V IPC, POSIX message queues),user(UID/GID remap). - Control groups (cgroups v1/v2) — limit and account CPU, memory, block I/O, network bandwidth, PIDs.
docker run --memory 512m --cpus 1.5writes to/sys/fs/cgroup/...on the host. - Union / layered filesystem — every image layer is a read-only directory; the container's writable layer is overlaid on top. Default driver on Linux is
overlay2; layers are content-addressed by SHA256 digest. - Capabilities — Linux splits root power into ~40 capabilities (
CAP_NET_ADMIN,CAP_SYS_ADMIN, …); Docker drops most by default and runs containers with a minimal set. This is why a container "root" cannot reboot the host. - What containers do NOT virtualise — the kernel, kernel modules, the time/date clock, sysctls (mostly), or hardware. A bad host kernel update can break every container at once.
Run docker run --rm -it alpine sh, then inside the shell: ps -ef shows your shell as PID 1 (PID namespace), ip addr shows only the container's eth0 (network namespace), hostname shows the container ID (UTS namespace), and cat /etc/os-release reports Alpine even though your host is Ubuntu (mnt namespace + image rootfs). Run uname -r in both the container and the host — same kernel version. That single command sums up the container model.
"Docker" is not one binary — it is a small stack of cooperating processes. The exam asks who talks to whom, who actually creates a container, and what survives a daemon restart. Get this graph right and half the troubleshooting questions answer themselves.
- Docker CLI (
docker) — thin client that speaks the Docker REST API to the daemon over the Unix socket/var/run/docker.sock(or TCP when configured). Stateless. - Docker daemon (
dockerd) — long-running server. Exposes the REST API, manages images / networks / volumes / Swarm state, and delegates container lifecycle to containerd. - containerd — the container runtime that pulls images, unpacks them, and manages running containers. Survives
dockerdrestarts (your containers keep running while you upgrade the daemon). - runc — the OCI low-level runtime that actually invokes the kernel syscalls to create the namespaces / cgroups bundle. containerd calls runc per container; runc exits once the container's PID 1 has been started.
- Registry — a separate service (Docker Hub, GHCR, Harbor, ECR, a self-hosted Registry image…) that stores image manifests and layer blobs. Always TLS-fronted in production.
- Object model — Image (read-only template, SHA256 digest), Container (running or stopped instance of an image with its writable layer), Volume (managed persistent data), Network (virtual L2/L3 fabric between containers).
When you run docker run -d nginx: the CLI POSTs to /containers/create on the daemon socket; dockerd checks for the nginx:latest image locally, asks containerd to pull missing layers from the registry; containerd writes the layers to /var/lib/docker/overlay2/; dockerd POSTs /containers/<id>/start; containerd creates an OCI bundle and shells out to runc; runc creates the namespaces + cgroups + writable layer mount, execs nginx as PID 1, then exits. The container keeps running attached to containerd-shim — which is exactly why systemctl restart docker can be safe with live-restore enabled.
docker ps hangs but containers stay up during daemon issues, and why a runc CVE is a critical patch.
daemon.json, logging drivers, live-restore
The DCA exam is loud about daemon configuration: where the file lives, which flags belong there, and how each one changes behaviour. Anything you would otherwise pass to dockerd on the command line goes in /etc/docker/daemon.json and survives upgrades.
- Canonical path —
/etc/docker/daemon.jsonon Linux,%programdata%\docker\config\daemon.jsonon Windows. After editing:systemctl reload dockerfor most keys; restart for storage driver or live-restore changes. - Logging drivers —
json-file(default, withmax-size+max-filerotation),journald,syslog,fluentd,gelf,awslogs,splunk. Set globally indaemon.jsonor per-container with--log-driver. - Live-restore —
"live-restore": truekeeps containers running acrossdockerdrestarts. Required for zero-downtime daemon upgrades but incompatible with Swarm mode. - Storage driver —
"storage-driver": "overlay2"on modern Linux. Changing it is destructive; configure on a fresh node only. - Insecure registries —
"insecure-registries": ["registry.lan:5000"]tells the daemon to skip TLS for a non-HTTPS registry; never use in production. - Default resource limits —
default-ulimits,default-shm-size, plus userns-remap and seccomp profile paths all live in this file.
Production-grade /etc/docker/daemon.json for a non-Swarm host: { "log-driver": "json-file", "log-opts": { "max-size": "10m", "max-file": "3" }, "storage-driver": "overlay2", "live-restore": true, "default-ulimits": { "nofile": { "Name": "nofile", "Hard": 65536, "Soft": 65536 } }, "userns-remap": "default" }. After systemctl reload docker, every new container writes capped, rotated JSON logs, can survive daemon restarts, has a sane file-descriptor limit, and runs with its in-container root mapped to an unprivileged host UID — three common DCA exam topics covered by one config block.
/etc/docker/daemon.json is the canonical place for production daemon config — logging rotation, live-restore, storage driver, default ulimits, userns-remap. Memorise the JSON keys (they are the exact CLI flag names without the leading --) and the reload-vs-restart rules. The exam tests both.
Images & Dockerfile Mastery3 lessons
Image Creation, Management & Registry is exam domain 2 (20%) — the second heaviest after Swarm. Every Dockerfile instruction has a layer-cache implication, every push has a registry-auth implication, and the exam tests both at the same time. Three lessons cover instruction semantics, multi-stage builds and layer caching, and registry management end-to-end.
📖 Read in-depth chapter ▾
The DCA exam loves to weaponise pairs of instructions that look identical but behave differently. CMD vs ENTRYPOINT, COPY vs ADD, ARG vs ENV — the question is rarely "what does X do" and almost always "given this Dockerfile, what does docker run produce?". Learn each pair with its exec form and you will read every question correctly.
- FROM — required first instruction (or after
ARGfor multi-platform). Pin to a digest in production:FROM nginx@sha256:abc…. - RUN — executes during build, creates a new layer per instruction. Chain with
&&+\to keep the number of layers low. - COPY vs ADD — both copy files into the image.
ADDadditionally auto-extracts local tar archives and supports URLs. Default toCOPY; useADDonly when you need extraction or URL fetch. - ARG vs ENV —
ARGis build-time only (passed with--build-arg, gone at runtime);ENVpersists into the image metadata and is visible todocker run. Both can be referenced in later RUN steps. - CMD — default arguments for the container. Overridden by any positional args after
docker run image. Exec form:CMD ["nginx", "-g", "daemon off;"]. - ENTRYPOINT — the executable. Combined with CMD, ENTRYPOINT receives
CMDas its arguments.docker run --entrypointoverrides it; CMD args are appended to ENTRYPOINT. - EXPOSE — pure documentation. Does not publish the port; you still need
-pondocker runor a Swarm--publish. - WORKDIR / USER / HEALTHCHECK / VOLUME — set working directory, runtime UID, container-level healthcheck command, declared mount points (auto-creates anonymous volumes if not mounted).
- Shell vs exec form — shell form (
CMD echo hi) wraps the command in/bin/sh -c; exec form (CMD ["echo", "hi"]) execs directly. Exec form is the safe default — it forwards signals (sodocker stopreaches your process) and avoids surprises with quoting.
Given FROM alpine\nENTRYPOINT ["echo"]\nCMD ["hello"]: docker run img prints hello (ENTRYPOINT + default CMD), docker run img world prints world (CMD is replaced, ENTRYPOINT stays), docker run --entrypoint ls img -la / runs ls -la / (ENTRYPOINT replaced, CMD discarded). This is the exam's favourite four-line trap — read every Dockerfile question first as "what is ENTRYPOINT", second as "what is CMD", then compose the answer.
Multi-stage builds and .dockerignore are the two cheapest wins in image quality. Multi-stage lets you compile in a fat image and ship from a thin one; cache ordering lets a one-line code change rebuild in seconds instead of minutes. The DCA exam phrases both as "which Dockerfile is correctly optimised?".
- Multi-stage syntax —
FROM golang:1.22 AS builderopens a build stage;FROM alpinestarts the final stage;COPY --from=builder /app/bin .moves only the compiled artefact across. Only the last stage is the published image (unless--targetis used). - Layer cache invariant — a layer is reused only if (a) all prior layers are identical and (b) the instruction's bytes are identical. The first changed instruction invalidates every layer below it.
- Order rule — put rarely-changing instructions first (base image, apt-get install, npm config), frequently-changing instructions last (copy source code, build). For Node:
COPY package*.json .→RUN npm ci→COPY . .→RUN npm run build. - .dockerignore — controls what enters the build context (sent to the daemon). Always exclude
.git/,node_modules/,*.log, secrets — keeps builds fast and avoids leaking files into images. - BuildKit — modern builder (enabled by
DOCKER_BUILDKIT=1or by default on newer Docker Desktop). Parallel stages, secret mounts (--mount=type=secret,id=mykey), cache mounts (--mount=type=cache,target=/root/.npm), and reproducible builds. - Distroless / scratch — final stages can be
FROM gcr.io/distroless/staticorFROM scratchfor the smallest, most-secure runtime images. Statically compiled Go binaries fit inFROM scratch.
Production multi-stage Dockerfile for a Go service: FROM golang:1.22 AS build\nWORKDIR /src\nCOPY go.mod go.sum ./\nRUN go mod download\nCOPY . .\nRUN CGO_ENABLED=0 go build -o /app -ldflags '-s -w' ./cmd/svc\n\nFROM gcr.io/distroless/static:nonroot\nCOPY --from=build /app /app\nUSER nonroot:nonroot\nENTRYPOINT ["/app"]. Three wins at once: final image < 20 MB, cache-friendly (only go.mod + go.sum trigger a dependency re-fetch), and runs as a non-root user with no shell to drop into after an exploit. This is exactly what DCA "best practice" questions reward.
.dockerignore is the small-image trio. Compile in a builder stage, copy only artefacts into a minimal runtime base, put COPY-source-code after dependency installation, and exclude .git / node_modules / secrets from the build context. Every DCA "which Dockerfile is correct" question is this rule in disguise.
Once images exist they need to move — into a registry, across environments, across teams. The DCA exam tests the difference between tags (mutable) and digests (immutable), the right way to authenticate against a private registry, and the difference between save / load and export / import.
- Tag vs digest — a tag (
myapp:1.0) is a human label that can be moved; a digest (myapp@sha256:abc…) is the content hash of the manifest and can never refer to different bytes. Pin production deployments by digest. - Push flow —
docker tag local registry.com/team/app:1.0+docker login registry.com+docker push registry.com/team/app:1.0. Credentials are stored under~/.docker/config.json(use a credentials helper in production). - Manifests & multi-arch — modern images are described by an OCI manifest. Multi-arch images ship a "manifest list" pointing to per-arch manifests (
amd64,arm64,arm/v7);docker buildx build --platform=linux/amd64,linux/arm64 --pushcreates both at once. - Image cleanup —
docker image prune(dangling untagged),docker image prune -a(all unused),docker system prune(containers + images + networks + build cache). Add--filter "until=24h"to scope. - save / load vs export / import —
docker save myimg | gzip > img.tar.gzpreserves all layers + history (use to move images offline);docker export <container>flattens a container's filesystem to a tar (no layers, no history). Match the right pair:save↔load,export↔import. - Run your own registry — the open-source
registry:2image runs a v2 OCI-compatible registry in seconds. Production setups put it behind TLS, add auth (htpasswd or token service) and persistent storage.
CI pipeline that builds and publishes a multi-arch image: docker buildx create --use --name multiarch\ndocker buildx build --platform linux/amd64,linux/arm64 \ \n -t ghcr.io/acme/svc:1.4.2 \ \n -t ghcr.io/acme/svc@sha256:<digest> \ \n --push .. Production deploy pulls by digest (immutable rollbacks), staging pulls by tag (latest 1.x). When the CFO asks "are we running the audited build?", the answer is kubectl describe pod — the digest in the spec is the proof.
save↔load and export↔import. Always TLS your registry, always prune build cache (docker builder prune + docker image prune -a --filter "until=168h" is a sane weekly cron) and always pin by digest in deployment manifests.
Storage — Volumes, Bind Mounts & tmpfs3 lessons
Storage & Volumes is exam domain 6 (10%) — the smallest weight, but every wrong choice corrupts data. The mount type, the storage driver, and the choice between volume plugins decide whether your database survives a container restart, a node reboot, or a Swarm rescheduling.
📖 Read in-depth chapter ▾
Containers are ephemeral; anything you write to the writable layer dies with docker rm. Docker exposes three mount types to escape this — each with a different durability, portability, and security profile. The exam reliably asks "which mount type for which use case?".
- Named volumes — Docker-managed, stored in
/var/lib/docker/volumes/<name>/_data. Survivedocker rm, portable across hosts viadocker volume create, shareable between containers. The default choice for database data, app state, anything you need to keep. - Bind mounts — map an arbitrary host path into the container. Bypass Docker's volume management; tightly coupled to the host filesystem layout. Right for dev (live code reload), config files (
/etc/nginx/conf.d), and host log forwarding; wrong for portable persistent data. - tmpfs mounts — Linux-only; backed by host RAM, never written to disk. Right for secrets you don't want to land on disk (session tokens, decrypted keys) and ephemeral caches you want gone on container stop.
--mountvs-v—--mountis verbose but explicit (key=value, supports all options);-vis the legacy shorthand. Behavioural difference worth remembering:-v /host:/ctsilently creates/hostif missing;--mount type=bind,src=/host,dst=/cterrors out — which is usually what you want.- Read-only mounts — append
:roorreadonly. Combine with--read-onlyroot filesystem for hardened containers that can only write to declared volumes. - Anonymous volumes — created automatically by
VOLUMEin a Dockerfile ordocker run -v /data. They live forever unless pruned. Always prefer named volumes for anything you want to find later.
Production Postgres with three mount types at once: docker run -d --name pg \ \n --mount type=volume,src=pg-data,dst=/var/lib/postgresql/data \ \n --mount type=bind,src=/etc/pg/postgresql.conf,dst=/etc/postgresql/postgresql.conf,readonly \ \n --mount type=tmpfs,dst=/tmp \ \n postgres:16. Named volume for durable data (survives rm/rebuild), read-only bind mount for the config file (host-managed, version-controlled, container can't modify it), tmpfs for /tmp so query temp files never hit disk. Three mounts, three responsibilities — that's the exam-correct Postgres.
--mount over -v for production — explicit, self-documenting, and surfaces typos as errors instead of creating unexpected paths.
A storage driver is how Docker stacks image layers and the container's writable layer onto a single filesystem view. The choice usually defaults to overlay2 and you should leave it there — but the DCA exam still tests why, and what each alternative trades off.
- overlay2 — the modern default on all major distros. Uses the kernel OverlayFS feature; very low overhead; supports page cache sharing between containers using the same image (RAM saving at scale).
- btrfs / zfs — block-level Copy-on-Write filesystems. Snapshots and quotas come "for free" from the filesystem itself. Right when you already run the host on btrfs/zfs; not worth migrating to.
- devicemapper — historical default on RHEL/CentOS without OverlayFS. Two modes:
loop-lvm(terrible — never in production) anddirect-lvm(acceptable). Being phased out; modern RHEL ships overlay2. - fuse-overlayfs — userspace OverlayFS, used by rootless Docker. Lower performance than kernel overlay2 but enables non-root install.
- Migration cost — changing storage drivers wipes all local images, containers, and writable layers. Always configure on a fresh host; never on a populated one without taking
docker savearchives first. - Inspect what you have —
docker info | grep -i "storage driver". The daemon refuses to start if the configured driver does not match the underlying filesystem.
Migrating a populated host from devicemapper loop-lvm to overlay2: (1) docker save every image you need to keep; (2) systemctl stop docker; (3) back up /var/lib/docker; (4) edit /etc/docker/daemon.json to set "storage-driver": "overlay2"; (5) rm -rf /var/lib/docker/* (the docs are not joking); (6) systemctl start docker + docker load your archives. This is destructive on purpose — Docker has no driver-to-driver layer translator, and the exam likes asking which step is "always" required (steps 5 + load).
overlay2; everything else exists for legacy or filesystem-specific reasons. Storage-driver changes wipe local state — back up images with docker save first. docker info is the single command that tells you which driver is active.
Local named volumes are perfect on a single host. The moment a container reschedules to another Swarm node, it loses its /var/lib/docker/volumes/.... Volume plugins solve this by backing volumes with networked storage (EBS, Ceph, NFS, GlusterFS, Portworx). The exam wants you to know the pattern and the right backup workflow.
- Local driver — the default; volumes live on the local host's filesystem. Cheap and fast, but not multi-host.
- Volume plugins — external drivers registered with Docker (
docker plugin install vendor/plugin). Common: Portworx (block storage with snapshots), NetApp Trident, REX-Ray, NFS vialocal-persist, Ceph RBD via rexray-ceph. - NFS the easy way —
docker volume create --driver local \ \n --opt type=nfs \ \n --opt o=addr=10.0.0.5,rw \ \n --opt device=:/exports/data \ \n shared-data. No plugin needed, kernel NFS client does the work. - Backup recipe — run a helper container that mounts both the volume and a host path, then
tar:docker run --rm \ \n -v mydata:/from -v /backup:/to \ \n alpine tar czf /to/mydata-$(date +%F).tgz -C /from .. Idempotent, works for any volume, and is the canonical "how do I back up a volume?" exam answer. - Restore recipe — same pattern in reverse: mount the empty volume, mount the backup,
tar xzfinto it. - Volume labels —
docker volume create --label env=prod --label backup=daily mydata;docker volume ls --filter label=env=prod. Use labels so your cron + observability tooling can find volumes without hard-coded lists.
Production pattern for a Swarm-deployed Postgres replica: declare volumes: { pg-data: { driver: rexray/ebs, driver_opts: { size: 50, encrypted: true } } } in the compose / stack file. When Swarm reschedules the Postgres task to another node, the EBS volume is detached from the old node and attached to the new one — same bytes, new host. A nightly cron runs the tar czf backup recipe above against the (already-mounted) volume name and ships the archive to S3. The DCA exam phrases this as "how do you make persistent data follow a service across Swarm nodes?" — answer: a volume plugin that supports cross-node attachment.
Docker Networking — Bridge, Overlay, Host, Macvlan3 lessons
Networking is exam domain 4 (15%). The DCA exam tests which driver to pick for which problem, why containers on the default bridge cannot resolve each other by name, and how the Swarm overlay+ingress combo actually load-balances traffic across nodes. Three lessons cover drivers, service discovery, and multi-host overlays.
📖 Read in-depth chapter ▾
Every container connects through a driver. The driver decides whether it gets its own IP, shares the host's network stack, is allowed to talk to anything, or appears on the LAN as a first-class device. Pick the wrong one and you either over-isolate ("why can't my app reach the DB?") or under-isolate ("why is my container's port open to the world?").
- bridge (default for standalone containers) — a virtual L2 switch (
docker0or a custom bridge). Containers get a private IP, traffic to the host is NAT'ed. Defaultdocker0bridge has no embedded DNS; custom bridge networks do — that single difference is on every DCA exam. - host — Linux-only; the container shares the host's network namespace. No isolation, no port mapping needed (the container's port IS the host's port), highest performance. Use sparingly; a misconfigured nginx now binds 0.0.0.0:80 on the host.
- none — only a loopback interface; the container is fully disconnected. Useful for batch jobs that should never see the network.
- macvlan — assigns each container its own MAC address; the container appears as a physical device on the parent LAN. Needs promiscuous mode on the NIC and good parent-interface config. Right for legacy apps that expect direct LAN connectivity (broadcast, DHCP, …); wrong almost everywhere else.
- overlay — multi-host VXLAN network used by Docker Swarm. Covered in Lesson 4.3.
- Inspect everything —
docker network lslists drivers;docker network inspect <net>shows subnet, gateway, IPAM, attached containers. Bookmark this command; it answers most "why can't X reach Y?" questions.
Side-by-side: docker network create app-net + docker run -d --name db --network app-net postgres:16 + docker run -it --network app-net alpine ping db — the ping works, because app-net is a custom bridge with embedded DNS. Drop the --network app-net arguments (so both containers run on docker0) and the same ping db fails with "bad address" — the default bridge has no DNS. This four-command demo is the answer to "why use a custom bridge?" and lives in roughly half of all DCA networking questions.
Once a container has an IP, two questions follow: how does other containers find it (service discovery) and how does the outside world reach it (port publishing). Docker answers both — embedded DNS for inside, -p for outside — and the exam mixes the two on purpose.
- Embedded DNS server —
127.0.0.11inside every container on a custom bridge or overlay network. Resolves container names, service names (Swarm), and network aliases. Forwards external lookups to the host's resolvers. - Container aliases —
docker run --network app-net --network-alias api myapimakes the container reachable as bothmyapiandapi. In compose:networks: app-net: aliases: [api]. - Port publishing forms —
-p 8080:80(host 8080 → ct 80, TCP, all interfaces),-p 127.0.0.1:8080:80(loopback only, the secure default),-p 53:53/udp(explicit UDP),-P(publish everyEXPOSEd port on random host ports). - EXPOSE ≠ publish —
EXPOSEin a Dockerfile is metadata; it does not open the port to the host. Only-p/-P/ Swarm--publishcreates the port mapping. - NAT path — bridge port publishing inserts iptables NAT rules.
iptables -t nat -L DOCKER -non the host shows them; that's where firewalling lives. - userland-proxy — fallback proxy used for some loopback scenarios. Disabled in
daemon.jsonwith"userland-proxy": falsefor performance; rarely needed to know unless an exam question asks why iptables NAT is missing.
Two-tier app on one host: docker network create webnet + docker run -d --name db --network webnet postgres:16 + docker run -d --name api --network webnet --network-alias backend -e DB_HOST=db myapi + docker run -d --name web --network webnet -p 127.0.0.1:8080:80 --link api:backend nginx-proxy. The frontend reaches backend by DNS alias, the API reaches db by name, the host exposes only 127.0.0.1:8080 — a reverse proxy or systemd-managed TLS proxy then publishes 443 to the world. The DCA exam will mix this up by hiding the missing -p or the missing --network — read the question slowly.
EXPOSE documents, -p publishes. Embedded DNS is on every custom bridge / overlay network and resolves container names + aliases. Bind publishes to 127.0.0.1 by default and let a dedicated proxy handle public ports — that single discipline closes a class of accidental-public-exposure bugs the exam loves.
Single-host bridges stop at the host's NIC. The moment two Swarm nodes need a service to talk across them, Docker uses an overlay network: a VXLAN-encapsulated virtual L2 fabric that spans every participating node. The routing mesh and ingress overlay are how Swarm load-balances published ports across the cluster.
- VXLAN encapsulation — overlay traffic is wrapped in a UDP/4789 packet between nodes. Each Swarm overlay network has its own VXLAN ID; the data plane runs entirely inside the kernel.
- Required ports — TCP 2377 (Swarm management, Raft), TCP+UDP 7946 (node discovery / gossip), UDP 4789 (VXLAN data plane). Open these on every node firewall.
- Encryption — overlay networks support
--opt encryptedfor IPsec on the VXLAN tunnels. Theingressoverlay network is encrypted by default. - ingress network — the special overlay network Swarm creates on init. Every published service port goes through it; every node accepts the published port even if no replica runs there (routing mesh).
- Routing mesh — when a request hits any node on a published port, the kernel IPVS rules load-balance it across all healthy task replicas, anywhere in the cluster. The hop is transparent to the client.
- Host mode publishing —
--publish mode=host,target=80,published=80bypasses the routing mesh; the port is bound only on the node running the task. Useful for performance-sensitive workloads (no extra hop, no NAT) and for protocols that don't survive load balancing. - Service mesh DNS — inside an overlay, the embedded DNS resolves
tasks.<svc>to the individual task IPs and<svc>to the service VIP. Picking the right one matters for stateful protocols.
A 3-node Swarm cluster running a frontend service replicated 6× and a single Postgres task. docker network create --driver overlay --opt encrypted app-overlay; docker service create --name web --replicas 6 --network app-overlay --publish 80:8080 web:1. Any client hitting node1:80 is load-balanced by IPVS to one of the six task replicas — possibly on node3. The Postgres task reaches the web service via the overlay's web VIP; if it needs sticky-per-replica connections it queries tasks.web instead, picks one IP, and bypasses the VIP. The exam phrases this as "which DNS name returns multiple A records?".
mode=host publishing opts out. Inside the overlay, <svc> resolves to a VIP, tasks.<svc> to each task IP.
Docker Swarm — Orchestration at Scale3 lessons
Orchestration is exam domain 1 (25%) — the single heaviest domain on the DCA. Three lessons cover the Raft-based control plane (init/join, quorum, manager promotion), the service / replica / rolling-update lifecycle, and the Swarm secrets + configs + ingress patterns. If you only have time for one module, double-down here.
📖 Read in-depth chapter ▾
Swarm is Docker's built-in orchestrator. A Swarm cluster has two role types — manager and worker — and the manager group runs Raft consensus to keep cluster state consistent. The DCA exam loves the quorum math (how many manager failures can you tolerate?) and the join-token model.
- Managers — accept CLI commands, schedule tasks, run the Raft log. Always an odd count for clean quorum: 1 (lab), 3 (most prod), 5 (HA), 7 (rare). Adding more does not increase performance; it just adds Raft write latency.
- Workers — execute tasks; report back to the leader manager. No state, can be scaled freely.
- Quorum formula — a Swarm with N managers tolerates
⌊(N-1)/2⌋failures. 3 managers → tolerate 1. 5 → tolerate 2. 7 → tolerate 3. Lose quorum and the cluster is read-only (running tasks keep running; you cannot schedule new ones). - Two join tokens —
docker swarm join-token worker+docker swarm join-token manager. Each is a long string starting withSWMTKN-1-…. Rotate with--rotateif a token leaks. - Auto-lock —
docker swarm init --autolockencrypts the Raft logs on disk; managers need an unlock key to restart. Right for high-security environments. - Node management —
docker node ls / inspect / promote / demote / update --availability {active|pause|drain}. Drain a node before maintenance; tasks reschedule elsewhere. - Leave / remove — workers can
docker swarm leaveat will; managers must demote themselves or be force-removed. Always remove the node from manager state withdocker node rmafter it leaves.
Bootstrap a 3-manager / 4-worker Swarm: on the first manager, docker swarm init --advertise-addr 10.0.0.10 (prints two join commands and a worker token); docker swarm join-token manager on the same host to get the manager token; on managers 2 + 3, run the printed manager join command; on the four workers, run the worker join command. Verify with docker node ls — you should see three nodes with MANAGER STATUS = Reachable/Leader and four with empty manager status. This cluster tolerates one manager failure (formula: ⌊(3-1)/2⌋ = 1) — the canonical DCA quorum-math question.
docker node rm after they leave. Auto-lock encrypts the Raft store on disk for high-security clusters.
Once the cluster is up, you stop thinking in containers and start thinking in services: a declarative spec (image + replicas + ports + constraints) that Swarm enforces. The DCA exam tests the rolling-update flags hardest — parallelism, delay, failure-action, monitor — because that is where bad deploys cause real outages.
- Service — desired state.
docker service create --name web --replicas 5 --publish 80:8080 nginx:1.27. Swarm schedules five tasks, restarts any that die, reschedules any whose node leaves. - Task — one container running on one node. The atomic unit of scheduling.
docker service ps weblists every task with current state, desired state, and node. - Replicated vs Global —
--mode replicated(default; N tasks total) vs--mode global(exactly one task per eligible node, perfect for monitoring agents or log collectors). - Scaling —
docker service scale web=10ordocker service update --replicas 10 web. Either form schedules five new tasks immediately. - Rolling update flags —
--update-parallelism N(tasks updated simultaneously),--update-delay 10s(wait between batches),--update-failure-action {pause|continue|rollback},--update-monitor 30s(post-update grace period). Default failure-action is pause — silent stops in production unless you setrollback. - Health-driven updates — combine with a Dockerfile
HEALTHCHECKor a service-level--health-cmd. Swarm only marks a new task "running" after it passes a health probe; otherwise rollback / pause kicks in. - Placement constraints —
--constraint node.role==worker,--constraint node.labels.zone==eu-west-1. Label managers + workers withdocker node update --label-add. - Rollback —
docker service rollback webreturns to the previous service spec. Swarm keeps the previous spec, not history — only one rollback step is available.
Production-safe rolling update of an HTTP API from v1.4 to v1.5: docker service update \ \n --image registry/api:1.5 \ \n --update-parallelism 2 \ \n --update-delay 20s \ \n --update-failure-action rollback \ \n --update-monitor 30s \ \n --update-order start-first \ \n api. Swarm updates two tasks at a time, waits 20s between batches; if a new task fails its healthcheck within 30s of starting, the entire service rolls back to v1.4 automatically. start-first starts the new task before stopping the old one — zero capacity dip during deploys. Memorise these five flags; they show up in basically every DCA Swarm question.
2 / 20s / rollback / 30s / start-first. docker service rollback reverts one step; pair it with healthchecks for automatic safety.
Production Swarm deploys live in compose files (docker stack deploy) and lean on first-class Secrets + Configs to keep credentials out of images. The DCA exam tests three things tightly: where secrets are mounted, how configs differ from secrets, and how routing-mesh vs host-mode publishing route traffic.
- Secrets — encrypted in the Raft log at rest, decrypted only on the nodes that run tasks needing them, mounted read-only into the container at
/run/secrets/<name>. Never appear indocker inspectoutput. Maximum size: 500 KB. - Configs — same delivery model as secrets but for non-sensitive content (nginx.conf, prometheus.yml). Stored unencrypted in Raft; mounted at any path you choose with
--config target=…. - Create from file —
docker secret create db_pw ./db_pw.txtor pipe stdinprintf %s "$PW" | docker secret create db_pw -. Same syntax for configs. - Immutability — secrets and configs are immutable. To change one: create a new
_v2secret, rundocker service update --secret-rm db_pw --secret-add source=db_pw_v2,target=db_pw, delete the old. The exam loves asking about this rotation pattern. - Stack files — compose v3+ YAML with
deploy:,secrets:,configs:top-level keys.docker stack deploy -c app.yml mystackcreates one stack with services prefixedmystack_<svc>. - Publish modes —
--publish mode=ingress,target=80,published=8080(default; routing-mesh load balancing) vs--publish mode=host,target=80,published=8080(bind directly on each task's node, bypass mesh). Host mode is required when the upstream LB does the load balancing or when the protocol can't traverse IPVS NAT. - Inspecting traffic —
docker service inspect --pretty <svc>shows endpoint mode + published ports;iptables -t mangle -Lon a node shows the IPVS magic that routes incoming connections.
A production stack: secrets: \n pg_pw: { external: true } \nservices: \n api: \n image: registry/api:1.5 \n secrets: [pg_pw] \n deploy: { replicas: 4, update_config: { parallelism: 2, failure_action: rollback }, restart_policy: { condition: on-failure } } \n ports: [ "target: 80, published: 80, mode: ingress" ]. The API reads /run/secrets/pg_pw at startup, ports 80 on every node route to the four replicas via the ingress mesh, rolling updates batch two at a time with auto-rollback. Rotating the password is a three-line change — create pg_pw_v2, service-update --secret-rm pg_pw --secret-add source=pg_pw_v2,target=pg_pw, prune the old. No image rebuild, no env-file leak, no downtime.
/run/secrets/<name>; configs are the unencrypted twin for non-sensitive files. Both are immutable — rotate by creating _v2 and swapping with --secret-rm + --secret-add. mode=ingress uses the routing mesh, mode=host bypasses it.
Container Security & Production Hardening3 lessons
Security is exam domain 5 (15%) and the question pool that punishes shortcuts. Three lessons cover the kernel security primitives (capabilities, seccomp, AppArmor, userns-remap), image security (Content Trust, signing, CVE scanning, distroless), and runtime hardening (non-root user, read-only root, no-new-privileges, resource limits).
📖 Read in-depth chapter ▾
Docker security is not a Docker feature — it is the kernel's. Capabilities slice up root power, seccomp filters syscalls, AppArmor/SELinux enforce mandatory access control, userns-remap maps in-container root to an unprivileged host UID. Knowing which layer addresses which threat is exactly what the DCA exam tests.
- Capabilities — Linux divides root power into ~40 capabilities. Docker drops most by default and keeps a minimal set (NET_BIND_SERVICE, CHOWN, DAC_OVERRIDE, …). Modify with
--cap-drop ALL --cap-add NET_BIND_SERVICE; never use--privilegedin production (it grants everything). - seccomp — kernel-level syscall filter. Docker ships a default profile that blocks ~44 dangerous syscalls (keyctl, ptrace from another PID namespace, …). Apply a custom profile with
--security-opt seccomp=/path/profile.json;unconfineddisables filtering — never do this. - AppArmor / SELinux — mandatory access control. Docker applies the
docker-defaultAppArmor profile on Debian/Ubuntu; SELinux is enabled per-container on RHEL with--security-opt label=.... - User namespace remapping —
"userns-remap": "default"indaemon.jsonmaps in-container UID 0 (root) to an unprivileged host UID (e.g. 100000). A container breakout lands on an unprivileged shell, not on host root. - no-new-privileges —
--security-opt no-new-privileges:truesets the kernelno_new_privsbit, blocking setuid/setgid escalation inside the container. Cheap and almost always safe. - Read-only root + tmpfs —
--read-only+--mount type=tmpfs,dst=/tmpmakes the rootfs immutable; combine with capability drops for a defence-in-depth runtime.
Hardened production run of an HTTP service: docker run -d \ \n --read-only \ \n --tmpfs /tmp:rw,noexec,nosuid \ \n --cap-drop ALL --cap-add NET_BIND_SERVICE \ \n --security-opt no-new-privileges:true \ \n --security-opt seccomp=default.json \ \n --user 10001:10001 \ \n --memory 512m --cpus 1 \ \n -p 127.0.0.1:8080:80 \ \n myapp:1.0. Drop all capabilities, allow only the one needed to bind port 80, read-only root with tmpfs scratch, syscall filtering, no privilege escalation, non-root UID, resource limits, loopback-only publishing. This eight-flag command is the answer to half of the DCA's "harden this container" questions.
--privileged.
Half of every container security incident traces back to the image: a vulnerable base layer, an unsigned third-party build, or a leaked secret in a layer no one inspected. Three controls — Docker Content Trust, image signing, and CVE scanning — defend the supply chain end-to-end, and the DCA exam tests each.
- Docker Content Trust (DCT) —
export DOCKER_CONTENT_TRUST=1tells the CLI to refuse to push or pull unsigned images. Backed by Notary v1 / TUF (The Update Framework). Per-publisher keys live under~/.docker/trust/. - Cosign / Sigstore — modern signing layered on top of OCI registries. Sign with
cosign sign $IMG, verify withcosign verify $IMG --certificate-identity-regexp …. Keyless flow uses OIDC + Sigstore's transparency log (Rekor) — no long-lived signing keys. - CVE scanning —
docker scout cves myapp:1.0,trivy image myapp:1.0,grype myapp:1.0all walk the image's package metadata and match against vulnerability databases. Run in CI on every push; fail the build on high / critical findings. - Minimal base images —
distroless,alpine,scratch. Fewer packages = fewer CVEs = faster scans. Distroless ships no shell, which alone defeats a large class of post-exploitation toolkits. - SBOM — Software Bill of Materials in SPDX or CycloneDX.
syft myapp:1.0 -o spdx-json > sbom.json+cosign attest --predicate sbom.json --type spdxjson myapp:1.0attaches the SBOM as a signed attestation. - Layer hygiene — never
ADD secretsin any layer; even if the next layer deletes them, the previous layer remains in the image. Use BuildKit secret mounts (--mount=type=secret,id=npm) so secrets never touch a persisted layer.
Supply-chain-secure CI: docker buildx build --push -t ghcr.io/team/api:1.5 . + cosign sign --yes ghcr.io/team/api:1.5 + trivy image --severity HIGH,CRITICAL --exit-code 1 ghcr.io/team/api:1.5 + syft ghcr.io/team/api:1.5 -o spdx-json > sbom.json && cosign attest --predicate sbom.json --type spdxjson ghcr.io/team/api:1.5. On deploy, an admission webhook on the cluster runs cosign verify and refuses any image without a valid signature and a clean trivy attestation. This four-command pipeline is the answer to "how do you stop unsigned or vulnerable images from reaching production?" — the exam's favourite supply-chain question.
The last layer is operational: how you run a container, what limits you set, how you watch it. The DCA exam frames this as "given this docker run command, what is missing for production?" — the answer is almost always a subset of the eight flags below plus a logging / monitoring story.
- Non-root user —
USER 10001:10001in the Dockerfile or--user 10001at run time. Most app frameworks do not need root at all. - Read-only rootfs + tmpfs —
--read-only+--mount type=tmpfs,dst=/tmpfor scratch space. Immutable rootfs kills a class of malware persistence. - Resource limits —
--memory 512m --memory-swap 512m --cpus 1.5 --pids-limit 200. Without these, a single buggy container can OOM-kill the host. - Restart policies —
--restart on-failure:5(retry up to 5 times) or--restart unless-stopped(always restart unless explicitly stopped). Pair with a healthcheck. - Logging discipline — set
--log-driver json-file --log-opt max-size=10m --log-opt max-file=3per container (or globally in daemon.json) so logs don't fill the disk. - Drop the socket — never mount
/var/run/docker.sockinto a container unless you have decided that container is allowed to be root on the host (which is what socket access means). Use a sidecar with the API exposed over TCP+TLS+auth if you really need it. - Observability —
docker stats, cAdvisor, Prometheus node-exporter + cadvisor scrapes, Falco for runtime anomaly detection. Every prod cluster needs at least metrics + alerting. - Patch cadence — pin base images by digest, run a weekly job that bumps the digest if a newer one exists, runs the CI scan, and opens a PR. Treat container patching like OS patching.
Final hardened service definition that puts every Module 6 lesson together: services: \n api: \n image: ghcr.io/team/api@sha256:<digest> \n user: "10001:10001" \n read_only: true \n tmpfs: [ /tmp ] \n cap_drop: [ ALL ] \n cap_add: [ NET_BIND_SERVICE ] \n security_opt: [ "no-new-privileges:true", "seccomp=default.json" ] \n deploy: \n replicas: 4 \n resources: { limits: { cpus: '1.0', memory: 512M } } \n update_config: { parallelism: 2, delay: 20s, failure_action: rollback } \n healthcheck: \n test: [ "CMD", "curl", "-f", "http://localhost:80/healthz" ] \n interval: 10s \n retries: 3 \n logging: { driver: json-file, options: { max-size: "10m", max-file: "3" } } \n secrets: [ db_pw ]. Digest pin, non-root user, immutable rootfs, capability drop, no privilege escalation, seccomp, replicas + rolling updates with auto-rollback, healthcheck-driven readiness, log rotation, secrets out of env. A DCA "production-ready stack" answer in 15 lines.