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.0Instructions
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
*.logMulti-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 80Good habits
- Use small base images (
-slim,alpine) and pin versions. - Run as a non-root user:
RUN useradd -m appthenUSER app. - Never bake secrets into the image; pass them at runtime.
- One main process per container.
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"]