Docker
This activity puts into practice the concepts from the Containerization with Docker lecture. You will investigate how containers actually behave: namespace isolation, ephemeral storage, volumes, image layer caching, port mapping, and multi-service Compose stacks. By the end, you will have run, built, and inspected containers and seen for yourself exactly what isolation they provide and where it ends.
What You Will Need
Section titled “What You Will Need”You need a container runtime installed on your laptop before starting.
On macOS, Docker Engine runs inside a Linux VM managed transparently by the tool you choose. All of the following expose the same docker CLI and are compatible with this activity.
Docker Desktop: Download from docker.com/products/docker-desktop. After installation, start Docker Desktop from your Applications folder and wait for the whale icon in the menu bar to show “Docker Desktop is running.”
OrbStack (lightweight alternative): Download from orbstack.dev. OrbStack runs a lightweight Linux VM with lower memory overhead and faster startup than Docker Desktop. All docker commands work without modification.
Podman Desktop: Download from podman-desktop.io. Provides a Podman-based environment; most docker commands work with podman substituted.
Rancher Desktop: Download from rancherdesktop.io. Free and open-source; bundles either containerd or dockerd.
Verify your installation:
docker versiondocker run --rm hello-worldOn Linux, Docker Engine and Podman run natively against the host kernel with no VM required.
Docker Engine: Follow the official installation guide for your distribution at docs.docker.com/engine/install/. After installing, add your user to the docker group so you can run commands without sudo:
sudo usermod -aG docker $USER# Log out and back in for the group change to take effectPodman (rootless alternative): Available in most package managers:
# Ubuntu/Debiansudo apt install -y podman
# Fedora / AlmaLinux / RHELsudo dnf install -y podman
# Arch Linuxsudo pacman -S podmanPodman runs rootless by default: no daemon, no sudo required.
Rancher Desktop: Download from rancherdesktop.io. Provides a GUI and bundles either containerd or dockerd.
Verify:
docker version # or: podman versiondocker run --rm hello-worldOn Windows, Linux containers require a WSL2-backed Linux VM. All of the following use WSL2.
Docker Desktop: Download from docker.com/products/docker-desktop. Docker Desktop will offer to enable WSL2 during installation if it is not already configured. After installation, open a PowerShell terminal and verify:
docker versiondocker run --rm hello-worldDocker Engine in WSL2 without Docker Desktop: Install a WSL2 Linux distribution (e.g., Ubuntu from the Microsoft Store), then follow the Docker Engine Linux install guide inside that distro. This gives you a full Docker Engine without the Docker Desktop application.
Podman Desktop: Download from podman-desktop.io. Provides a Podman environment using a WSL2-backed VM. All docker commands work with podman substituted.
Rancher Desktop: Download from rancherdesktop.io. Free and open-source; uses WSL2 and bundles either containerd or dockerd.
Pulling and Inspecting Images
Section titled “Pulling and Inspecting Images”Before running anything, let us look at what an image actually is.
-
Check which cgroup version your container runtime is using:
Terminal window docker info | grep -Ei 'Cgroup'Most modern Linux distributions and recent Docker Desktop setups report cgroup v2. If you are using Podman,
podman infoalso reports the cgroup version in its host information. -
Pull two variants of the nginx image:
Terminal window docker pull nginx:alpinedocker pull nginx:latest -
Compare their sizes:
Terminal window docker images nginxRecord the size of each. The
alpinevariant should be considerably smaller. -
Inspect the layers of the alpine image:
Terminal window docker history nginx:alpineEach row is one layer. The columns show the instruction that created it, when it was built, and how much disk space it added.
Record:
- How many layers does
nginx:alpinehave? - Which layer is the largest, and what instruction created it?
- Why are some layers listed with size
0B?
- How many layers does
-
Examine the image’s metadata:
Terminal window docker inspect nginx:alpineIn the JSON output, find:
ExposedPorts: what port does the image declare as its service port?EntrypointandCmd: what command runs when a container starts from this image?Env: what environment variables are set by default?
Inside the Container: Namespace Isolation
Section titled “Inside the Container: Namespace Isolation”A running container is an isolated process with its own view of the system. This section makes that isolation visible.
-
Start an nginx container in the background and then open an interactive shell inside it:
Terminal window docker run -d --name sandbox nginx:alpinedocker exec -it sandbox shYou are now inside the container’s namespace.
-
Explore what the container can see:
Terminal window # What is this machine's hostname?hostname# Who is the current user?whoami# What processes are running?ps aux# What does the filesystem look like?ls /# What OS is this?cat /etc/os-releaseps auxlists running processes:aincludes processes from all users,ushows the user-oriented format with CPU and memory columns, andxincludes processes not attached to a terminal. Inside the container you should see only a handful of nginx processes. Record each result. -
Exit the container and run the same commands on your host machine:
Terminal window exithostnamewhoamips auxcat /etc/os-releaseThe host
ps auxoutput will be considerably longer than what you saw inside the container. Every daemon, shell session, and background service on your machine is visible here; inside the container you saw only the processes that belong to it.
Container Lifecycle and Configuration
Section titled “Container Lifecycle and Configuration”Now that you have seen what isolation looks like from inside, look at container management from the outside: checking logs, monitoring resource usage, stopping and restarting without discarding, and passing configuration in at runtime.
-
The
sandboxcontainer from the previous section is still running. Generate an HTTP request inside it to produce a log entry, then view the logs from the host:Terminal window docker exec sandbox wget -qO- http://localhost > /dev/nulldocker logs sandboxwgetdownloads a URL from the command line.-qsuppresses progress output;-O-writes the response to standard output instead of saving it to a file.docker logsshows everything the container has written to stdout and stderr since it started. For nginx, that is the access log. Follow logs in real time with-f; press Ctrl+C to stop:Terminal window docker logs -f sandbox -
Check resource usage across all running containers:
Terminal window docker stats --no-stream--no-streamprints one snapshot and exits instead of staying in interactive mode. You will see CPU percentage, memory usage, and network I/O for each container. -
Stop the container and observe its status:
Terminal window docker stop sandboxdocker psdocker ps -adocker psshows only running containers;sandboxis gone from that list.docker ps -ashows all containers including stopped ones;sandboxappears with statusExited. The container is paused, not erased. -
Restart it:
Terminal window docker start sandboxdocker psThe same container is running again. Its writable layer was preserved through the stop and start.
-
Pass configuration into a one-off container at runtime.
-esets an environment variable inside the container;--rmremoves the container automatically when it exits:Terminal window docker run --rm -e GREETING="hello from the host" alpine sh -c 'echo $GREETING'You should see
hello from the hostprinted.--rmis useful for short-lived commands that produce output and do not need to persist. -
Clean up:
Terminal window docker stop sandboxdocker rm sandbox
Ephemeral Storage: What Containers Don’t Keep
Section titled “Ephemeral Storage: What Containers Don’t Keep”A container’s writable layer disappears when the container is removed. This section makes that concrete.
-
Run a container and write something to its filesystem:
Terminal window docker run -d --name ephemeral nginx:alpinedocker exec ephemeral sh -c 'echo "this will not survive" > /tmp/testfile.txt'docker exec ephemeral cat /tmp/testfile.txtConfirm the file is there.
-
Inspect what changed in the container’s writable layer:
Terminal window docker diff ephemeraldocker diffprefixes each path with a status:Ameans added,Cmeans changed, andDmeans deleted. In this example you should seeA /tmp/testfile.txt; you may also seeCentries for files nginx touched while running.Dis not used in this step, but you would see it if a file or directory from the container filesystem had been removed. -
Stop and remove the container completely:
Terminal window docker stop ephemeraldocker rm ephemeral -
Start a fresh container from the same image:
Terminal window docker run -d --name ephemeral2 nginx:alpinedocker exec ephemeral2 cat /tmp/testfile.txtThe file is gone.
-
Clean up the second container:
Terminal window docker stop ephemeral2docker rm ephemeral2
Persistent Storage with Volumes
Section titled “Persistent Storage with Volumes”Volumes live outside the container’s writable layer and survive container replacement.
-
Create a named volume:
Terminal window docker volume create mydatadocker volume ls -
Run a container with the volume attached and write to it:
Terminal window docker run -d --name writer -v mydata:/data nginx:alpinedocker exec writer sh -c 'echo "persisted across containers" > /data/record.txt'docker exec writer cat /data/record.txt -
Remove the container and start a completely new one with the same volume:
Terminal window docker stop writerdocker rm writerdocker run -d --name reader -v mydata:/data nginx:alpinedocker exec reader cat /data/record.txtThe data is still there, even though the original container no longer exists.
-
Find where Docker stores the volume on your host:
Terminal window docker volume inspect mydataLook for the
Mountpointfield. On Linux, you can read the files directly at that path (as root). On macOS and Windows, this path is inside the Linux VM that Docker Desktop manages. -
Clean up:
Terminal window docker stop readerdocker rm readerdocker volume rm mydata
Building an Image and Observing Layer Caching
Section titled “Building an Image and Observing Layer Caching”This section shows how Docker’s layer cache behaves in practice: both when it helps and when it gets invalidated.
-
Create a working directory and add two files:
Terminal window mkdir mysite && cd mysiteCreate
index.html:<h1>Hello from my container</h1>Create
Dockerfile:FROM nginx:alpineRUN echo "server_tokens off;" > /etc/nginx/conf.d/security.confCOPY index.html /usr/share/nginx/html/index.htmlserver_tokens offis a real nginx security setting: it removes the server version number from response headers. -
Build the image:
Terminal window docker build -t mysite:v1 .This is the first build, so nothing is cached. You will see each step execute from scratch. Note the layer IDs in the output.
-
Build again without changing anything:
Terminal window docker build -t mysite:v1 .Every step should now say
CACHED. Docker reused all layers from the first build. -
Change only
index.html(edit the file to say something different), then rebuild:Terminal window docker build -t mysite:v2 .The
RUNstep saysCACHED. Only theCOPYstep re-runs, because that is the first step whose inputs changed. -
Now change the
FROMline in the Dockerfile toFROM nginx:latestand rebuild:Terminal window docker build -t mysite:v3 .FROMnow resolves to a different base image. Both theRUNstep and theCOPYstep must re-run: every cached layer was built on top of the previous base image, so none of them can be reused. -
Run the resulting image and verify it serves your content:
Terminal window docker run -d -p 8080:80 --name mysite mysite:v3curl http://localhost:8080docker stop mysite && docker rm mysiteClean up your working directory when done:
cd .. && rm -rf mysite
Port Mapping and Container Networking
Section titled “Port Mapping and Container Networking”Containers have their own network namespace. Port mapping is how services inside that namespace become reachable from outside.
-
Start a container with port mapping and verify you can reach it from your host:
Terminal window docker run -d -p 8080:80 --name webserver nginx:alpinecurl http://localhost:8080curlmakes an HTTP request from the command line and prints the response body. You made a request to your host on port 8080; Docker forwarded it to the container on port 80. -
Inspect the container’s network configuration:
Terminal window docker inspect webserverIn the
NetworkSettingssection, find:IPAddress: the container’s private IP on the Docker bridge networkPorts: the port mapping Docker configured
-
Try to reach the container’s private IP directly from your host:
Terminal window # Substitute the IP address you found in step 2curl http://<container-ip>:80On Linux with Docker Engine this will work. On macOS and Windows with Docker Desktop it likely will not, because the bridge network is inside a Linux VM.
-
List the Docker networks on your system and inspect the default bridge:
Terminal window docker network lsdocker network inspect bridgeFind the
webservercontainer listed underContainers. Note the bridge network’s subnet. -
Create a user-defined network and run two containers on it:
Terminal window docker network create mynetdocker run -d --network mynet --name server1 nginx:alpinedocker run -d --network mynet --name server2 nginx:alpine# From server2, reach server1 by container namedocker exec server2 wget -qO- http://server1Container DNS resolves
server1to its IP withinmynet. -
Confirm that containers on different networks cannot reach each other:
Terminal window # webserver is on the default bridge, server1 is on mynetdocker exec server2 wget -qO- http://webserver 2>&1This should fail or time out:
webserveris not onmynet. -
Clean up:
Terminal window docker stop webserver server1 server2docker rm webserver server1 server2docker network rm mynet
A Multi-Service Stack with Compose
Section titled “A Multi-Service Stack with Compose”Compose wires multiple containers together with automatic networking and a single declarative file. This section uses nginx as a web frontend and Redis as an internal cache, a common real-world pairing. Redis is a fast in-memory data store that should never be exposed directly to the network; only the application that uses it should be able to reach it.
-
Create a working directory and write a
compose.yml:Terminal window mkdir mystack && cd mystackmkdir htmlecho "<h1>Compose stack</h1>" > html/index.htmlcompose.yml:services:web:image: nginx:alpineports:- "8080:80"volumes:- ./html:/usr/share/nginx/html:rodepends_on:- cachecache:image: redis:alpineTwo things to notice: the
:rosuffix on the bind mount makes it read-only inside the container, so nginx can read your files but cannot modify them. Thedepends_onentry tells Compose to startcachebeforeweb. -
Start the stack:
Terminal window docker compose up -dWatch the output: Compose starts
cachefirst, thenweb, respecting thedepends_onorder. Confirm this in the startup logs:Terminal window docker compose logs -
Inspect what is running:
Terminal window docker compose psLook at the
PORTScolumn.webshows0.0.0.0:8080->80/tcp;cacheshows nothing. That absence tells youcacheis not reachable from outside the Compose network. -
Reach the web service from your host:
Terminal window curl http://localhost:8080 -
Verify the cache service is running.
redis-cliis the Redis command-line client, included in theredis:alpineimage:Terminal window docker compose exec cache redis-cli pingRedis responds with
PONG. Now store and retrieve a value to confirm it is working:Terminal window docker compose exec cache redis-cli set greeting "hello from cache"docker compose exec cache redis-cli get greeting -
Confirm that the
webcontainer can resolve thecachehostname via Compose’s embedded DNS:Terminal window docker compose exec web nslookup cacheYou should see the IP address assigned to the
cachecontainer within the Compose network. Containers on the same Compose network reach each other by service name; the host cannot. -
Upgrade the web service. Open
compose.ymland change thewebimage fromnginx:alpinetonginx:latest, then pull and recreate:Terminal window docker compose pulldocker compose up -dRead the output carefully. You will see something like:
✔ Container mystack-cache-1 Running✔ Container mystack-web-1 StartedRunningmeans the container was already up and was left alone.Startedmeans it was stopped, replaced with a new container from the updated image, and started again. Compose determines which containers to recreate by comparing the running container’s image digest against what was just pulled. -
Verify the web content is still served:
Terminal window curl http://localhost:8080The HTML is unchanged. It lives in
html/on your host; replacing the web container had no effect on it. -
Stop the stack and confirm what persists:
Terminal window docker compose downdocker compose ps # containers are gonels html/ # bind-mounted host directory is unchanged -
Clean up:
Terminal window cd .. && rm -rf mystack
Your Image
Section titled “Your Image”You have explored existing images, observed isolation, built a layered image, and run a Compose stack. Now build an image that carries your identity and applies the best practices from the lecture: a non-root user and a realistic server process.
-
Create a working directory and an HTML file with your ONID:
Terminal window mkdir myapp && cd myappecho "<h1>Built by: ONID@oregonstate.edu</h1>" > index.html -
Create a
Dockerfile:FROM python:3.12-slimRUN useradd --create-home appuserWORKDIR /home/appuser/appCOPY index.html .USER appuserEXPOSE 8080CMD ["python3", "-m", "http.server", "8080"]python3 -m http.server 8080is Python’s built-in static file server. It serves any files in the working directory over HTTP on port 8080. Because it does not need to bind a privileged port, it can run as a non-root user. -
Build and run the image:
Terminal window docker build -t myapp:v1 .docker run -d -p 9090:8080 --name myapp myapp:v1 -
Confirm your content is served:
Terminal window curl http://localhost:9090You should see your ONID in the response.
-
Verify the container process is running as a non-root user:
Terminal window docker exec myapp whoamiYou should see
appuser, notroot. -
Tag the image in the format a registry expects. Replace
YOURONIDwith your actual ONID:Terminal window docker tag myapp:v1 YOURONID/myapp:v1docker images myappBoth tags point to the same image ID. The
username/repository:tagformat is what Docker Hub expects; a private registry would useregistry.example.com/repository:tag. No account or login is needed to tag locally. -
Inspect the image layers:
Terminal window docker history myapp:v1You will see several base layers from
python:3.12-slim, then your own layers on top: theRUN useradd,WORKDIR,COPY,USER, andCMDentries. -
Clean up:
Terminal window docker stop myappdocker rm myappcd .. && rm -rf myapp
Going Further
Section titled “Going Further”You have worked through the core mechanics of containers from scratch. The natural next step is to build something real.
The Docker documentation’s Workshop guide is the most direct continuation of this activity. It walks you through containerizing a complete application, adding a database with Compose, and pushing the finished image to Docker Hub. Plan about an hour for it.
If you would rather start from your own code, pick a project in a language you already know and write a Dockerfile for it from scratch. Start with a single-stage build that runs correctly, then improve it step by step: add a multi-stage build to strip the build toolchain from the final image, shrink the base image, add a non-root USER, and add a HEALTHCHECK. Run docker images after each change and watch what happens to the size. The gap between a first-draft image and an optimized one is often an order of magnitude.
Two tools worth knowing once you are writing real Dockerfiles:
- hadolint is a Dockerfile linter that catches common mistakes and anti-patterns, from wrong
COPYordering to unintentional package cache bloat. Run it against any Dockerfile you write. - dive lets you explore an image layer by layer in the terminal and shows exactly which files each instruction added, removed, or changed. It is the fastest way to understand why an image is larger than you expect.