What You'll Build
You will put Module 1 into practice end to end: pull images from a registry, run a real web server container with a published port, manage its lifecycle (stop, start, restart with a policy), and use the CLI trio (inspect, logs, exec) to observe and debug it — then clean up. By the end you will have run, managed, inspected, and removed containers fluently, the foundational hands-on Docker skills.
This exercise is deliberately practical and uses only the official commands from this module. You will see the client-daemon-registry flow in action, the difference between detached and interactive runs, the container lifecycle, restart policies for resilience, and how to look inside a running container — consolidating the foundations before you start building your own images.
Prerequisites
- Completion of lessons 01–05, or equivalent familiarity with images, running containers, the lifecycle, and the inspect/logs/exec commands.
- Docker installed and verified (docker run hello-world works).
- A terminal and a web browser to reach a published port.
- Basic command-line comfort.
- Internet access to pull images from Docker Hub.
Setup & Project Structure
You will work entirely from the command line using official images from Docker Hub — nginx (a web server) and alpine (a tiny Linux image) — so there is nothing to install beyond Docker itself. Verify Docker is working, then you will pull images, run containers, and manage them through their lifecycle.
The flow: pull and run nginx as a published background service, observe and debug it with the CLI trio, apply a restart policy for resilience, then run a throwaway interactive container, and finally clean everything up. Each step uses only the commands from this module, reinforcing the foundations.
# Verify Docker and pull the images you'll use
docker version # client + daemon talking?
docker run hello-world # end-to-end check
docker pull nginx:1.25 # pinned web server image (reproducible)
docker pull alpine:3.20 # tiny Linux image for quick experiments
docker images # confirm both images are present locallyStep 1 — Run and Publish a Web Server
Run nginx as a detached, named, published container so you can reach it from your browser: detached (-d) to run in the background, published (-p 8080:80) so host port 8080 reaches nginx's port 80, and named (--name web) for easy reference. Visit http://localhost:8080 to confirm the default nginx page loads — proof the container is running and reachable.
This single command exercises the whole foundation: the daemon runs a container from the pinned image, and the port mapping bridges your host to the service inside. Confirm it is running with docker ps, and note that without the -p mapping the service would run but be unreachable.
# Run nginx: detached, published on host 8080 -> container 80, named 'web'
docker run -d -p 8080:80 --name web nginx:1.25
docker ps # confirm 'web' is running (status Up, ports 8080->80)
# Open http://localhost:8080 in your browser -> the nginx welcome page
# (If unreachable: check the -p order is HOST:CONTAINER and the port is free)Step 2 — Observe and Debug with the CLI Trio
Use the three essential commands on the running container. docker logs web shows nginx's output (and docker logs -f web follows it live — refresh the browser and watch requests appear). docker inspect web reveals its configuration; extract its IP or port mapping with --format. docker exec -it web sh drops you into a shell inside the container to look around.
Inside the exec shell, explore: list nginx's config (ls /etc/nginx), check the served files (ls /usr/share/nginx/html), and confirm you are inside the container's isolated environment. Exit the shell. This step makes the container transparent — you have seen its output, its configuration, and its internals live.
docker logs web # nginx output so far
docker logs -f web # follow live; refresh the browser to see requests (Ctrl-C to stop)
docker inspect -f '{{.NetworkSettings.IPAddress}}' web # container IP
docker inspect -f '{{.State.Status}}' web # running?
docker exec -it web sh # shell INSIDE the running container
# inside: ls /etc/nginx (config)
# ls /usr/share/nginx/html (served files)
# exit (leave the shell)Step 3 — Lifecycle and Restart Policy
Manage the container's lifecycle: stop it (docker stop web) and confirm it shows as Exited in docker ps -a, then start it again (docker start web) and confirm it is back. Then replace it with a resilient version using a restart policy: remove the old one and run a new one with --restart unless-stopped, so it would auto-recover from crashes and reboots.
Demonstrate the policy's intent: a container with --restart unless-stopped auto-restarts on failure but respects a deliberate stop. Also run a quick throwaway interactive container (docker run -it --rm alpine sh) to contrast the ephemeral pattern with the long-lived service — and notice it cleans itself up on exit thanks to --rm.
# Lifecycle: stop, see it persisted, start again
docker stop web && docker ps -a # 'web' now Exited (still listed)
docker start web && docker ps # back to Up
# Replace with a resilient version (auto-restart unless deliberately stopped)
docker rm -f web
docker run -d -p 8080:80 --restart unless-stopped --name web nginx:1.25
# Contrast: a throwaway interactive container that cleans itself up
docker run -it --rm alpine:3.20 sh
# inside: echo "ephemeral!"; exit -> container is auto-removed (--rm)Step 4 — Testing & Verification
Confirm the full workflow: nginx is reachable at http://localhost:8080, you observed it via logs/inspect/exec, you stopped and started it, replaced it with a restart-policy version, and ran a self-cleaning interactive container. Finally, clean up completely — stop and remove the web container and prune any leftover stopped containers — and verify with docker ps -a that nothing remains.
# Final verification + cleanup
docker ps # 'web' running with restart policy
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:8080 # expect 200
# Clean up everything from this exercise
docker rm -f web # stop + remove the service
docker container prune -f # remove any leftover stopped containers
docker ps -a # verify: no exercise containers remain
# (Optionally remove the pulled images too)
# docker rmi nginx:1.25 alpine:3.20Warning: Remember that anything you changed inside the nginx container via docker exec (editing a config, creating a file) lives only in that container's writable layer and is lost the moment you docker rm it — as you do in cleanup. That is expected here, but it underscores the rule: never rely on a container's own filesystem for changes you need to keep. Permanent changes belong in an image (next module); persistent data belongs in a volume (covered later).
Extension Challenge: Run a second nginx container on a different host port (e.g. -p 8081:80 --name web2) from the same image, and confirm both run independently from shared image layers. Then deliberately give a container a bad command so it exits with a non-zero code, find that exit code with docker ps -a, read docker logs to see why, and observe how --restart on-failure reacts. Finally, compare docker stats across your running containers to see their resource use.
- Pull pinned images (nginx:1.25, alpine:3.20) and run a detached, published, named web server: docker run -d -p 8080:80 --name web.
- Reach a published service in the browser; without -p the service runs but is unreachable (mind HOST:CONTAINER order).
- Debug with the trio: docker logs (output, -f to follow), docker inspect (config/state, --format to extract), docker exec -it (shell inside).
- Manage the lifecycle: stop/start (stopped containers persist), and use --restart unless-stopped for a resilient service.
- Use docker run -it --rm for throwaway interactive containers that clean themselves up on exit.
- Clean up with docker rm -f and docker container prune; changes inside a container's writable layer are lost on removal.