← Back to all posts

6 Docker Practice Exercises for Beginners, with Checks and Solutions

Practice container exit codes, command overrides, configuration, networking, persistent data, and logs through six small Docker tasks with verified solutions.

Six rounded blue shipping containers arranged as a compact stepped stack

You have run your first container. What should you practice next without first building an entire application?

These six Docker exercises focus on small behaviors you can observe directly: an exit code, an overridden command, a configuration value, an internal HTTP response, a persistent file, and an error log. Each task has a concrete success condition and a reference solution. Try the task before opening its solution, then change one input to check your understanding.

We executed the solutions in a LabEx Ubuntu 22.04 instance VM with Docker Engine 20.10.21 on September 7, 2026. They are original exercises for this article, separate from the challenges in the LabEx Docker course.

Set up a small practice environment

Use a disposable Linux Docker environment with daemon access and internet access for the initial image pulls:

docker version
docker pull alpine:3.23
docker pull python:3.12-alpine

docker version should show both client and server information. A client-only installation cannot run these examples without a reachable daemon. The versioned image tags can receive updates; output IDs will differ between runs.

The exercises use names beginning with practice-. If those names already belong to your own resources, choose different names consistently rather than deleting unrelated work. Exercises 1–3 and 5–6 do not depend on each other. Exercise 4 creates a network and server, which the cleanup section removes.

Exercise Skill Passing evidence
1 Container lifecycle A stopped container with exit code 7
2 Startup command A custom message without building an image
3 Runtime configuration A value read inside the container
4 Container networking HTTP 200 by container name with no published port
5 Data persistence A fresh reader sees a removed writer’s file
6 Failure diagnosis Logs explain an intentionally failed process

These are behavior checks, not a speed test. Looking up a flag in the Docker run reference is allowed. The useful question is whether you know which behavior the flag controls.

Exercise 1: create a container that exits with code 7

Task: Start an Alpine container named practice-exit whose command exits with status 7. Keep the stopped container so you can inspect it. Do not add a long-running sleep to make it appear healthy.

Success condition: The container is stopped, and Docker inspection reports exit code 7.

Reference solution and explanation
docker run --name practice-exit alpine:3.23 sh -c 'exit 7'
docker inspect practice-exit --format '{{.State.Status}} {{.State.ExitCode}}'

Expected inspection output:

exited 7

The first command intentionally returns a nonzero status. That is the task’s expected result. Run the inspection as a separate command; an && chain would skip it after the intentional failure. A shell script using set -e also needs to handle this expected failure explicitly.

The container stops because its primary process finishes. “Exited” describes lifecycle state; it does not by itself tell you whether a task succeeded. A short job can exit successfully with code 0, while a deliberate failure can produce another code.

Change one input: Repeat under a new name using exit code 0. Explain why both containers are stopped even though only one reports failure. Avoid --rm here because it would remove the object you want to inspect.

Exercise 2: override the default command without rebuilding

Task: Inspect the Alpine image’s configured command, then run a container that prints override worked. Do not create a Dockerfile.

Success condition: The message appears, and the one-off container is automatically removed.

Reference solution and explanation
docker image inspect alpine:3.23 --format '{{json .Config.Cmd}}'
docker run --rm alpine:3.23 printf 'override worked\n'

In our run, the configured command was ["/bin/sh"]. The command after the image reference replaced that default for this invocation and printed the requested text.

This example uses an image without an entrypoint that changes the interpretation. With other images, arguments can be passed to an existing ENTRYPOINT instead. Check both image configuration and the Dockerfile CMD/ENTRYPOINT reference before generalizing the result.

--rm is appropriate here because the output is the evidence and we do not need to inspect a stopped instance afterward.

Change one input: Replace printf with uname -s. Predict whether the image has changed. Inspect the image again to confirm that a per-container command override did not rewrite its default configuration.

Exercise 3: pass a value at runtime

Task: Supply an environment variable named MESSAGE with the value hello from config, and print it from inside the container. Use the same Alpine image.

Success condition: The container prints the supplied value without rebuilding anything.

Reference solution and explanation
docker run --rm --env MESSAGE='hello from config' \
  alpine:3.23 sh -c 'printf "%s\n" "$MESSAGE"'

The single quotes around the sh -c program prevent the host shell from expanding $MESSAGE before Docker starts the container. The shell inside the container performs that expansion using its environment.

This is a small example of runtime configuration: the image stays the same while an invocation receives a different value. Put Docker options such as --env before the image name. Text after the image name participates in the container command.

Use a harmless practice value. Environment variables can be inspected and should not be treated as a universal secret-storage mechanism.

Change one input: Supply a different message and verify the new output. Then omit --env and observe the empty line. Can you explain the result without assuming that the variable was stored permanently in the image?

Exercise 4: reach a server without publishing a host port

Task: Create a user-defined Docker network called practice-net. Start a Python HTTP server named practice-web on that network. From another container on the same network, request http://practice-web:8000 and print the HTTP status. Do not use -p.

Success condition: The client prints 200, while docker port practice-web prints no mapping.

Reference solution and explanation
docker network create practice-net
docker run -d --name practice-web --network practice-net \
  python:3.12-alpine python -u -m http.server 8000 --bind 0.0.0.0

After the server has started, make the request:

docker run --rm --network practice-net python:3.12-alpine \
  python -c 'import urllib.request; print(urllib.request.urlopen("http://practice-web:8000", timeout=5).status)'
docker port practice-web

If you issue the request immediately and the server is still starting, check docker logs practice-web and repeat the read-only request. Do not change the network configuration to solve a brief startup delay.

The user-defined bridge provides name resolution between attached containers. The client reaches the server’s container port directly through that network. A host-side published port is not needed for this path. See Docker’s bridge network documentation.

The Python server serves its current directory inside this disposable image. It is only a practice endpoint and has no host directory mount. Python’s http.server documentation explains why it should not be used as a production server.

Change one input: Run the client without --network practice-net. Do not expect the same container-name lookup to work from that separate network context. Explain why adding EXPOSE to an image would not attach a client to this network.

If you need browser access instead, work through EXPOSE versus publish. That is a different request path from the one tested here.

Exercise 5: preserve a receipt after removing its writer

Task: Create a named volume practice-data. Use a disposable container to write receipt-42 to /data/receipt.txt, then read that value through a new container with a read-only mount.

Success condition: The writer is gone, but the new reader prints receipt-42.

Reference solution and explanation
docker volume create practice-data
docker run --rm --mount type=volume,src=practice-data,dst=/data \
  alpine:3.23 sh -c 'printf "receipt-42\n" > /data/receipt.txt'
docker run --rm --mount type=volume,src=practice-data,dst=/data,readonly \
  alpine:3.23 cat /data/receipt.txt

The writer uses --rm, so its container is removed when the command finishes. The named volume remains and supplies the file to the next consumer. The reader’s mount permits reading but not writing through that mount.

This proves persistence across writer removal. It does not prove that the data would survive removal of the volume, loss of the Docker host, or an application overwriting the file. Docker’s volume documentation describes the separate volume lifecycle.

Change one input: Write a second marker through another temporary writer, then read both files. Before deleting anything, identify the resource containing the data. The storage comparison adds a bind mount and container writable layer to the same kind of test.

The VM terminal shows HTTP 200 through a container name, no published-port output, and receipt-42 read from a volume

Actual acceptance checks for exercises 4 and 5. The blank port result is expected: this server has no host mapping.

Exercise 6: diagnose an intentional failure from logs

Task: Start a container named practice-log that writes starting to standard output, writes missing configuration to standard error, and exits with code 3. Diagnose it without opening a shell inside the stopped container.

Success condition: Logs contain both messages, and inspection reports exit code 3.

Reference solution and explanation
docker run --name practice-log alpine:3.23 sh -c \
  'printf "starting\n"; printf "missing configuration\n" >&2; exit 3'
docker logs practice-log
docker inspect practice-log --format '{{.State.ExitCode}}'

As in exercise 1, run the diagnostic commands after the intentional failure rather than joining them with &&. The log messages explain the simulated problem; the exit code establishes the process’s reported failure.

The relative presentation order of output and error streams can vary. Check for both messages rather than relying on their display order. Docker’s logs reference documents retrieving available container output. Real applications and logging-driver configurations can require additional investigation.

A stopped container has no running application process into which docker exec can start your troubleshooting command. Inspection and logs remain useful even when the application is no longer running.

Change one input: Change the simulated program to exit 0 under a new name. Explain why an error-looking message and a process exit code are separate evidence: applications choose both their message text and their status.

Docker inspection reports exit codes 7 and 3, and logs include starting and missing configuration

The deliberately failed containers remain inspectable. No interactive rescue shell was needed to recover these messages.

Clean up only these exercises

After completing all six reference solutions:

docker rm practice-exit practice-log
docker rm -f practice-web
docker network rm practice-net
docker volume rm practice-data

The last command deletes the practice receipt data. Remove any extra containers you created for variations by their specific names. If you skipped an exercise, omit its cleanup name. The pulled base images can remain for future practice.

Keep a short record of one failed prediction and how you corrected it. If networking or storage still feels uncertain, choose the matching activity in the LabEx Docker course. If the six checks are comfortable, add image-building practice with Custom Docker Images, then use the Kubernetes readiness guide to choose the next step.

References