Back to the course
Test yourself

Docker in depth

Final exam · 40 questions · answers explained as you pick
Image internals
5 questions
01A Dockerfile ends with COPY app /srv/app, then ENV MODE=prod, LABEL team=payments, USER 10001 and CMD ["/srv/app"]. A teammate wants to merge the last four lines into fewer instructions to cut the layer count. What would that change?
Correct — ENV, LABEL, USER and CMD add history entries marked empty_layer and no filesystem layer at all, so there is nothing to merge away.
Incorrect — Metadata-only instructions do not add an empty layer either. .RootFS.Layers in the lab listed only the base, WORKDIR, RUN and COPY layers.
Incorrect — Neither store creates layers for metadata instructions. The layer list comes from the image config, which is the same on both stores.
Incorrect — A manifest lists only filesystem layers. The 0B rows exist in the history, not in the manifest.
02You build a single-platform image on a fresh Docker 29 host and push it. The push ends with 1: digest: sha256:5e82..., and docker image inspect --format '{{.Id}}' prints the same value. A colleague says a single-platform image is a plain manifest, so that digest must be the manifest's. What is it?
Incorrect — The config digest is the ID only on the legacy graph-driver store. On the containerd store the ID is the index digest.
Incorrect — Layer digests name compressed layer blobs. The push prints the digest of the document that ties everything together.
Correct — BuildKit attaches a minimal provenance attestation by default, so even a single-platform build becomes an index, and the containerd store uses that index digest as the image ID.
Incorrect — The lab's single-platform build exported a manifest, an attestation manifest and a manifest list, and the pushed digest was the manifest list's.
03Production's Compose file names registry.example.com/shop/api:2.4@sha256:<tested>. After the release someone pushes a quick fix under the same 2.4 tag. The next docker compose pull && docker compose up -d on the production host runs which image?
Incorrect — When a reference carries a digest, the tag is ignored. The lab ran app:1.0@digest and got build 1 although the tag had moved to build 2.
Correct — In name:tag@digest the tag is only there for people reading the file; the digest decides which bytes run.
Incorrect — Docker does not compare the two. The lab pulled and ran name:tag@digest with a moved tag without any error.
Incorrect — The pull resolves the digest, not the tag, so the quick fix is never pulled for this reference.
04A release job promotes with docker pull staging/app@<tested>, docker tag and docker push prod/app:1.0. It runs on a host upgraded from Docker 27 that still uses the legacy image store, and the digest the push prints is not the tested one. Why?
Incorrect — A registry stores blobs as pushed. The lab hashed a pulled blob and got exactly its manifest digest.
Incorrect — Tagging adds a name; it does not touch the config. Rebuilding changes the config, tagging does not.
Incorrect — Platform choice would not explain a different top-level digest for the same platform. The cause is what the store kept.
Correct — A re-push from that host cannot rebuild the original index digest. docker buildx imagetools create copies the index registry-side and avoids the local store.
05A CI host on a fresh Docker 29 install rebuilds app:ci under the same tag fifty times a day. The cleanup script, written for an older host, looks for dangling <none> images and never finds any, yet disk use keeps growing. Where do the bytes of the earlier builds live?
Incorrect — The lab ran docker image ls -a and the dangling=true filter after a rebuild; both showed nothing.
Correct — The containerd store drops the old image record when the tag moves, and the earlier builds' results stay in the build cache. Trim it with docker builder prune and a budget.
Incorrect — Nothing in the lab pointed there. docker system df showed the growth in the Build Cache row, which Docker tools manage.
Incorrect — Builds do not run as containers with json-file logs. Their leftovers are build cache records.
5 questions · explanations appear as you answer
Building with BuildKit
5 questions
01Builds sent to a remote BuildKit node spend most of their time before the first step, and the log shows transferring context: 1.2GB. The Dockerfile only uses RUN --mount=type=bind,target=. and copies one binary into a scratch stage. What reduces the wait?
Incorrect — The client sends the context before any step runs, whatever the instructions do with it. COPY would also put the source into a layer.
Incorrect — Cache mounts keep data a step writes. They do not change what the client uploads as context.
Incorrect — The target decides which stages run. The context is transferred in full before that.
Correct — The ignore file decides how much of the directory is sent, and with a bind mount it also decides what the build can see. In the lab the build sent 35.01MB without the ignore file and only a few bytes of metadata with it.
02A step uses RUN --mount=type=secret,id=npm_token,required=true npm ci. A developer builds without passing any --secret and the build succeeds. Is required=true broken?
Correct — The lab built without the secret and passed from cache; the same build with --no-cache failed with secret api_token: not found.
Incorrect — The source of the secret does not matter. What matters is whether the step actually executes.
Incorrect — Secrets never land in a layer or in the history; the lab found no /run/secrets in the image.
Incorrect — Docker 29 builds with BuildKit by default, and the legacy builder does not understand RUN --mount at all.
03A shell profile on a build host runs docker buildx use ci-builder, a docker-container builder. Since then docker buildx build -t app:dev . finishes without errors but docker run app:dev says the image does not exist, while docker build -t app:dev . works. Why?
Incorrect — --load exports a tarball that the daemon imports; the lab loaded lab-api:1 from lab-builder that way.
Incorrect — Both commands follow the selected builder. The lab ran docker build on lab-builder after buildx use.
Correct — Without an output, the docker-container builder keeps the result as cache only. Pass --load or --push, and name the builder with --builder in scripts.
Incorrect — Tags are not rewritten. Nothing was exported at all, which is what the "No output specified" warning says.
04A pipeline on a host that still uses the classic image store fails at --cache-to type=registry,ref=registry.example.com/app:buildcache,mode=max with Cache export is not supported for the docker driver. Which change keeps registry cache export working?
Incorrect — On the classic store the docker driver refuses every cache export except inline, whatever the mode.
Correct — A docker-container builder runs its own BuildKit and supports every cache backend and output the lesson covered.
Incorrect — --load decides where the image goes. It has nothing to do with cache export support.
Incorrect — The default builder is the docker driver, the one producing the error.
05On an arm64 Linux host you register the amd64 emulator with tonistiigi/binfmt --install amd64. docker buildx ls still lists only linux/arm64 for the default builder. Will docker buildx build --platform linux/amd64 work now?
Correct — binfmt_misc does the emulation in the kernel. The default builder reads the supported platforms when dockerd starts, so only the listing lags.
Incorrect — The lab built amd64 and arm64 on the default builder with the emulator registered.
Incorrect — It is the other way round: the registration lives in kernel memory and is lost at reboot unless something registers it again.
Incorrect — The lab's Dockerfile had a RUN step, and it executed under QEMU, reporting x86_64.
5 questions · explanations appear as you answer
The Docker engine
5 questions
01During an incident docker ps hangs, while the web containers on the host keep answering requests. Which command shows the container processes are still running without going through dockerd?
Incorrect — Every docker command goes through the dockerd API, which is the part that hangs.
Incorrect — runc exits once the process is started. The lab found no runc process with a container running.
Correct — containerd keeps Docker's containers in the moby namespace, and its task list shows they are running even while dockerd is stuck.
Incorrect — It shows the state of the daemon. The container cgroups are separate scopes, and the processes are children of their shims.
02On a Kubernetes node, docker ps shows no containers at all, yet the node runs a dozen pods. What explains it?
Incorrect — Kubernetes nodes run containerd without dockerd; there is no second Docker daemon.
Incorrect — Docker has no notion of pods to hide. It never knew about these containers.
Incorrect — Nothing here is rootless. The containers belong to another containerd client and namespace.
Correct — dockerd keeps its containers in moby; sudo ctr -n k8s.io containers ls lists the pod containers.
03Containers on the default bridge cannot reach hosts on a VPN that uses 172.17.0.0/16. On a host that runs with live-restore: true, you added default-address-pools with 10.200.0.0/16 to daemon.json and restarted, and new user-defined networks now use 10.200.x.0/24. The problem on the default bridge remains. Why?
Correct — Without bip, dockerd reuses the bridge's existing address, and live-restore stops it from re-creating the bridge. With live-restore off it would be re-created from the pools. Set bip to choose the range in every case.
Incorrect — It is the other way round: the pools are used when no --subnet is given, which is how the lab's new networks got 10.200.x.0/24.
Incorrect — True for existing containers, but a new container on the default bridge still gets a 172.17 address, because the bridge kept its 172.17 address.
Incorrect — features holds feature switches such as containerd-snapshotter. The default bridge range is its own option.
04An old runbook adds "storage-driver": "overlay2" to daemon.json on a fresh Docker 29 host. After the restart docker image ls is empty. The operator removes the line and restarts again; docker info still reports overlay2 and the list is still empty. What brings the images back?
Incorrect — Nothing is cached in memory. The daemon keeps the legacy store because it finds legacy-store data on disk.
Correct — Once legacy-store directories exist, dockerd keeps using that store unless the containerd store is requested explicitly. The images were hidden, not deleted.
Incorrect — Switching stores never deletes data; containerd still held the images, as ctr -n moby images ls showed.
Incorrect — data-root moves dockerd's own directory. Pointing it at containerd's root would not select the containerd store.
05Services on a node log with --log-driver fluentd. While the collector is being upgraded, every deploy on the node fails: docker run exits 125 with dial tcp ...:24224: connect: connection refused, and the container is left in Created. Which option lets containers start while the collector is away?
Incorrect — The container never got as far as running; a restart policy acts on exits of a started container.
Incorrect — The dual-logging cache has nothing to do with the connection at start, and disabling it only breaks docker logs.
Incorrect — The lab showed the opposite pattern: drivers that open a connection fail the start when the collector is down.
Correct — The container starts and the driver buffers and reconnects in the background, as the lab showed.
5 questions · explanations appear as you answer
Storage
4 questions
01A forensics runbook reads docker inspect -f '{{.GraphDriver.Data.UpperDir}}' to find a suspect container's changed files. On a fresh Docker 29 host the field is empty because GraphDriver is null. Where do you find the writable layer?
Incorrect — docker diff lists changed paths inside the container (A, C, D), not where they live on the host.
Incorrect — That layout belongs to the legacy store. On the containerd store the snapshots live under /var/lib/containerd.
Correct — findmnt on that mount point shows the upper directory under containerd's snapshotter, and ctr -n moby snapshots info <id> describes the same snapshot.
Incorrect — The new Storage field only names the snapshotter (overlayfs); it carries no path.
02An old XFS host formatted with ftype=0 runs the legacy store ("containerd-snapshotter": false). After provisioning, docker info reports the vfs driver, container starts are slow and disk use multiplies. The journal holds no error except one about fuse-overlayfs. What happened?
Correct — Automatic selection skipped overlay2 without a d_type message and settled on vfs, which has no copy-on-write. Reformat with ftype=1 and alert on the driver.
Incorrect — metacopy only affects copy-up of metadata changes. The lab ran overlay2 with metacopy off on ext4.
Incorrect — Since Engine 23 a daemon told to use overlay2 on such a filesystem refuses to start.
Incorrect — The lab tried it: the overlayfs snapshotter went into an error state and dockerd refused to start.
03An installer writes a script to scratch space and runs it. With --mount type=tmpfs,dst=/scratch,tmpfs-size=64m the script fails with Permission denied and exit status 126. What makes it run?
Incorrect — The mode was not the problem. The lab mounted with mode 1770 and the script still failed because of noexec.
Incorrect — noexec applies to every user, root included.
Incorrect — --mount type=tmpfs has no field for exec in 29.8.2; it always mounts noexec.
Correct — --tmpfs takes raw mount options, and exec lifts the restriction. Keep the size: without one the limit is half the host's RAM.
04A project's compose.yaml declares pgdata with external: true and name: shop-pgdata. Someone runs docker compose down -v by mistake on the production host. What happens to the database volume?
Incorrect — -v removes volumes Compose created for the project. External volumes are not Compose's to delete.
Correct — The lab ran down -v and lab-shared was still listed. That is the reason to declare long-lived data volumes external.
Incorrect — down ran normally in the lab and removed the network; it only skipped the external volume.
Incorrect — Use by other containers is not the rule. External volumes are never removed by the project.
4 questions · explanations appear as you answer
Networking
5 questions
01A host runs with live-restore: true. dockerd is stopped for an engine upgrade. The containers keep running, but an application that connects to db by name gets resolution failures until dockerd is back. Why?
Incorrect — Networking keeps working; requests by IP address and existing connections are unaffected. Only name resolution stops.
Correct — NAT rules inside the container send port 53 to ports dockerd listens on. With dockerd gone, nothing answers.
Incorrect — Docker does not rewrite the file for an upgrade. The nameserver stays 127.0.0.11; it just has no one behind it.
Incorrect — There is no such expiry; the server is simply not running.
02Containers must get addresses directly on a cloud network whose switch ports accept only one MAC address each. Which driver fits?
Incorrect — macvlan gives each container its own MAC, which networks that drop unknown MACs will discard.
Incorrect — An internal network has no route to the outside at all.
Incorrect — Then the containers share the host's address and ports; they do not get addresses of their own.
Correct — ipvlan containers are told apart by IP, not MAC, which suits ports and cloud networks that accept one MAC.
03nginx publishes -p 8080:80 on an IPv4-only network. Its access log shows real LAN addresses for outside clients, but every request from a monitoring agent on the host to 127.0.0.1:8080 appears to come from the bridge gateway address. Why?
Correct — Loopback is excluded from the DNAT rules, so docker-proxy accepts the connection and dials the container itself. conntrack shows the two halves.
Incorrect — MASQUERADE applies to traffic leaving the container subnet through another interface; outside clients kept their real addresses.
Incorrect — DNAT rewrites the destination. A request to the host's own address kept its source in the lab.
Incorrect — ufw does not rewrite addresses, and loopback traffic never crosses FORWARD.
04A host that ran the iptables backend is switched to "firewall-backend": "nftables" with a daemon restart, no reboot. Published ports now time out from the LAN and containers cannot reach the internet, although docker info reports the nftables driver. What did the migration leave to you?
Incorrect — With the nftables backend there is no DOCKER-USER chain; your rules go in a table of your own.
Incorrect — The proxy covers loopback and IPv6 clients of IPv4-only networks; it does not carry forwarded LAN traffic.
Correct — The iptables backend set FORWARD to DROP when it enabled forwarding, and that policy still drops packets Docker's nftables rules accepted. Forwarding itself stayed on from the earlier start; on a fresh boot Docker reports an error if a network needs forwarding and it is off. Once the host forwards freely, add your own nftables rules that block forwarding between non-Docker interfaces.
Incorrect — Swarm mode is refused outright with the nftables backend.
05For the same published port, curl http://127.0.0.1:8090/ on the host gets Connection reset by peer and curl http://<host-ip>:8090/ gets connection refused. Both containers of the app are up. What is the likely fault?
Correct — DNAT carried the SYN to the container, whose kernel refused it, and docker-proxy accepted the loopback client and reset it when its own connection failed.
Incorrect — A drop gives a timeout. Both answers here came from somewhere: a refusal and a reset.
Incorrect — Network membership matters between containers. Host clients reach published ports whatever network the container is on.
Incorrect — Both requests used IP addresses, so no lookup was involved.
5 questions · explanations appear as you answer
Orchestration with Swarm
5 questions
01A team running a swarm with one manager adds a second manager "for high availability". What did that do to the cluster's tolerance for manager failures?
Incorrect — With two managers the quorum is two; a copy of the state is not a majority.
Incorrect — Workers have no Raft vote. Only managers count towards quorum.
Correct — Two managers need both for a majority, and there are now two machines that can fail. Run three, or five to survive two failures.
Incorrect — Being leader does not replace a majority. One of two is not more than half.
02Autolock is on. A manager reboots during unattended patching and now answers every cluster command with Swarm is encrypted and needs to be unlocked. What brings it back?
Incorrect — The Raft data is encrypted with a key that the unlock key protects. Without that key nothing can read it.
Incorrect — Rotating needs a working, unlocked manager; it does not open a locked one.
Incorrect — A locked manager stays locked across restarts until someone provides the key.
Correct — The manager stays locked until the unlock key is provided. Keep the key away from the cluster, and know that losing it with every manager locked is unrecoverable.
03A Compose file with depends_on: {db: {condition: service_healthy}} is converted to a stack by dropping the condition. After docker stack deploy, the web tasks fail a few times at startup with database connection errors, then stay up. What is going on?
Correct — Services start in parallel and are restarted until they stay up. The short depends_on list is accepted but orders nothing in Swarm.
Incorrect — There is no such key. Swarm has no startup ordering in any form.
Incorrect — Health checks work in Swarm and decide when a task counts as healthy. They do not hold other services back.
Incorrect — --detach=false makes the CLI wait for convergence; it does not order services.
04Inside a Swarm task, ls -l /run/secrets/api_key shows the file owned by root with mode 0444, readable by every user in the container. The service runs as UID 10001, and only that user should read it. What is the fix?
Incorrect — Swarm stores the secret in the cluster and mounts it with the mode from the service spec, not the source file's mode.
Incorrect — Configs follow the same spec fields and are not meant for sensitive data; anyone with manager API access can read them back.
Correct — Swarm applies these fields when it mounts the secret; the lab mounted the Postgres secret as UID 70 with mode 0400 that way.
Incorrect — Read-only stops writes, not reads. Mode 0444 lets every user in the container read the secret.
05Overlay traffic works between nodes on a plain overlay network. On a network created with --opt encrypted, cross-node requests time out, while 2377/tcp, 7946/tcp+udp and 4789/udp are all open between the nodes. What is missing?
Incorrect — The lab captured no VXLAN at all on the encrypted network, only ESP packets.
Correct — Encrypted overlays use IPsec ESP between every pair of nodes, and many security group templates do not include protocol 50.
Incorrect — 2376 was the lab's TLS Docker API port for contexts, not part of the swarm data plane.
Incorrect — --attachable only lets standalone containers join. Service tasks attach regardless.
5 questions · explanations appear as you answer
Practical recipes
6 questions
01A Python image runs gunicorn 26 as USER 10001:10001, a numeric user with no passwd entry. At startup it logs Control server error: [Errno 13] Permission denied: '/.gunicorn'. What is the cause and the fix?
Incorrect — gunicorn bound and served normally in the lab. The error is about a directory, not a port.
Incorrect — The code and venv are deliberately not owned by the app user, so it cannot modify them. The error names /.gunicorn.
Incorrect — Nothing in this setup is read-only. / simply is not writable for UID 10001.
Correct — With no passwd entry HOME is /, which the app user cannot write. A container is managed with signals, so the socket is not needed.
02The Postgres recipe's health check runs pg_isready -h 127.0.0.1 ... over TCP instead of the default Unix socket. What does the TCP form protect against?
Incorrect — pg_isready does not authenticate in either form; it only checks whether the server accepts connections.
Incorrect — The socket exists; docker compose exec db psql -U postgres uses it.
Correct — During first start the image runs a temporary server for the init scripts. Only the real server listens on TCP, so dependants wait for it.
Incorrect — Speed is not the reason. The reason is what a passing check proves.
03To check that the application role's new password works, an operator runs docker compose exec db psql -U shop_app -d shop and gets in without being asked for a password. What does that prove?
Incorrect — psql was never asked for a password. Nothing was read or checked.
Correct — Test from another container on the network, as the recipe's psql tools service does, where a SCRAM password is required.
Incorrect — The role may well have a password; local connections inside the container are simply trusted.
Incorrect — On a populated volume the image skips all first-start settings, the password file included.
04A Redis compose file switches the default user off in the ACL file. Its health check is redis-cli ping with no credentials, and the container is always reported healthy, even after an ACL change locked out the application user. Why?
Incorrect — Without logging in, the lab's client got NOAUTH Authentication required. for its command.
Incorrect — The official image runs with protected mode off, and protected mode never answers commands on anyone's behalf.
Incorrect — Redis ACLs apply to Redis connections, whatever the Unix user of the client.
Correct — The recipe logs in as app and greps the reply for PONG, so a refused login fails the check.
05An NGINX container gets its certificate and key as single-file bind mounts. The renewal tool writes new files and renames them over the old ones. After nginx -t and nginx -s reload, clients still receive the old certificate. Why?
Correct — A single-file bind mount follows the inode. Tools that write in place are fine; tools that rename need a directory mount or a recreated container.
Incorrect — The lab's reload picked up a renewed certificate written in place, with a new serial number.
Incorrect — A Compose secret outside Swarm is a bind mount of the host file, not a cached copy.
Incorrect — The resolver controls DNS answers for the upstream, not certificates.
06You restore the WordPress recipe's backup onto a new host with restore.sh. The new host's secret files hold different passwords from the old host. Does WordPress connect to the database afterwards?
Incorrect — The dump is mysqldump --databases wordpress; the wp account was created by MySQL's initialisation on the new host.
Incorrect — That is needed when a secret changes on a populated volume. Here the volume starts empty.
Correct — The first start creates wp with the current password, and the dump adds the site's tables.
Incorrect — The connection does not depend on the file restore at all; it depends on MySQL creating wp from the new secrets. The recipe restores the files before WordPress first starts so the site never runs half-restored.
6 questions · explanations appear as you answer
Production & CI
5 questions
01A service passes its health check under read_only: true and serves traffic, then fails on the first image upload with EROFS: read-only file system. What should have caught this before shipping, and what is the fix?
Incorrect — Read-only is a run setting. It fails at run time, often only on the first request that writes.
Correct — Scratch data gets a tmpfs, data that must survive gets a volume. Run those paths before shipping.
Incorrect — A health check that writes to the app directory would fail under read-only too, and would need the same tmpfs or volume.
Incorrect — Uploads belong on a volume anyway, so they survive the container. Read-only stays.
02On every deploy a Node service takes exactly ten seconds to stop, and docker inspect then shows exit code 137 with OOMKilled=false. What is happening?
Incorrect — 137 is 128 + 9, SIGKILL, from any sender. OOMKilled is false here, and the timing points at the stop timeout.
Incorrect — Blocking delivery can stall writes, but it does not produce a SIGKILL after exactly ten seconds.
Incorrect — The init process forwards signals and reaps zombies quickly; the lab's stop with init took a fraction of a second.
Correct — Use exec-form CMD, handle SIGTERM, and run an init process so the signal reaches the app.
03An image sets a numeric USER, a HEALTHCHECK and exec-form CMD. Deployed with a plain docker run, a pre-ship check still fails nine items. Which settings can the image itself never carry?
Incorrect — Both live in the image config and passed in the lab's plain run.
Incorrect — Rotation is one of them, but limits, capabilities and the read-only root are run settings too.
Correct — Those belong to the run configuration (Compose, Swarm, Kubernetes), so check the running container, not only the image.
Incorrect — Labels are metadata. Docker does not turn them into runtime settings.
04A release pipeline builds, tests, scans, pushes, signs and promotes. On one commit the Trivy step fails on a CRITICAL finding that has a fixed version. What does the build registry hold from that run?
Correct — The lab's first run stopped at the scan and the build registry's catalog was empty: the failed image exists only on the build machine.
Incorrect — The pipeline never pushes before the gates. Relying on a missing signature to stop an image that should not exist is a weaker control.
Incorrect — No such step exists. The fix is a new build of the corrected dependency, which gets its own digest.
Incorrect — Attesting comes after the push, which never happened. The scan report is the record of the failure.
05A promotion job verifies build/app@<digest>, copies it to the production registry with docker buildx imagetools create, and then runs cosign verify against prod/app@<digest>. The production check finds no signature, although the digest is identical. Both registries are registry:3, without the OCI referrers API. What did the job leave out?
Incorrect — The lab verified the same signature in the production registry after copying it; no new signature was made.
Correct — imagetools create copies the index and what it references, and Cosign's signature and attestation live in a separate artifact under that tag. On a registry with the referrers API there is no such tag, and oras cp -r copies the image with its referrers.
Incorrect — Promotion by tag is exactly what the job must refuse: a tag can point anywhere by the time the copy runs.
Incorrect — Provenance is a BuildKit attestation inside the index, and it was copied. Cosign signatures are stored outside the index.
5 questions · explanations appear as you answer