Dockerfile
What is a Dockerfile?
A Dockerfile is a plain text file with a set of instructions that tells Docker how to build a custom image. Each instruction creates a layer in the image.
Dockerfile (instructions) -> docker build -> Image -> docker run -> Container
It is the standard and recommended way to create Docker images (preferred over docker commit).
Basic Dockerfile Structure
# Comment lines start with #
# Every Dockerfile must start with FROM
FROM ubuntu:22.04
# Install software
RUN apt-get update && apt-get install -y curl
# Copy files from host to image
COPY app.py /app/app.py
# Set the command to run when container starts
CMD ["python3", "/app/app.py"]
Build the Image
# Build from Dockerfile in current directory
docker build -t my-app:1.0 .
# Build from a specific file
docker build -f MyDockerfile -t my-app:1.0 .
# Build without cache
docker build --no-cache -t my-app:1.0 .
Every Dockerfile Instruction Explained
FROM - Base Image
Every Dockerfile must start with FROM. It defines the base image your image is built on.
# Use Ubuntu 22.04 as base
FROM ubuntu:22.04
# Use Node.js 20
FROM node:20
# Use a specific digest (pinned version)
FROM node:20@sha256:abc123...
# Use a minimal image (no OS)
FROM scratch
# Use Alpine Linux (very small, ~5 MB)
FROM alpine:3.18
# Use Python 3.11 slim variant
FROM python:3.11-slim
# Multi-stage: second stage
FROM nginx:alpine AS production
Common base images:
| Image | Use case | Size |
|---|---|---|
ubuntu:22.04 | General purpose | ~77 MB |
debian:bookworm-slim | General (smaller) | ~74 MB |
alpine:3.18 | Minimal Linux | ~7 MB |
node:20 | Node.js apps | ~1.1 GB |
node:20-alpine | Node.js (minimal) | ~180 MB |
python:3.11 | Python apps | ~1 GB |
python:3.11-slim | Python (smaller) | ~130 MB |
nginx:alpine | Web server | ~40 MB |
mysql:8.0 | MySQL database | ~530 MB |
RUN - Execute Commands During Build
Runs a command during the image build process. Used to install software, set up files, etc.
# Shell form (runs in /bin/sh -c)
RUN apt-get update
# Exec form (preferred - no shell variable expansion)
RUN ["apt-get", "update"]
# Install packages (combine commands to reduce layers)
RUN apt-get update && apt-get install -y \
curl \
wget \
git \
vim \
&& rm -rf /var/lib/apt/lists/*
# Run as multiple separate layers (each creates a new layer)
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y git
# Install Node.js dependencies
RUN npm install
# Create a directory
RUN mkdir -p /app/logs
# Set permissions
RUN chmod +x /app/start.sh
# Add a user
RUN useradd -m -s /bin/bash appuser
> Best practice: Combine related RUN commands with && to reduce image layers.
CMD - Default Command When Container Starts
Specifies the default command to run when the container starts. Can be overridden by docker run <image> <command>.
# Shell form
CMD echo "Hello World"
# Exec form (recommended)
CMD ["nginx", "-g", "daemon off;"]
# Run a Python app
CMD ["python3", "app.py"]
# Run Node.js app
CMD ["node", "server.js"]
# Run shell
CMD ["/bin/bash"]
> Only the last CMD in a Dockerfile takes effect. If the user passes a command to docker run, CMD is ignored.
ENTRYPOINT - Fixed Command That Always Runs
Similar to CMD but cannot be overridden by docker run arguments. Used to make a container behave like an executable.
# Exec form (recommended)
ENTRYPOINT ["nginx", "-g", "daemon off;"]
# Shell form
ENTRYPOINT nginx -g "daemon off;"
# Common pattern: ENTRYPOINT + CMD together
ENTRYPOINT ["python3"]
CMD ["app.py"]
# docker run my-image -> runs: python3 app.py
# docker run my-image other.py -> runs: python3 other.py
ENTRYPOINT vs CMD:
CMD | ENTRYPOINT | |
|---|---|---|
Can be overridden with docker run args | Yes | No (only with --entrypoint) |
| Purpose | Default arguments | Fixed executable |
| Together | CMD becomes default args for ENTRYPOINT | Both work together |
COPY - Copy Files from Host to Image
Copies files from your build context (your machine) into the image.
# Copy a single file
COPY app.py /app/app.py
# Copy to a directory (trailing slash = directory)
COPY app.py /app/
# Copy multiple files
COPY file1.txt file2.txt /app/
# Copy entire directory
COPY src/ /app/src/
# Copy with wildcard
COPY *.json /app/
# Copy with permissions
COPY --chmod=755 script.sh /app/script.sh
# Copy from a specific build stage (multi-stage builds)
COPY --from=builder /app/dist /app/dist
# Copy with chown
COPY --chown=appuser:appuser . /app
ADD - Copy Files (with Extra Powers)
Like COPY but with additional features - can extract tar files and download URLs.
# Copy a file (same as COPY)
ADD app.py /app/app.py
# Copy and auto-extract a tar file
ADD app.tar.gz /app/
# Download a file from a URL
ADD https://example.com/file.tar.gz /tmp/
# Copy with chown
ADD --chown=user:group files /app/
> Best practice: Use COPY for local files. Only use ADD when you specifically need tar extraction or URL download.
ENV - Set Environment Variables
Sets environment variables that are available inside the container at runtime.
# Set a single variable
ENV APP_ENV=production
# Set multiple variables (old syntax, one per line)
ENV NODE_ENV=production
ENV PORT=3000
ENV DB_HOST=localhost
# Set multiple variables (new syntax, preferred)
ENV NODE_ENV=production \
PORT=3000 \
DB_HOST=localhost
# Access it in RUN commands
ENV VERSION=1.0
RUN echo "Building version $VERSION"
Variables set with ENV persist into the running container:
docker run my-app printenv APP_ENV
# production
ARG - Build-Time Variables
Defines variables that can be passed at build time using --build-arg. Not available at runtime.
# Define an ARG
ARG VERSION=latest
# Use it
FROM ubuntu:$VERSION
ARG NODE_VERSION=20
RUN apt-get install -y nodejs=$NODE_VERSION
# ARG before FROM applies to FROM only
ARG BASE_IMAGE=ubuntu
FROM $BASE_IMAGE:22.04
# Multiple ARGs
ARG APP_VERSION
ARG BUILD_DATE
Pass them at build time:
docker build --build-arg VERSION=22.04 .
docker build --build-arg NODE_VERSION=18 --build-arg APP_VERSION=1.2 .
ARG vs ENV:
ARG | ENV | |
|---|---|---|
| Available during build | Yes | Yes |
| Available at runtime | No | Yes |
Override at docker run | No | Yes (-e) |
EXPOSE - Document Exposed Ports
Documents which ports the container will listen on. Does NOT actually publish the port - it's metadata for documentation and the -P flag.
# Expose a single port
EXPOSE 80
# Expose multiple ports
EXPOSE 80 443
# Expose UDP port
EXPOSE 53/udp
# Expose both TCP and UDP
EXPOSE 80/tcp
EXPOSE 80/udp
To actually publish the port when running:
# Map port 8080 (host) to 80 (container)
docker run -p 8080:80 my-app
# Auto-map all EXPOSED ports to random host ports
docker run -P my-app
WORKDIR - Set Working Directory
Sets the working directory for all following instructions (RUN, CMD, ENTRYPOINT, COPY, ADD).
# Set working directory
WORKDIR /app
# All commands after this run inside /app
COPY . .
RUN npm install
CMD ["node", "server.js"]
# You can set multiple WORKDIRs - each is relative to the last
WORKDIR /app
WORKDIR src
WORKDIR scripts
# Now working in /app/src/scripts
> Best practice: Always use WORKDIR instead of RUN cd /app. It creates the directory automatically.
USER - Set the User
Sets the user for all following instructions and the running container. Prevents running as root.
# Create a user
RUN useradd -m -s /bin/bash appuser
# Switch to the user
USER appuser
# All commands now run as appuser
CMD ["node", "server.js"]
# Use user ID instead of name
USER 1000
# Use user:group format
USER appuser:appgroup
> Security best practice: Always run containers as a non-root user in production.
VOLUME - Declare Mount Points
Declares a directory as a volume mount point. Docker will create an anonymous volume for it automatically.
# Declare a volume
VOLUME /data
# Declare multiple volumes
VOLUME ["/data", "/logs"]
# Common examples
VOLUME /var/lib/mysql # MySQL data
VOLUME /var/log/nginx # Nginx logs
VOLUME /app/uploads # File uploads
LABEL - Add Metadata
Adds metadata labels to the image. Used for documentation, filtering, and tooling.
# Add labels
LABEL version="1.0"
LABEL maintainer="irfan@example.com"
LABEL description="My web application"
# Multiple labels at once
LABEL version="1.0" \
maintainer="irfan@example.com" \
description="My web application" \
org.opencontainers.image.source="https://github.com/irfan/my-app"
View labels:
docker image inspect --format '{{json .Config.Labels}}' my-app
HEALTHCHECK - Container Health Check
Tells Docker how to test if the container is healthy.
# HTTP health check
HEALTHCHECK --interval=30s --timeout=10s --retries=3 \
CMD curl -f http://localhost/ || exit 1
# Custom health check script
HEALTHCHECK --interval=1m --timeout=5s --start-period=30s --retries=3 \
CMD /app/healthcheck.sh
# Disable inherited health check
HEALTHCHECK NONE
Options:
| Option | Default | Meaning |
|---|---|---|
--interval | 30s | How often to check |
--timeout | 30s | How long to wait for response |
--start-period | 0s | Wait this long before first check |
--retries | 3 | How many failures before "unhealthy" |
Check health status:
docker inspect --format '{{.State.Health.Status}}' my-container
docker ps # Shows (healthy) or (unhealthy) in STATUS column
SHELL - Override Default Shell
Changes the shell used for RUN, CMD, ENTRYPOINT shell form.
# Default shell is /bin/sh -c on Linux
SHELL ["/bin/sh", "-c"]
# Use bash
SHELL ["/bin/bash", "-c"]
RUN echo "Using bash"
# Use PowerShell (Windows containers)
SHELL ["powershell", "-Command"]
RUN Write-Host "Hello"
STOPSIGNAL - Set Stop Signal
Sets the system signal that will be sent to the container to stop it.
# Default is SIGTERM
STOPSIGNAL SIGTERM
# Use SIGINT
STOPSIGNAL SIGINT
# Use signal number
STOPSIGNAL 9
ONBUILD - Trigger for Child Images
Adds a trigger instruction that runs when the image is used as a base image in another Dockerfile.
# This runs when someone uses this image as their FROM
ONBUILD COPY . /app
ONBUILD RUN npm install
Example:
# base image (already built)
FROM node:20
ONBUILD COPY . /app
ONBUILD RUN npm install
# child Dockerfile
FROM my-base-node <- triggers COPY and RUN from above automatically
EXPOSE 3000
CMD ["node", "server.js"]
.dockerignore - Exclude Files from Build
Just like .gitignore, .dockerignore tells Docker what to exclude from the build context.
# .dockerignore
# Dependencies
node_modules/
vendor/
# Build output
dist/
build/
*.class
# Git
.git/
.gitignore
# Environment files
.env
.env.local
# Logs
*.log
logs/
# OS files
.DS_Store
Thumbs.db
# Tests
tests/
test/
*.test.js
# IDE
.vscode/
.idea/
Build context without .dockerignore:
Sending build context to Docker daemon 500MB <- slow!
Build context with .dockerignore:
Sending build context to Docker daemon 1.5MB <- fast!
Complete Real-World Examples
Example 1 - Node.js Web App
FROM node:20-alpine
WORKDIR /app
# Copy package files first (layer caching)
COPY package*.json ./
# Install dependencies
RUN npm ci --only=production
# Copy app source
COPY . .
# Create non-root user
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD wget -qO- http://localhost:3000/health || exit 1
CMD ["node", "server.js"]
Example 2 - Python Flask App
FROM python:3.11-slim
WORKDIR /app
# Install dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy source
COPY . .
# Non-root user
RUN useradd -m appuser
USER appuser
EXPOSE 5000
ENV FLASK_APP=app.py \
FLASK_ENV=production
CMD ["python", "-m", "flask", "run", "--host=0.0.0.0"]
Example 3 - Nginx with Custom Config
FROM nginx:alpine
# Remove default config
RUN rm /etc/nginx/conf.d/default.conf
# Copy custom config
COPY nginx.conf /etc/nginx/conf.d/
# Copy website files
COPY html/ /usr/share/nginx/html/
EXPOSE 80
HEALTHCHECK --interval=30s CMD wget -qO- http://localhost || exit 1
CMD ["nginx", "-g", "daemon off;"]
Dockerfile Instructions Quick Reference
| Instruction | Purpose |
|---|---|
FROM | Set base image |
RUN | Execute command during build |
CMD | Default command at startup (can be overridden) |
ENTRYPOINT | Fixed startup command |
COPY | Copy files from host to image |
ADD | Copy files (also extracts tar, supports URLs) |
ENV | Set environment variable (build + runtime) |
ARG | Set build-time variable only |
EXPOSE | Document listening ports |
WORKDIR | Set working directory |
USER | Set user for commands |
VOLUME | Declare mount points |
LABEL | Add metadata |
HEALTHCHECK | Define health check command |
SHELL | Override default shell |
STOPSIGNAL | Set container stop signal |
ONBUILD | Trigger for child Dockerfiles |
FAQ
Should I memorize every Docker command?+
No. Memorize the core workflow first: build, run, list, inspect, logs, exec, stop, remove, and clean up. Then learn specialized commands when you need them.
Is Docker only for developers?+
No. Docker is useful for system administrators, infrastructure engineers, DevOps engineers, cloud engineers, support engineers, and learners who want repeatable labs.
What should I do after reading this guide?+
Run the examples, write down what each command changes, rebuild the workflow with Docker Compose, and then add one CI/CD step that builds the image automatically.
Need help applying Docker in a real project?
Work directly with Muhammad Irfan Aslam for Docker, Linux, DevOps, CI/CD, cloud deployment, or infrastructure troubleshooting support.
Hire Me for Support