Mastering Dockerfile Commands: A Beginner to Expert Guide

Posted on August 03 2023

Mastering Dockerfile Commands: A Beginner to Expert Guide

Whether you're just getting started with Docker or looking to level up your container knowledge, understanding every Dockerfile instruction is essential. This guide breaks down all Dockerfile commands — what they do, when to use them, and real examples to cement your understanding.


Quick Reference Table

Instruction Runs At Purpose
FROM Build start Sets the base image
RUN Build time Executes shell commands
WORKDIR Build time Sets working directory
COPY Build time Copies files into image
ADD Build time Copies files, extracts tarballs, or fetches URLs
ENV Build + Runtime Sets environment variables
ARG Build time only Defines build-time variables
LABEL Build time Adds metadata to image
EXPOSE Documentation Hints at which port the app uses
USER Build + Runtime Sets the user for commands
VOLUME Runtime Declares persistent storage
ENTRYPOINT Runtime Sets the main executable
CMD Runtime Sets default command or arguments
HEALTHCHECK Runtime Monitors container health
SHELL Build time Changes the default shell
STOPSIGNAL Runtime Sets the container stop signal
ONBUILD Child build Deferred instructions for derived images

The Commands Explained

FROM — Choose Your Base

Every Dockerfile starts here. It defines the base image your container is built on top of.

# Start from an official lightweight Node.js image
FROM node:18-alpine

# Start from bare Ubuntu
FROM ubuntu:22.04

# Multi-stage: name a stage for reference later
FROM node:18-alpine as builder

💡 Tip: Use -slim or -alpine variants for smaller image sizes in production.


RUN — Execute Build Commands

Runs any shell command during the build process. Each RUN adds a new layer to the image.

# Install system packages
RUN apt-get update && apt-get install -y curl git

# Chain commands to reduce layers
RUN npm install && npm run build

# Clean up in the same layer to keep image small
RUN apt-get install -y curl && rm -rf /var/lib/apt/lists/*

💡 Tip: Chain related commands with && to minimize image layers.


WORKDIR — Set Your Working Directory

Sets the current directory for all subsequent instructions. Creates the folder if it doesn't exist.

WORKDIR /app

# Now all commands below run from /app
COPY . .
RUN npm install

💡 Tip: Always set WORKDIR instead of using RUN cd /some/path — it's cleaner and more predictable.


COPY — Copy Files Into the Image

Copies files or folders from your local machine into the image. Simple and explicit — the preferred way to copy files.

# Copy a single file
COPY package.json /app/

# Copy an entire folder
COPY ./src /app/src

# Copy from a previous build stage (multi-stage builds)
COPY --from=builder /app/dist /app/dist

💡 Tip: Copy package.json before your source code so Docker caches npm install and only reruns it when dependencies change.


ADD — Copy With Superpowers

Like COPY but with two extra abilities: it can extract .tar archives and download files from URLs.

# Auto-extracts a tar archive
ADD archive.tar.gz /app/

# Download a file from the internet
ADD https://example.com/config.json /app/config.json

# Regular file copy (prefer COPY for this)
ADD package.json /app/

💡 Tip: Use COPY for simple file copying. Only use ADD when you specifically need tar extraction or URL fetching.


ENV — Set Environment Variables

Defines environment variables available both during the build and at runtime.

ENV PORT=8080
ENV NODE_ENV=production
ENV DB_URL="postgres://localhost/mydb"

# Multiple variables in one instruction
ENV APP_HOME=/app \
    LOG_LEVEL=info \
    MAX_CONNECTIONS=100

💡 Tip: Use ENV for values your app needs at runtime. Use ARG for values only needed during the build.


ARG — Build-Time Variables

Defines variables passed in at build time only. They are not available when the container is running.

ARG APP_VERSION=1.0.0
ARG BUILD_ENV=production

RUN echo "Building version $APP_VERSION"

Pass them in at build time:

docker build --build-arg APP_VERSION=2.1.0 .

💡 Tip: Never use ARG for secrets — they can be exposed in the image history.


LABEL — Add Metadata

Attaches key-value metadata to your image. Useful for versioning, documentation, and automation.

LABEL maintainer="ibrahim@example.com"
LABEL version="1.0.0"
LABEL description="Node.js production server"
LABEL org.opencontainers.image.source="https://github.com/user/repo"

💡 Tip: Follow the Open Container Initiative label conventions for standardized metadata.


EXPOSE — Document Your Port

Tells Docker (and other developers) which port the container listens on. It is documentation only — it does not actually publish the port.

EXPOSE 8080
EXPOSE 443

You still need to publish ports when running:

docker run -p 8080:8080 myapp

💡 Tip: Always include EXPOSE — it makes your Dockerfile self-documenting and is required for some Docker tools to work correctly.


USER — Run as Non-Root

Sets which user runs subsequent commands. Running as root inside a container is a security risk.

# Create a user and switch to it
RUN useradd -m appuser
USER appuser

# Or use a numeric UID
USER 1001

💡 Tip: Always switch to a non-root user before your CMD or ENTRYPOINT in production images.


VOLUME — Persist Data

Declares a directory as a mount point for data that should survive container restarts — like databases, uploads, or logs.

# Persist a data folder
VOLUME /app/data

# Persist multiple paths
VOLUME ["/app/data", "/app/logs"]

💡 Tip: Data written inside a VOLUME directory is stored on the host machine, not inside the container layer.


ENTRYPOINT — Set the Main Executable

Defines the main process of the container. Unlike CMD, it is not easily overridden when running the container.

# Always run node
ENTRYPOINT ["node"]

# Combine with CMD to set a default argument
ENTRYPOINT ["node"]
CMD ["server.js"]         # docker run myapp → runs: node server.js
                          # docker run myapp app.js → runs: node app.js

💡 Tip: Use ENTRYPOINT for the executable and CMD for its default arguments — this gives users flexibility without losing control.


CMD — Default Command

Sets the default command that runs when the container starts. Can be overridden by passing a command to docker run.

# Run the app
CMD ["npm", "start"]

# Or run a specific script
CMD ["node", "server.js"]

Override at runtime:

docker run myapp node other.js   # overrides CMD

💡 Tip: Only one CMD is allowed per Dockerfile. If you define multiple, only the last one takes effect.


HEALTHCHECK — Monitor Container Health

Defines a command Docker runs periodically to check if the container is functioning correctly.

HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
  CMD curl -f http://localhost:8080/health || exit 1
Option Default Meaning
--interval 30s How often to check
--timeout 30s Max time for the check
--retries 3 Failures before marking unhealthy

💡 Tip: Always add a health check to production containers so orchestrators like Kubernetes know when to restart them.


SHELL — Change the Default Shell

Changes the shell used by RUN, CMD, and ENTRYPOINT instructions. Default is /bin/sh -c on Linux.

# Switch to bash for more features
SHELL ["/bin/bash", "-c"]
RUN echo "This runs in bash"

# Use PowerShell on Windows
SHELL ["powershell", "-Command"]

💡 Tip: Useful when you need bash-specific features like arrays or pipefail in your build scripts.


STOPSIGNAL — Control How the Container Stops

Sets the OS signal sent to the container process when it's told to stop. Default is SIGTERM.

# Use SIGQUIT instead of SIGTERM
STOPSIGNAL SIGQUIT

# Use numeric signal
STOPSIGNAL 9

💡 Tip: Some apps (like Nginx) respond better to SIGQUIT for graceful shutdown. Check your app's documentation.


ONBUILD — Instructions for Child Images

Defines instructions that are dormant in the current image but automatically run when another image uses yours as a FROM base.

# In your base image:
ONBUILD COPY . /app
ONBUILD RUN npm install

When someone else does:

FROM your-base-image   # ← ONBUILD instructions fire here automatically

💡 Tip: Great for creating standardized base images for a team — enforces consistent setup without requiring developers to repeat boilerplate.


Putting It All Together

Here's a production-ready Dockerfile using best practices from everything above:

# 1. Base image
FROM node:18-alpine as base
LABEL maintainer="ibrahim@example.com"
ENV NODE_ENV=production PORT=8080

# 2. Install dependencies
FROM base as deps
WORKDIR /app
COPY package*.json ./
RUN npm install

# 3. Build
FROM deps as build
COPY . .
RUN npm run build

# 4. Final lean image
FROM base
WORKDIR /app
RUN adduser -D appuser
COPY --from=deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
USER appuser
EXPOSE 8080
HEALTHCHECK --interval=30s CMD wget -qO- http://localhost:8080/health || exit 1
CMD ["node", "dist/server.js"]

Summary

Mastering Dockerfile instructions lets you build images that are small, secure, fast to build, and easy to maintain. The key principles:

  • Use multi-stage builds to keep final images lean
  • Order layers from least to most frequently changed for better caching
  • Always run as a non-root user in production
  • Use HEALTHCHECK so orchestrators can manage your containers
  • Prefer COPY over ADD unless you need its extra features
  • Use ENTRYPOINT + CMD together for flexible but controlled container behavior