Docker Course
Dockerfile Introduction
Up until now, you've been running images other people built. Section II is where you start building your own. A Dockerfile is the recipe — and this lesson teaches you exactly how to read and write one.
Every Docker image you've ever pulled — nginx, postgres, node, redis — started as a Dockerfile. Someone wrote a set of instructions, ran docker build, and pushed the result to Docker Hub. By the end of Section II, you'll be doing the exact same thing with your own applications.
A Dockerfile Is a Build Script
A Dockerfile is a plain text file — no extension, just the name Dockerfile — containing a sequence of instructions that tell the Docker Daemon how to build an image. Each instruction becomes a layer in the final image.
The Daemon reads the Dockerfile top to bottom, executes each instruction in order, and stacks the resulting layers into a complete image. When you run docker build, that's what's happening under the hood — the Daemon is following your recipe, one step at a time.
The Chef's Recipe Analogy
A Dockerfile is a list of instructions sent to a chef. Each instruction creates a new action on top of the previous one — start with a base kitchen (FROM), install your equipment (RUN), bring in your ingredients (COPY), and tell the chef what to cook when the restaurant opens (CMD). The final dish — the image — is the result of all those instructions executed in order. Hand the recipe to any kitchen in the world and they'll produce the identical dish.
The Core Dockerfile Instructions
A Dockerfile has many possible instructions, but six of them cover the vast majority of real-world use cases. Learn these six and you can Dockerize almost any application.
FROM
Sets the base image
Every Dockerfile must start with FROM. It defines the starting point — the OS and runtime your image is built on top of. Example: FROM node:18-alpine.
RUN
Executes a command during the build
Runs a shell command inside the image at build time — installing packages, creating directories, compiling code. Each RUN creates a new layer. Example: RUN npm install.
COPY
Copies files from host into the image
Takes files from your local machine (the build context) and places them inside the image filesystem. Example: COPY . /app copies everything in the current directory to /app inside the image.
WORKDIR
Sets the working directory
All subsequent RUN, COPY, and CMD instructions execute relative to this directory. Creates the directory if it doesn't exist. Example: WORKDIR /app.
EXPOSE
Documents the port the app listens on
Tells Docker and other developers which port the containerised application uses. It's documentation — it does not actually publish the port. You still need -p in docker run to expose it to the host. Example: EXPOSE 3000.
CMD
Sets the default command to run
Defines the command that runs when a container starts from this image. Only one CMD is allowed — the last one wins. It can be overridden at runtime. Example: CMD ["node", "server.js"].
Reading a Real Dockerfile
The scenario: You've just joined a startup as a backend developer. The team's Node.js API already has a Dockerfile in the repository. Before you run anything, you want to understand exactly what it does — every instruction, every decision, every layer it creates.
FROM node:18-alpine
# Start from the official Node 18 image built on Alpine Linux
# Alpine keeps the base image tiny — ~35 MB vs ~900 MB for the full Debian-based node image
WORKDIR /app
# All subsequent instructions run relative to /app inside the container
# If /app doesn't exist, Docker creates it automatically
COPY package*.json ./
# Copy package.json and package-lock.json (the * matches both) into /app
# We copy these BEFORE the source code — this is a deliberate caching trick (more in Lesson 14)
RUN npm install
# Install all Node.js dependencies declared in package.json
# This runs at BUILD time, not at runtime — the node_modules end up baked into the image
COPY . .
# Now copy the rest of the application source code into /app
# The dot on the right means "current WORKDIR" — i.e. /app
EXPOSE 3000
# Document that the app listens on port 3000
# This is metadata — it does not publish the port to the host
CMD ["node", "server.js"]
# The default command to run when a container starts from this image
# Array syntax (exec form) is preferred — it runs node directly, not through a shell
[+] Building 18.3s (9/9) FINISHED => [internal] load build definition from Dockerfile 0.0s => [internal] load .dockerignore 0.0s => [internal] load metadata for docker.io/library/node:18-alpine 1.2s => [1/5] FROM node:18-alpine 3.4s => [2/5] WORKDIR /app 0.0s => [3/5] COPY package*.json ./ 0.1s => [4/5] RUN npm install 11.8s => [5/5] COPY . . 0.2s => exporting to image 1.6s => => naming to docker.io/library/payment-api:latest 0.0s
What just happened?
The build output shows five numbered steps — each one corresponds to an instruction in the Dockerfile. Step 1 is WORKDIR (FROM doesn't count as a build step — it's the base). Step 4 (RUN npm install) took 11.8 seconds — installing dependencies is always the slowest step, which is exactly why you want it cached. The final line names the resulting image. Each step that completed added a new read-only layer to the image. The total build time of 18.3 seconds includes pulling the base image — on the second build, that layer is cached and the total drops dramatically.
CMD vs RUN — The Confusion Everyone Hits
Every beginner confuses RUN and CMD at least once. Here's the clearest way to think about it:
RUN
Runs at build time
Executed when you run docker build. The result is baked into the image as a layer. Use it to install software, create files, compile code — anything that prepares the environment.
Example: RUN apt-get install -y curl
CMD
Runs at container start
Executed when someone runs docker run on the image. It's the default startup command. Not baked into the image — it's metadata that tells Docker what to launch.
Example: CMD ["node", "server.js"]
EXPOSE Does Not Open Ports
EXPOSE 3000 in your Dockerfile is documentation, not configuration. The port is not accessible from your host machine until you add -p 3000:3000 to your docker run command. Think of EXPOSE as leaving a sticky note — it tells other developers which port to map, but it doesn't do the mapping itself.
Teacher's Note
The order of instructions in a Dockerfile matters enormously for build speed. Lesson 14 covers layer caching in depth — but the single biggest habit to build right now is always copying package.json and running npm install before copying your source code.
Practice Questions
1. Every Dockerfile must begin with which instruction that sets the base image?
2. To install a package inside an image during the build process, which Dockerfile instruction do you use?
3. The Dockerfile instruction that defines the default command to run when a container starts is called what?
Quiz
1. What is the core difference between the RUN and CMD instructions in a Dockerfile?
2. A Dockerfile contains EXPOSE 8080. What does this actually do?
3. A Dockerfile contains WORKDIR /app followed by COPY . . — where does the COPY instruction place the files?
Up Next · Lesson 12
Writing Your First Dockerfile
You've read a Dockerfile — now you write one from scratch, build the image, and run a container from your own creation for the first time.