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:

ImageUse caseSize
ubuntu:22.04General purpose~77 MB
debian:bookworm-slimGeneral (smaller)~74 MB
alpine:3.18Minimal Linux~7 MB
node:20Node.js apps~1.1 GB
node:20-alpineNode.js (minimal)~180 MB
python:3.11Python apps~1 GB
python:3.11-slimPython (smaller)~130 MB
nginx:alpineWeb server~40 MB
mysql:8.0MySQL 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:

CMDENTRYPOINT
Can be overridden with docker run argsYesNo (only with --entrypoint)
PurposeDefault argumentsFixed executable
TogetherCMD becomes default args for ENTRYPOINTBoth 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:

ARGENV
Available during buildYesYes
Available at runtimeNoYes
Override at docker runNoYes (-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:

OptionDefaultMeaning
--interval30sHow often to check
--timeout30sHow long to wait for response
--start-period0sWait this long before first check
--retries3How 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

InstructionPurpose
FROMSet base image
RUNExecute command during build
CMDDefault command at startup (can be overridden)
ENTRYPOINTFixed startup command
COPYCopy files from host to image
ADDCopy files (also extracts tar, supports URLs)
ENVSet environment variable (build + runtime)
ARGSet build-time variable only
EXPOSEDocument listening ports
WORKDIRSet working directory
USERSet user for commands
VOLUMEDeclare mount points
LABELAdd metadata
HEALTHCHECKDefine health check command
SHELLOverride default shell
STOPSIGNALSet container stop signal
ONBUILDTrigger 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