Dockerfile Basics
A Dockerfile is a text file containing instructions to build a Docker image. It automates the image creation process and ensures reproducible builds.
What is a Dockerfile?
A Dockerfile contains step-by-step instructions that Docker uses to create an image:
# Start from a base image
FROM node:18-alpine
# Set working directory
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm install
# Copy application code
COPY . .
# Expose port
EXPOSE 3000
# Define startup command
CMD ["npm", "start"]
Each instruction creates a new layer in the image.
Dockerfile Structure
┌─────────────────────────────────────────────────────────┐
│ # Comments start with hash │
│ │
│ INSTRUCTION argument │
│ INSTRUCTION argument argument │
│ INSTRUCTION ["executable", "param1", "param2"] │
└─────────────────────────────────────────────────────────┘
Key Rules
- Instructions are uppercase (convention, not required)
- One instruction per line (use
\for line continuation) - Instructions run in order (top to bottom)
- Each instruction creates a layer (except some metadata instructions)
Essential Instructions
FROM
Every Dockerfile must start with FROM. It specifies the base image:
# Official image
FROM ubuntu:22.04
# Specific version
FROM node:18.19-alpine
# Minimal base
FROM alpine:3.19
# Empty base (for static binaries)
FROM scratch
WORKDIR
Sets the working directory for subsequent instructions:
# All following commands run in /app
WORKDIR /app
# Can use relative paths after
WORKDIR src # Now in /app/src
WORKDIR ../config # Now in /app/config
COPY
Copies files from build context to image:
# Copy single file
COPY package.json .
# Copy multiple files
COPY package.json package-lock.json ./
# Copy directory
COPY src/ ./src/
# Copy with wildcard
COPY *.json ./
RUN
Executes commands during image build:
# Shell form (runs in /bin/sh -c)
RUN apt-get update && apt-get install -y curl
# Exec form (no shell processing)
RUN ["apt-get", "update"]
# Multiple commands
RUN apt-get update \
&& apt-get install -y python3 \
&& rm -rf /var/lib/apt/lists/*
CMD
Defines the default command when container starts:
# Exec form (preferred)
CMD ["node", "app.js"]
# Shell form
CMD npm start
# Only one CMD per Dockerfile (last one wins)
EXPOSE
Documents which ports the container listens on:
# Single port
EXPOSE 3000
# Multiple ports
EXPOSE 80 443
# UDP port
EXPOSE 53/udp
Note: EXPOSE doesn't actually publish ports - it's documentation. Use -p with docker run to publish.
Your First Dockerfile
Let's create a simple Node.js application Dockerfile:
# Use official Node.js image
FROM node:18-alpine
# Create app directory
WORKDIR /app
# Copy package files first (for caching)
COPY package*.json ./
# Install dependencies
RUN npm install
# Copy source code
COPY . .
# Document the port
EXPOSE 3000
# Start the application
CMD ["node", "server.js"]
Building an Image
Build an image from a Dockerfile:
# Build with default Dockerfile in current directory
docker build -t myapp .
# Build with specific Dockerfile
docker build -f Dockerfile.prod -t myapp:prod .
# Build with build arguments
docker build --build-arg VERSION=1.0 -t myapp .
# Build without cache
docker build --no-cache -t myapp .
Build Context
The . in docker build -t myapp . specifies the build context - the directory containing files accessible to COPY and ADD instructions.
# Current directory as context
docker build -t myapp .
# Different directory as context
docker build -t myapp ./app
# URL as context
docker build -t myapp https://github.com/user/repo.git
Build Output
$ docker build -t myapp .
Sending build context to Docker daemon 2.048kB
Step 1/7 : FROM node:18-alpine
---> b3db67c7d3a3
Step 2/7 : WORKDIR /app
---> Running in 5d4e8b6c9a2f
Removing intermediate container 5d4e8b6c9a2f
---> 3d4e8b6c9a2f
Step 3/7 : COPY package*.json ./
---> 8d3c7b6c9a2f
...
Successfully built a1b2c3d4e5f6
Successfully tagged myapp:latest
.dockerignore
Exclude files from the build context:
# .dockerignore
node_modules
npm-debug.log
Dockerfile
.dockerignore
.git
.gitignore
README.md
.env
*.log
Benefits:
- Faster build (smaller context)
- More secure (excludes secrets)
- Smaller images (don't copy unnecessary files)
Practical Examples
Python Application
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 5000
CMD ["python", "app.py"]
Go Application
FROM golang:1.21-alpine
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o main .
EXPOSE 8080
CMD ["./main"]
Static Website with Nginx
FROM nginx:alpine
COPY index.html /usr/share/nginx/html/
COPY css/ /usr/share/nginx/html/css/
COPY js/ /usr/share/nginx/html/js/
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
Layer Caching
Docker caches layers to speed up builds. Order matters:
# Bad - any code change invalidates npm install cache
FROM node:18-alpine
WORKDIR /app
COPY . . # Includes package.json AND source code
RUN npm install # Must reinstall on any change
CMD ["npm", "start"]
# Good - package.json changes less often than source code
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./ # Copy package files first
RUN npm install # Cached unless package.json changes
COPY . . # Source code copied last
CMD ["npm", "start"]
Key Takeaways
- Dockerfiles automate image creation with reproducible builds
- FROM defines the base image - every Dockerfile needs one
- WORKDIR sets the working directory
- COPY transfers files from build context to image
- RUN executes commands during build
- CMD defines the default command at runtime
- Order instructions to maximize layer caching
- Use .dockerignore to exclude unnecessary files

