Writing Dockerfiles
A Dockerfile is a text file of instructions Docker uses to build an image layer by layer. Each instruction creates a cached layer; order matters for build speed and image size.
For build and run commands, see the Docker CLI Cheat Sheet.
1. Basic structure
Section titled “1. Basic structure”Create a file named Dockerfile (no extension) in your project root:
# Base imageFROM eclipse-temurin:21-jdk-alpine
# Working directory inside the containerWORKDIR /app
# Copy source and buildCOPY pom.xml .COPY src ./srcRUN mvn package -DskipTests
# Runtime commandCMD ["java", "-jar", "target/app.jar"]Build it:
docker build -t my-java-app .2. Instructions
Section titled “2. Instructions”| Instruction | Description |
|---|---|
FROM <image> | Base image for all following layers (required as first instruction) |
WORKDIR <path> | Set the working directory for RUN, CMD, ENTRYPOINT, COPY, and ADD |
COPY <src> <dest> | Copy files from the build context into the image |
ADD <src> <dest> | Like COPY, but can also extract tar archives and fetch remote URLs (prefer COPY when possible) |
RUN <command> | Execute a command during the build and commit the result as a new layer |
CMD ["exec", "form"] | Default command when the container starts (can be overridden at docker run) |
ENTRYPOINT ["exec", "form"] | Main process of the container; pairs with CMD for default arguments |
EXPOSE <port> | Document which port the container listens on (does not publish it — use -p on docker run) |
ENV <KEY>=<value> | Set an environment variable available at build and runtime |
ARG <name>[=<default>] | Build-time variable; passed with --build-arg (not available after the image is built) |
USER <name> | Run subsequent instructions and the container process as this user |
VOLUME <path> | Declare a mount point for persistent or shared data |
HEALTHCHECK | Define a command Docker runs periodically to check container health |
2.1 CMD vs ENTRYPOINT
Section titled “2.1 CMD vs ENTRYPOINT”CMD— default command; overridden by arguments passed todocker run.ENTRYPOINT— fixed executable;docker runarguments are appended as parameters.
ENTRYPOINT ["java", "-jar", "app.jar"]CMD ["--spring.profiles.active=prod"]docker run my-java-app --spring.profiles.active=dev# Runs: java -jar app.jar --spring.profiles.active=dev2.2 ENV vs ARG
Section titled “2.2 ENV vs ARG”ARG— only duringdocker build(e.g., version pins, build flags).ENV— persists in the final image and is available to running containers.
ARG JAVA_VERSION=21FROM eclipse-temurin:${JAVA_VERSION}-jdk-alpine
ENV APP_ENV=productiondocker build --build-arg JAVA_VERSION=17 -t my-java-app .3. Layer caching
Section titled “3. Layer caching”Docker reuses a layer if the instruction and its inputs have not changed. Put instructions that change often last, and stable steps first:
FROM node:22-alpineWORKDIR /app
# Dependencies change less often than source codeCOPY package.json package-lock.json ./RUN npm ci
COPY . .RUN npm run build
CMD ["node", "dist/index.js"]4. Multi-stage builds
Section titled “4. Multi-stage builds”Use multiple FROM stages to keep the final image small — compile in one stage, copy only the artifact into a minimal runtime image:
# Stage 1: buildFROM eclipse-temurin:21-jdk-alpine AS builderWORKDIR /appCOPY . .RUN mvn package -DskipTests
# Stage 2: runtimeFROM eclipse-temurin:21-jre-alpineWORKDIR /appCOPY --from=builder /app/target/app.jar .EXPOSE 8080CMD ["java", "-jar", "app.jar"]Build a specific stage:
docker build --target builder -t my-java-app:builder .5. .dockerignore
Section titled “5. .dockerignore”Exclude files from the build context (similar to .gitignore). Smaller context means faster builds and fewer accidental cache invalidations:
.gitnode_modulestarget*.md.env6. Example: Python app
Section titled “6. Example: Python app”FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]7. Quick checklist
Section titled “7. Quick checklist”- Choose a minimal base image (
-alpine,-slim, or-distrolesswhen possible). - Use
COPYinstead ofADDunless you need archive extraction. - Do not run as root in production — add a
USERdirective. - Pin base image tags (
node:22-alpine, notnode:latest). - Add a
.dockerignorefile. - Use multi-stage builds for compiled languages.