[Docker Series Part 2] Connecting Images and Containers with Dockerfiles

한국어 버전

Part 1 showed how Docker bridges developer workflows and infrastructure. The Dockerfile is the bridge itself. Once you know how to craft an image and how that image becomes a running container, phrases like “capture the environment as code” stop sounding abstract. This post walks through a first Dockerfile, highlights the difference between images and containers, and keeps the entire lesson hands-on.

How this post flows

  1. What a Dockerfile is responsible for
  2. Syntax basics and frequently used instructions
  3. First lab: build a personal web server image
  4. Understanding layers and the build cache
  5. Practice the image → container → data flow
  6. What to learn next

Terms introduced here

  1. Layer: A filesystem snapshot stored during an image build. Instructions such as RUN, COPY, and ADD create layers that Docker can cache and reuse.
  2. Base image: The existing image you start from when creating a new one.
  3. Build context: The folder snapshot Docker can read files from while building an image.
  4. ENTRYPOINT: The main executable that always runs when the container starts.
  5. .dockerignore: A file that excludes unneeded paths from the build context so they never reach the image.

Reading card

  • Estimated time: 18 minutes
  • Prereqs: command-line Linux basics plus a way to clone projects with Git
  • After reading: you can explain, step by step, how one Dockerfile ties images and containers together.

What a Dockerfile is responsible for

Think of a Dockerfile as a recipe that spells out which environment to build and in what order. One file captures the base image, package installs, file copies, and startup command. Anyone who builds from that file gets the same image, and that image behaves the same whenever it becomes a container.

That promise becomes much stronger when you pin exact image tags. FROM ubuntu:24.04 points to a specific Ubuntu release, while FROM ubuntu:latest can change over time. If you want reproducible builds, use stable tags on purpose instead of floating ones.

That single file delivers real advantages:

  1. Reproducible environments: The same Dockerfile always yields the same image.
  2. Version control: Managing Dockerfiles in Git records every environment change.
  3. Faster learning: Beginners can read the file and understand the build flow instruction by instruction.

Keep this mental model in mind throughout the post:

  1. Dockerfile: the text recipe
  2. docker build: the step that turns the recipe into an image
  3. Image: the reusable package stored on disk
  4. docker run: the step that starts a container from that image
  5. Container: the running process created from the image

Syntax basics and frequently used instructions

The simplest Dockerfile looks like this:

FROM ubuntu:24.04
RUN apt update && apt install -y curl
COPY . /app
CMD ["/app/run.sh"]
  • FROM: chooses the starting point. Official images such as ubuntu, alpine, node, and nginx are the usual picks.
  • RUN: executes shell commands and stores the result as the next layer.
  • COPY: copies local files from the build context into the image.
  • ADD: can also unpack local tar archives or fetch remote URLs, but COPY is the clearer default when you only need file copies.
  • CMD: sets the default command.
  • ENTRYPOINT: sets the main executable. If you define both, CMD becomes the default arguments passed to ENTRYPOINT.

You will also see these instructions constantly:

  • WORKDIR /app: sets the working directory for all following instructions.
  • ENV NODE_ENV=production: defines environment variables that are available during later build steps and in the container at runtime.
  • EXPOSE 8080: annotates which port the container uses. Remember it is only metadata—you still have to publish a port with docker run -p or a Compose ports entry.

That last point trips up many beginners. Writing EXPOSE alone does not make a service reachable from a browser, and it does not secure or restrict the port either. It simply documents the intended listening port for people and tools that use the image.

Dockerfile instructions do not all run at the same time, so separate build-time behavior from runtime behavior:

Step When it happens Examples
Build-time While docker build creates the image FROM, RUN, COPY, ADD
Runtime When docker run starts a container CMD, ENTRYPOINT

The build context also matters here. When you run docker build ., Docker uses the current folder as the build context and sends that snapshot to the builder. Large folders slow builds, which is why .dockerignore should exclude paths such as node_modules/, .git/, and *.log.

First lab: build your own web server image

Use this mini project to build Dockerfile instincts.

Before starting, make sure docker --version prints normally and port 8080 is free on your machine.

  1. Create a folder structure.
mkdir -p docker-lab/app && cd docker-lab
cat <<'EOF' > app/index.html
<!doctype html>
<h1>Hello Docker!</h1>
<p>This is my first container.</p>
EOF
  1. Write the Dockerfile.
# Dockerfile
FROM nginx:alpine
COPY ./app /usr/share/nginx/html
  1. Build and run it.
docker build -t my-nginx-lab .
docker run -d -p 8080:80 --name lab my-nginx-lab
curl http://localhost:8080
docker stop lab && docker rm lab

A short Dockerfile is enough to produce your own image, and running that image starts a web server right away. The COPY instruction literally bakes the HTML files into the image, so the container can serve them without needing your local editor or web server.

If docker run fails with a message saying port 8080 is already in use, change the command to -p 8081:80 and then open http://localhost:8081 instead.

Remember that Docker sends the entire current folder as the build context. In real projects you add .dockerignore entries such as node_modules, .git, and log files so large or sensitive paths never reach the image in the first place.

Understanding layers and the build cache

Now that you have built an image once, the cache has a job to do. Docker stores filesystem-changing steps such as RUN, COPY, and ADD as reusable layers. If nothing affecting one of those steps changes, Docker can reuse the cached result instead of rebuilding it.

Follow these cache-friendly guidelines:

  1. Place rarely changing steps near the top. Package installs and OS updates benefit most from reuse.
  2. Copy dependency files before application code. That way a source-code edit does not force a full dependency reinstall.
  3. Remember cache invalidation flows downward. If one layer changes, Docker rebuilds that layer and every layer after it.

A common pattern is copying package.json first, running npm install, and copying the rest of the source later. Changing application code no longer invalidates the dependency install cache.

Quick cache experiment

docker build -t my-nginx-lab:v1 .
printf '<p>Updated content</p>\n' >> app/index.html
docker build -t my-nginx-lab:v2 --progress=plain .

On the second build, Docker should reuse the unchanged base-image step and rebuild only the layers affected by the modified file. Look for CACHED lines in the output. Later in the series, multi-stage builds will build on this same idea to keep runtime images smaller.

Practice the image → container → data flow

If images and containers still feel blurry, try these two experiments.

1. Launch multiple containers from a single image

docker run -d --name web-a -p 8081:80 my-nginx-lab
docker run -d --name web-b -p 8082:80 my-nginx-lab

The same image spawns independent containers. Deleting one does not affect the other.

2. Attach a data volume

docker volume create html-data
docker run -d --name web-volume \
  -v html-data:/usr/share/nginx/html \
  -p 8083:80 nginx:alpine

docker exec -it web-volume sh -c "echo 'Volume Test' > /usr/share/nginx/html/index.html"
curl http://localhost:8083

docker rm -f web-volume
docker run -d --name web-volume \
  -v html-data:/usr/share/nginx/html \
  -p 8083:80 nginx:alpine
curl http://localhost:8083

Even after deleting the container, the index.html stored inside the named volume remains. That is the key distinction: containers are disposable, but volumes persist data outside the container lifecycle. Without a volume, files written only inside the container disappear when the container is removed.

What to learn next

With Dockerfile fundamentals in place, the next question is how to choose a base image. Should you stick with a familiar Ubuntu image, or switch to a lightweight Alpine image? That choice affects both learning difficulty and deployment strategy. Part 3 compares those two bases, shares a checklist you can run through on your own, and offers a Compose example to experiment with.

One last reminder: never embed API keys or passwords directly in a Dockerfile or pass them as casual build arguments. Secrets can leak into image history. Inject them later through environment variables, Docker secrets, or another dedicated secret manager.

💬 댓글

이 글에 대한 의견을 남겨주세요