Writing a Dockerfile
Turn your own application into a portable image, one instruction at a time.
Running other peopleβs images is useful, but the real power is packaging your own app. A Dockerfile is a plain-text recipe that tells Docker how to build your image, step by step.
π― Learning Objectives
By the end of this lesson you will:
- Understand the most common Dockerfile instructions
- Write a Dockerfile for a real application
- Build and run your own image
- Apply best practices for small, cached, secure images
The Core Instructions
| Instruction | What it does |
|---|---|
FROM |
The base image to build on |
WORKDIR |
Set the working directory inside the image |
COPY |
Copy files from your project into the image |
RUN |
Execute a command at build time (install deps) |
EXPOSE |
Document which port the app listens on |
CMD |
The command that runs when the container starts |
A Real Dockerfile
Hereβs a complete Dockerfile for a small Node.js app:
# 1. Start from an official, slim base image
FROM node:20-slim
# 2. Set the working directory
WORKDIR /app
# 3. Copy dependency manifests first (better caching)
COPY package*.json ./
# 4. Install dependencies
RUN npm install --production
# 5. Copy the rest of the source code
COPY . .
# 6. Document the port the app uses
EXPOSE 3000
# 7. Command to start the app
CMD ["node", "server.js"]π‘ Why copy package.json first?
Docker caches each instruction as a layer. By copying package.json and installing deps before copying your code, Docker only re-runs npm install when your dependencies change β not on every code edit. This makes rebuilds dramatically faster.
Build and Run Your Image
docker build -t myapp:1.0 . # build, tag it myapp:1.0
docker run -d -p 3000:3000 myapp:1.0The -t flag tags the image with a name and version, so you can run and share it easily.
Best Practices
β Don't run as root
By default containers run as root, which is risky. Add a non-root user and switch to it with USER appuser before your CMD. Itβs a small change that greatly reduces the blast radius of a compromise.
A few habits that separate good images from bad:
- Use slim/alpine base images β smaller images pull faster and have fewer vulnerabilities.
- Add a
.dockerignoreβ keepnode_modules,.git, and secrets out of the build context. - Order layers by change frequency β least-changing instructions first for maximum cache reuse.
- One process per container β a container should do one job well.
- Use multi-stage builds β compile in a big image, then copy only the artifact into a tiny final image.
# Multi-stage build: build stage + tiny runtime stage
FROM node:20 AS build
WORKDIR /app
COPY . .
RUN npm ci && npm run build
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/htmlπ§ͺ Hands-on Lab
Containerise a Static Site
- Create an
index.htmlwith any content - Write a Dockerfile based on
nginx:alpinethat copies it into the web root - Build the image as
mysite:1.0and run it on port 8080
π§ Knowledge Check
Why do experienced engineers copy package.json and install dependencies before copying the rest of the code?
What does a multi-stage build achieve?
πΌ Interview Preparation
How would you reduce the size of a Docker image?
Summary
You can now write a Dockerfile, build and tag your own image, and apply best practices for small, cached, secure builds. Next, youβll run multiple containers together β app plus database β using Docker Compose.