Learn / Programming / Docker / Writing a Dockerfile

Intermediate 16 min

Writing a Dockerfile

Build your own image, use layer caching and keep images small.

What you will learn

  • Write a Dockerfile
  • Use .dockerignore
  • Use multi-stage builds

A Dockerfile is a text file of instructions for building your own image. Each instruction adds a layer. Docker caches layers, so ordering the file well makes rebuilds very fast.

FROM python:3.12-slim

WORKDIR /app

# copy dependency list first so this layer is cached
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# then the source code (changes often)
COPY . .

EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
docker build -t my-api:1.0 .
docker run -d -p 8000:8000 my-api:1.0

Instructions

  • FROM: the base image to start from.
  • WORKDIR: set the working directory (creates it if needed).
  • COPY: copy files from your project into the image.
  • RUN: execute a command at build time, such as installing packages.
  • ENV: set an environment variable.
  • EXPOSE: document the port the app listens on (does not publish it).
  • CMD / ENTRYPOINT: the command that runs when the container starts.

Why order matters

When you change a file, Docker rebuilds from the first changed layer onward. Copying requirements.txt and installing dependencies before copying the rest of your code means everyday code edits reuse the cached dependency layer, cutting build time from minutes to seconds.

.dockerignore

Keep junk and secrets out of the build context:

.git
.venv
__pycache__
node_modules
.env
*.log

Multi-stage builds

Build tools such as compilers are needed to build the app but not to run it. A multi-stage build compiles in one stage and copies only the result into a small final image.

# Stage 1: build
FROM node:20 AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Stage 2: serve
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80

Good habits

  • Use small base images (-slim, alpine) and pin versions.
  • Run as a non-root user: RUN useradd -m app then USER app.
  • Never bake secrets into the image; pass them at runtime.
  • One main process per container.
Shell form vs exec form

Prefer CMD ["python", "app.py"] (exec form). It receives stop signals correctly, so docker stop shuts the app down cleanly.

Try it yourself

Write a Dockerfile for a Node.js app that installs dependencies with npm ci, copies the code and starts node server.js on port 3000, using a cache-friendly order.

Show solution
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
EXPOSE 3000
USER node
CMD ["node", "server.js"]