Dockerfile Basics

What Is a Dockerfile?

A Dockerfile is a plain text file that lists step-by-step instructions for building a Docker image — think of it as a recipe your app follows every time you build. Docker reads it top to bottom; each instruction typically creates a new image layer that gets cached for faster rebuilds.

Dockerfile vs Docker Image

The Dockerfile is the blueprint; the Docker image is the finished product built from that blueprint. You write the Dockerfile once, run docker build, and Docker produces a reusable image you can run as many containers as you need.

From Dockerfile to image to running container

A Simple Node.js Dockerfile

Here is a realistic Dockerfile for a small Express API — notice how dependency files are copied before source code so Docker can cache the npm ci layer:

FROM node:22-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY . .
ENV NODE_ENV=production
EXPOSE 3000
CMD ["node", "server.js"]

All 15 Dockerfile Instructions

Every instruction below is something you will encounter in real Dockerfiles. Each entry covers what it does, a minimal example, and a practical note.

#InstructionWhat it doesExamplePractical note
1FROMStarts the image; downloads the base if not available locallyFROM ubuntu:22.04Required — every Dockerfile must begin with FROM
2LABELAdds metadata (author, version, project, description)LABEL maintainer="you@example.com"Useful for documentation and image management tools
3ARGBuild-time variable — available only during docker buildARG VERSION=1.0Override with docker build --build-arg VERSION=2.0 .
4ENVSets environment variables for build and runtimeENV APP_ENV=productionYour app reads these via process.env or equivalent
5WORKDIRSets the working directory; creates it if missingWORKDIR /appAll following commands execute from this path
6COPYCopies local files into the imageCOPY app.py /app/Default choice — simple, predictable, no remote URLs
7ADDLike COPY, plus auto-extracts tar archives and fetches URLsADD app.tar.gz /app/Prefer COPY unless you need extraction or a remote file
8RUNExecutes a command during the build; creates a new layerRUN apt-get update && apt-get install -y nginxUse for installing packages, compiling, or any setup step
9EXPOSEDocuments which port the app listens onEXPOSE 8080Does not publish the port — you still need docker run -p 8080:8080
10VOLUMEDeclares a mount point for persistent dataVOLUME /dataData here survives container recreation — good for DBs, logs, uploads
11USERRuns subsequent commands as a non-root userUSER appuserImproves security — avoid running as root in production
12HEALTHCHECKDefines how Docker checks if the app is healthyHEALTHCHECK CMD curl --fail http://localhost:8080 || exit 1Docker reports: starting → healthy or unhealthy
13CMDDefault command when a container startsCMD ["python", "app.py"]Fully overridable — docker run image-name bash replaces it
14ENTRYPOINTDefines the main executable; args are appended, not replacedENTRYPOINT ["python"]See CMD vs ENTRYPOINT below
15ONBUILDRuns when this image is used as a base in another DockerfileONBUILD COPY . /appUseful for reusable base images that child images extend

Instruction Deep Dives

FROM, LABEL, and ARG — Starting the Build

FROM is non-negotiable — it tells Docker which base image to extend. Pin a specific version (node:22-alpine) instead of latest so rebuilds stay predictable.

LABEL is optional but helpful for tagging who maintains the image, what version it represents, or what project it belongs to.

ARG variables exist only at build time. They are ideal for passing a version number or build flag into RUN commands, but they do not persist when the container runs. Use ENV if the value needs to survive into runtime.

COPY vs ADD — When to Use Which

Both copy files into the image, but they are not interchangeable:

  • COPY — copies files and directories from your build context. No surprises. Use this by default.
  • ADD — can auto-extract a local .tar.gz into the destination and download files from a URL. The extra behaviour can be unexpected, which is why most teams reach for COPY first and only switch to ADD when extraction or remote download is genuinely needed.

RUN — Building Layers

Every RUN instruction creates a permanent layer in the image. Chain commands with && in a single RUN to keep layer count down:

RUN apt-get update && apt-get install -y nginx && rm -rf /var/lib/apt/lists/*

That one line installs nginx and cleans up in a single layer instead of three.

EXPOSE and VOLUME — Runtime Hints

EXPOSE is documentation, not magic. It tells humans and tools which port your app uses, but nothing is reachable from outside the container until you publish it:

docker run -p 8080:8080 my-image

VOLUME marks a path as persistent storage managed by Docker. If a database writes to /data and that path is a volume, stopping and recreating the container keeps the data intact.

CMD vs ENTRYPOINT — How Containers Start

These two instructions work together and are the most commonly confused pair in Dockerfiles.

CMD alone — sets the default command. Fully replaceable at run time:

CMD ["python", "app.py"]
docker run myapp bash          # runs bash instead of python app.py

ENTRYPOINT + CMD — ENTRYPOINT locks in the executable; CMD supplies default arguments that docker run args override:

ENTRYPOINT ["python"]
CMD ["app.py"]
docker run myapp               # runs: python app.py
docker run myapp test.py       # runs: python test.py

Use ENTRYPOINT when the container should always run the same binary (a CLI tool, a server process). Use CMD alone when you want flexibility to override the entire command at run time.

HEALTHCHECK — Knowing When Apps Fail

A healthcheck gives Docker a way to detect a running but broken app — one that started successfully but stopped responding:

HEALTHCHECK CMD curl --fail http://localhost:8080 || exit 1

Docker cycles through three states: starting (waiting for first check), healthy (check passed), and unhealthy (check failed). Orchestrators like Compose and Kubernetes use this to restart or reroute traffic away from failing containers.

ONBUILD — Base Images That Trigger on Extend

ONBUILD instructions do not run when you build the base image. They run when someone else uses your image as their FROM:

# In a base image Dockerfile:
ONBUILD COPY . /app

This pattern is common in language base images that expect the child Dockerfile to supply the application code.

Quick Reference

InstructionPurposeInstructionPurpose
FROMBase imageEXPOSEDocument application port
LABELMetadataVOLUMEPersistent storage
ENVEnvironment variablesUSERRun as non-root user
WORKDIRWorking directoryHEALTHCHECKCheck container health
COPYCopy local filesCMDDefault startup command
ADDCopy + extract archivesENTRYPOINTMain executable
RUNExecute build commandsARGBuild-time variable
ONBUILDTrigger for child images

Best Practices

  • Use a minimal base image — Alpine, Debian-slim, or distroless keep images small and reduce attack surface.
  • Combine multiple RUN commands with && in a single instruction to reduce layer count.
  • Add a .dockerignore file to exclude node_modules, .git, and other files that bloat the build context.
  • Prefer COPY over ADD unless you specifically need archive extraction or a remote URL.
  • Run containers as a non-root USER whenever possible.
  • Pin image versions — python:3.12-slim instead of latest — so builds stay reproducible across machines and CI.
  • Place frequently changing instructions (like COPY . .) near the end of the Dockerfile to maximise build cache hits.
  • Use multi-stage builds to keep compilers and build tools out of your production image.
  • Add a HEALTHCHECK for production workloads so Docker can detect and report failing apps.
  • Scan your Dockerfile for best-practice violations, and never hard-code secrets — pass them at run time via environment variables or a secrets manager.

Key Takeaway A Dockerfile is a layered recipe read top to bottom: FROM picks your base, COPY and RUN assemble the app, EXPOSE and ENV document runtime config, and CMD or ENTRYPOINT start the process. Know all 15 instructions, understand when to use each, and follow the best practices to build images that are small, secure, and fast to rebuild.