Skip to content

Metadata Card

  • Prerequisites: Terminal basics (Ch2), Package manager (Ch5)
  • Estimated time: 40 min
  • Core difficulty:
  • Completion marker: Can write a Dockerfile, build an image, run a container; understand the difference between images and containers

Your Progress

You're in the workshop, and you've packaged your project beautifully. Dependencies are managed, code compiles cleanly. But when you hand it to a colleague — their machine doesn't have the right JDK version. Another colleague gets a Python version mismatch. A third one is on Windows and your shell script doesn't run.

You've finished your project setup — dependencies, Git, everything. A friend wants to run your project. You send them the code. They try to run it: "JDK version mismatch." "Oh, and Python 3.9 is required, not 3.8." "Also, my Windows doesn't have GNU Make."

You think: "Is there a way to package the entire environment — the operating system bits, the runtime, the dependencies, my code — into one thing they can just run?"

Docker is that "one thing."

Chapter Layers

  • Required reading: Docker image vs container, Dockerfile basics (FROM, RUN, COPY, CMD/ENTRYPOINT), building and running
  • Optional reading: Docker Compose, Volumes for persistent data, .dockerignore
  • Advanced: Multi-stage builds, Docker networking, image layer optimization

This chapter will NOT require you to understand

  • Docker Compose full syntax — just common actions
  • Kubernetes (Docker is the container runtime, K8s is the orchestrator — two different things)
  • Container security and user permission configuration

The Breakthrough · Tracing the Origins

Scenario: The Everywhere-and-Nowhere Workshop

"Let me show you something." The workshop master walks to a whiteboard and draws three boxes — one labeled "Windows," one "macOS," one "Linux." "Your workshop is in the first box. Your friend's is in the second. Your server is in the third. You need your tools to work in all three places — without rebuilding them each time."

He draws a fourth box, crossing all three. "This box is Docker. It's like a standardized shipping container — the same shape everywhere, the same interface everywhere. What you put inside — your tools, your code, the exact version of the JDK — is sealed in. It runs on any machine, in any dock."

Your setup is working perfectly on your machine. But if you send the code to someone else, or to a server, there will always be differences: different OS, different runtime versions, different environment variables.

Docker solves this. It's like a standardized shipping container. The worker at the port doesn't care whether the container is full of vacuum tubes or steel ingots — they just pick it up and put it on the ship. Container is the same shape, same handling method, anywhere in the world.

Your Task

After this chapter, you'll be able to:

  1. Write a Dockerfile — the blueprint that describes how to build your container
  2. Build an image — like making a mold of your entire environment
  3. Run a container — actually start the thing inside Docker
  4. Understand images vs containers — like a program vs a process

First Battle: Packaging the Workshop

You have a simple Python script. It reads a file, processes it, and outputs a result. You want to send it to a friend who uses Windows. They have Python installed, but version 3.6 — yours uses 3.10 syntax. You could ask them to upgrade, but maybe they have other dependencies that break.

Instead, you package the entire environment into a Docker image.

First, create a file called Dockerfile:

dockerfile
# Dockerfile — the workshop blueprint
# FROM: start from a base image (like buying a pre-built workbench)
FROM python:3.10-slim

# WORKDIR: set the working directory (clear a spot on the workbench)
WORKDIR /app

# COPY: copy your files into the image (move materials from your cabinet)
COPY requirements.txt .
COPY main.py .

# RUN: execute commands during image build (assemble while building the mold)
RUN pip install -r requirements.txt

# CMD: the default command when the container starts (what to do when powered on)
CMD ["python", "main.py"]

Language: Dockerfile How to run: docker build -t my-tool . then docker run my-tool

Step by step:

  1. python:3.10-slim is like a "Python workbench" from Docker Hub — the standard registry
  2. COPY brings your materials into the container — including your code
  3. RUN pip install installs the dependencies during the build
  4. CMD says "when this container starts, run python main.py"
bash
# Build the image (create the mold)
docker build -t my-tool .

# Run the container (start the process in the mold)
docker run my-tool

That's it. Now your friend can run your tool without installing anything except Docker. No Python version conflicts, no missing packages, no "it works on my machine."

What really happened?

When your friend does docker run my-tool, Docker:

  1. Downloads the python:3.10-slim image (if not cached)
  2. On top of it, adds the layers you defined (your files, installed packages)
  3. Starts the container in an isolated environment
  4. Runs python main.py

Your project runs inside this container, isolated from the host OS. The machine has Python 3.6? Doesn't matter — the container has Python 3.10. The machine runs Windows? Doesn't matter — the container runs Linux (the base image is Debian-based).

Second Battle: Images vs Containers

You've built your Docker image. You've run it. Now let's go deeper.

"Image? Container? I'm using these words, but I don't really know the difference." You look at the workshop master.

He picks up a cookie cutter and presses it into dough. "The cutter is the image — it's a fixed mold, defining the shape. The cookie is the container — it's what you get when you use the mold."

  • Image (docker image): The read-only template. It contains the filesystem, dependencies, and configuration. You build it once with docker build. It's a blueprint.
  • Container (docker container): A runnable instance of an image. You create it with docker run. You can start, stop, delete, and create again from the same image.

One image → Many containers.

bash
# See your images
docker images

# See running containers
docker ps

# See all containers (including stopped)
docker ps -a

Third Battle: Packing for Different Tools

Your toolkit grows. Now you have:

  • A Python tool (calculator)
  • A Java backend (order service)
  • A PostgreSQL database (data storage)

You don't want to install Python, Java, and PostgreSQL on your machine directly. You'd rather each has its own container.

dockerfile
# Dockerfile for your Java order service
FROM eclipse-temurin:17-jre

WORKDIR /app

COPY target/order-service.jar .

EXPOSE 8080

CMD ["java", "-jar", "order-service.jar"]
bash
# Build and run
docker build -t order-service .
docker run -p 8080:8080 order-service

Add -p 8080:8080 to expose the container's port 8080 to your host machine.

dockerfile
# Dockerfile for your Python calculator
FROM python:3.10-slim

WORKDIR /app

COPY calculator.py .

CMD ["python", "calculator.py"]

What about the database? You don't need to write a Dockerfile for PostgreSQL — it's already on Docker Hub:

bash
docker run --name my-postgres -e POSTGRES_PASSWORD=secret -d postgres:16

Docker Hub has official images for almost everything. PostgreSQL, MySQL, Redis, Nginx — just docker run them.

Fourth Battle: Multiple Containers Working Together

Now you have three containers. Your backend needs to connect to the database. Your Python tool needs to call your backend.

How do they find each other?

With docker compose. Create a docker-compose.yml file:

yaml
services:
 db:
 image: postgres:16
 environment:
 POSTGRES_DB: myapp
 POSTGRES_PASSWORD: secret
 volumes:
 - db_data:/var/lib/postgresql/data

 backend:
 build: ./backend
 ports:
 - "8080:8080"
 depends_on:
 - db
 environment:
 DB_URL: jdbc:postgresql://db:5432/myapp

 tool:
 build: ./tool
 depends_on:
 - backend

volumes:
 db_data:
bash
# Start everything
docker compose up -d

# Stop everything
docker compose down

Now your backend can reach the database at db:5432 (Docker's internal DNS), and your whole system runs together.

Advanced: The full Docker Compose syntax and Docker networking model are covered in Vol 0's supplementary content. For now, just know that docker compose makes multi-container apps much easier to manage.

Common Pitfalls

Story: The 2GB Image

Your first Docker image was 2GB. You copied the entire JDK, then installed Maven, downloaded all dependencies, copied your source code, compiled inside the image... That's like bringing an entire steel mill to forge a single nail.

The lesson: Multi-stage builds. Build with Maven in one stage, then copy only the final jar into a smaller runtime image.

dockerfile
# Stage 1: Build
FROM maven:3.9 AS build
WORKDIR /app
COPY pom.xml .
COPY src ./src
RUN mvn package -DskipTests

# Stage 2: Run (much smaller!)
FROM eclipse-temurin:17-jre
WORKDIR /app
COPY --from=build /app/target/order-service.jar .
CMD ["java", "-jar", "order-service.jar"]

The final image only contains the JRE and the jar. No Maven, no source code, no build dependencies. The image drops from 2GB to ~200MB.

Trap: Volume = Persistence

By default, everything in a container is ephemeral. When you delete the container, the data is gone. Like writing on a fogged-up window — it's there until the heat turns on.

If you're running a database in Docker, you need volumes:

bash
docker run -v mydata:/var/lib/postgresql/data postgres:16

Or in Docker Compose, as shown above: volumes: - db_data:/var/lib/postgresql/data. This mounts a persistent volume from your host, so even if the container is deleted, the data survives.

Final Challenge

  • Warm-up (5 min, required): Run docker run hello-world. If Docker is not installed, install it first.
  • Challenge (30 min, optional): Write a Dockerfile for [a simple project of your choice]. Build it. Run it. Change the code, rebuild, re-run.
  • Observe: Run docker images and see how many layers each image has. Run docker history <image> to see what each layer does.
  • Troubleshooting: docker build fails with "COPY failed: file not found." Check: is the file you're copying actually in the build context (the directory you passed to docker build)? Docker can only copy files from within the build context.

Checklist

  • [ ] Can explain the difference between an image and a container
  • [ ] Can write a basic Dockerfile with FROM, WORKDIR, COPY, RUN, CMD
  • [ ] Can build an image and run a container
  • [ ] Can expose a port with -p
  • [ ] Knows what Docker Compose is for
  • [ ] Knows how to use volumes for persistent data
  • [ ] Knows that each RUN in a Dockerfile creates a new image layer

No Need to Understand Now

  • Docker networking internals (bridge, host, overlay networks)
  • Docker Swarm or Kubernetes
  • Container security, user namespaces, seccomp profiles
  • Image layer sharing and caching optimization details

Traveler's Notes

Docker is like a standardized shipping container for your software.
Image = mold. Container = the thing made from the mold.
One image → many identical containers.
FROM picks your base, COPY brings your code, RUN sets it up, CMD defines its purpose.
Volumes persist data across container restarts.
Docker Compose orchestrates multiple containers as one system.

Preview of Next Stop

Docker packages your entire environment into a portable container. But how does code actually go from your editor to running on a real server? Next chapter traces the full pipeline — from git push to a running production service.

Built with VitePress | Software Systems Atlas