Overview
This lab builds fluency with the Docker commands you will use every day. You will run a real web server in a container, watch its logs, work inside it, stop and restart it, configure it, and give it storage that outlives it. Every command is something you will meet again in the rest of the series and in industry.
Learning Objectives
By the end of this lab you will be able to:
- Run containers in the background with a name, published ports and environment variables
- Inspect running containers with ps, logs, exec, inspect and stats
- Describe the container lifecycle and predict what each command does to a container
- Explain why data written inside a container is lost when it is removed
- Keep data outside a container with bind mounts and named volumes
Concept
Most Docker commands follow the pattern docker <object> <action>, for example docker container ls or docker image rm. The most common ones also have short forms, so docker ps means docker container ls and docker run means docker container run. docker <command> --help lists every option for any command.
A container is isolated from your machine by default, including its network. A web server listening on port 80 inside a container cannot be reached from your browser until you publish the port, which tells Docker to forward a port on your machine to a port in the container.
Every container gets a thin writable layer on top of its read-only image. Anything the application writes goes there. The layer survives stopping and starting the container, but it is deleted with the container. That is deliberate. Containers are meant to be disposable, replaced instead of repaired, so data that matters must live outside them. Docker offers two ways to do that. A bind mount maps a folder on your machine into the container. A named volume is storage that Docker creates and manages itself, independent of any container.
Tasks
Task 1 - Run a web server in the background
nginx is a popular web server. Start it in a container.
docker run -d --name web -p 8080:80 nginx:1.29-alpine
| Part | Meaning |
|---|---|
-d | Detached. Run in the background and give the terminal back. |
--name web | A name to refer to the container by. Without it Docker invents one. |
-p 8080:80 | Publish a port. Always your machine first, container second. |
nginx:1.29-alpine | The image. The part after the colon is the tag, which picks a version. |
Success condition: Docker prints a long ID, and http://localhost:8080 in your browser shows Welcome to nginx!
Task 2 - See what a container is doing
docker ps
docker logs web
docker ps shows the container, its image, its status and its port mapping. docker logs shows everything the main process has written to its output. Now follow the logs live with -f, refresh the browser a few times, and press Ctrl+C to stop following. Stopping the follow does not stop the container.
docker logs -f web
Success condition: one new GET / line appears for each refresh. Containerised applications are expected to log to their output instead of to files, so that docker logs, and the platforms you will meet later, can collect them.
docker stats --no-stream
docker inspect web
docker stats shows live CPU and memory use. docker inspect prints every detail Docker knows about the container as JSON. Scroll through it and find the IP address the container was given on Docker's internal network.
Task 3 - Work inside a running container
docker exec runs an extra command inside a container that is already running. With -it and a shell you get an interactive session, which is the usual way to investigate a problem.
docker exec web nginx -v
docker exec -it web sh
Inside the container, replace the home page, then leave.
echo "<h1>Changed inside the container</h1>" > /usr/share/nginx/html/index.html
exit
Success condition: refreshing the browser shows your heading. You changed the container's writable layer. The image is unchanged.
Task 4 - Explore the container lifecycle
Use the buttons to drive a container through its lifecycle. Try commands that are not allowed in the current state as well, and read the explanations.
Now do it for real. Stop the container, show that it still exists, start it again and check the page.
docker stop web
docker ps
docker ps -a
docker start web
Success condition: after docker start the browser still shows Changed inside the container. Stopping kept the writable layer. Now remove the container and create a fresh one from the same image.
docker rm -f web
docker run -d --name web -p 8080:80 nginx:1.29-alpine
Success condition: the page is back to Welcome to nginx! Your change was in the writable layer of the old container, and docker rm deleted it. -f forces removal of a running container by stopping it first.
Task 5 - Configure containers with environment variables
Images are built once and configured when they run, usually with environment variables passed using -e. The single quotes stop your own shell from replacing $NAME before Docker sees it.
docker run --rm -e NAME=Sam alpine sh -c 'echo Hello $NAME'
Many official images refuse to start without required settings. Try to start the Postgres database without a password.
docker run --name broken-db postgres:17-alpine
docker ps -a
docker logs broken-db
Success condition: the container status is Exited (1), and the logs explain that POSTGRES_PASSWORD must be set. When a container stops unexpectedly, docker ps -a and docker logs are always the first two things to check. The image page on Docker Hub documents which variables an image expects. Remove the broken container.
docker rm broken-db
Task 6 - Keep data outside the container
A bind mount shares a folder on your machine with a container. Create a folder with a web page in it, then run nginx with that folder mounted over its web root. $(pwd) is the folder you are currently in.
mkdir -p site
echo "<h1>Served from my own folder</h1>" > site/index.html
docker rm -f web
docker run -d --name web -p 8080:80 -v "$(pwd)/site:/usr/share/nginx/html" nginx:1.29-alpine
Success condition: the browser shows Served from my own folder. Edit site/index.html in any editor, save, and refresh. The change appears immediately with no rebuild and no restart. Bind mounts are ideal during development, which you will use in the dev container lab.
A named volume is storage that Docker manages. It is the normal choice for data a container produces, such as a database. Two containers that never run at the same time can share it.
docker volume create notes
docker run --rm -v notes:/data alpine sh -c 'echo "written by the first container" >> /data/log.txt'
docker run --rm -v notes:/data alpine cat /data/log.txt
docker volume ls
Success condition: the second container prints the line written by the first, even though both containers were deleted the moment they finished. The data lives in the volume, not in any container.
-v "$(pwd)/site:/usr/share/nginx/html" # bind mount: a path on your machine
-v notes:/data # named volume: just a name
Task 7 - Tidy up
Images, stopped containers and unused volumes take disk space. See how much, then clean up.
docker system df
docker rm -f web
docker volume rm notes
docker system prune
docker system prune removes stopped containers, unused networks and dangling images, and asks first. It does not remove volumes unless you add --volumes, because volumes usually hold data you care about.
Task 8 - Command drill
Fill in each blank with the missing command or option, then check your answers.
Run nginx in the background: docker run nginx
Make port 3000 in the container reachable as port 5000 on your machine: -p
Show stopped containers as well as running ones: docker ps
Open an interactive shell in a running container called api: docker -it api sh
Watch the output of a container called api as it happens: docker logs api
Pass a setting into a container: docker run PORT=8080 myimage
Delete a container automatically when it exits: docker run alpine echo hi
Mount a named volume called pgdata at /var/lib/postgresql/data: -v
You run docker run -p 80:8080 myapp and the app listens on port 8080. Which address works in your browser?
A container was stopped with docker stop. What happened to files written inside it?
Task 9 - Evaluate
Explain why the change you made in Task 3 disappeared in Task 4, and why Docker is designed to behave this way.
When would you choose a bind mount and when a named volume?