DevOpsLesson
DevOpsLesson

Free, comprehensive DevOps tutorials and learning roadmaps. Master Docker, Kubernetes, CI/CD, and more.

Stay Updated

Get notified about new tutorials and features.

Tutorials

  • What is DevOps?
  • Docker Tutorial
  • Terraform Tutorial
  • CI/CD Pipeline
  • All Tutorials

Roadmaps

  • DevOps Engineer
  • Cloud Engineer
  • SRE Path
  • All Roadmaps

Company

  • About Us
  • Blog
  • Contact
  • Privacy Policy
  • Terms of Service

© 2026 DevOpsLesson. All rights reserved.

DOCKERKUBERNETESTERRAFORMAWSCI/CDLINUXGITDEVOPS ROADMAPCLOUD ROADMAPSRE ROADMAPGIT CHEATSHEETDOCKER CHEATSHEETK8S CHEATSHEETTF CHEATSHEETLINUX CHEATSHEETDOCKERFILE LINTERYAML VALIDATORCRON PARSERREGEX TESTER

Docker Tutorial

Introduction to docker
Why Use Docker?
Docker vs Virtual Machines
Installing Docker
Key Docker Concepts
Docker Images
Docker Containers
Writing Dockerfiles
Docker Volumes

Building Images

PreviousPrev
Next

Learn to write Dockerfiles and use docker build to create custom images tailored to your applications.

What Is a Dockerfile?

A Dockerfile is a plain text file that contains step-by-step instructions for building a Docker image. Think of it as a recipe: Docker reads it from top to bottom, follows each instruction in order, and produces a finished image at the end.

Every instruction in a Dockerfile adds a new layer to the image, like stacking transparent sheets on top of each other. This layered system is what makes Docker images so efficient to store and share.

Here's what a complete, real-world Dockerfile looks like for a Node.js app:

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

# Set the working directory inside the image
WORKDIR /app

# Copy the dependency file first (explained below)
COPY package*.json ./

# Install dependencies
RUN npm install

# Copy the rest of your application code
COPY . .

# Tell Docker your app listens on port 3000
EXPOSE 3000

# The command that runs when someone starts a container from this image
CMD ["node", "server.js"]

The Core Dockerfile Instructions

FROM - Choose Your Starting Point

Every Dockerfile must begin with FROM. It tells Docker which base image to build on top of.

FROM ubuntu:22.04          # Full Ubuntu operating system
FROM node:18-alpine        # Node.js on a tiny Alpine Linux base
FROM python:3.12-slim      # Python with a trimmed-down Debian base
FROM scratch               # Completely empty, for advanced use cases only

Choosing the right base image matters. Alpine-based images (like node:18-alpine) are much smaller than full OS images, which means faster downloads and a smaller attack surface.

WORKDIR - Set Your Working Directory

WORKDIR sets the folder that all following instructions will run inside. If the folder doesn't exist yet, Docker creates it automatically.

WORKDIR /app
# From this point on, RUN, COPY, and CMD all operate inside /app

This is cleaner than writing cd /app && before every command, and it makes your Dockerfile easier to read.

COPY - Bring Your Files Into the Image

COPY takes files from your computer (the build context) and puts them into the image. The first argument is the source on your machine, the second is the destination inside the image.

COPY package.json ./           # Copy a single file
COPY ./src /app/src/           # Copy an entire folder
COPY package*.json ./          # Copy files matching a pattern
COPY --chown=node:node . .     # Copy everything, set file ownership

RUN - Execute Commands During the Build

RUN runs a shell command at build time. This is how you install software, compile code, create folders, or do anything else that needs to happen before your app runs.

RUN apt-get update && apt-get install -y curl
RUN npm install
RUN pip install -r requirements.txt

One important thing to know: each RUN instruction creates a new layer. To keep your image lean, combine related commands into a single RUN using &&:

# Bad Example as it creates 3 unnecessary layers
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get clean

# Good Example as it creates one layer, and gives same result
RUN apt-get update && apt-get install -y curl && apt-get clean

ENV - Set Environment Variables

ENV sets environment variables that are available both during the build and when the container is running.

ENV NODE_ENV=production
ENV PORT=3000
ENV DB_HOST=localhost DB_PORT=5432

These are a great way to configure your application's behavior without hardcoding values directly into your code.

ARG - Build-Time Variables

ARG is similar to ENV, but the variable only exists during the build process which means it won't be available inside the running container.

ARG VERSION=1.0.0
ARG BUILD_DATE
RUN echo "Building version $VERSION"

Use ARG for things like version numbers or build timestamps that you want to pass in at build time but don't need at runtime.

EXPOSE - Document Your Port

EXPOSE tells Docker which port your application listens on. Important to understand: this is documentation only, it doesn't actually open the port. You still need to use the -p flag when running the container to make it accessible.

EXPOSE 3000
EXPOSE 80 443

Think of it as leaving a note for whoever runs your image: "hey, this app expects traffic on port 3000."

CMD - The Default Startup Command

CMD defines what command runs when someone starts a container from your image. It can be overridden by passing a different command to docker run.

# Exec form - preferred, runs directly without a shell
CMD ["node", "server.js"]

# Shell form - runs through /bin/sh, slightly less predictable
CMD node server.js

Stick with the exec form (the one with square brackets) as it's more explicit and handles signals correctly.

ENTRYPOINT - Lock In the Executable

ENTRYPOINT is similar to CMD but harder to override. Use it when your container should always run a specific program, no matter what.

ENTRYPOINT ["python", "app.py"]
CMD ["--port", "8000"]    # These become the default arguments to the entrypoint

When both ENTRYPOINT and CMD are set, CMD provides the default arguments passed to ENTRYPOINT. Someone running the container can change the arguments (CMD) but not the executable (ENTRYPOINT).


How to Build an Image

Once you have a Dockerfile, building it is a single command:

# Build from the current directory and tag it as myapp:v1.0
docker build -t myapp:v1.0 .

# Use a specific Dockerfile (useful for multiple environments)
docker build -f Dockerfile.prod -t myapp:production .

# Pass in a build argument
docker build --build-arg VERSION=2.1.0 -t myapp:2.1.0 .

# Force a full rebuild, ignoring cached layers
docker build --no-cache -t myapp:latest .

# Build for a specific CPU architecture
docker build --platform linux/amd64 -t myapp:amd64 .

The . at the end is called the build context. It is the folder that Docker looks in when processing COPY instructions. It's almost always the current directory.


Hands-On Practice: Build Your First Image

# Step 1: Build the image from the current directory and tag it
docker build -t myapp:v1.0 .

# Step 2: Confirm the image was created
docker images | grep myapp

# Step 3: Run a container from it to verify everything works
docker run myapp:v1.0

After step 1, you'll see Docker processing each instruction in your Dockerfile one by one. Each step corresponds to a layer, and if you build again without changes, Docker reuses the cached layers, making rebuilds much faster.


The .dockerignore File - Don't Skip This

A .dockerignore file works exactly like .gitignore. It tells Docker which files and folders to exclude from the build context. This matters for two reasons: speed and security.

Without it, COPY . . could accidentally copy hundreds of megabytes of node_modules into your image, or copy your .env file containing API keys and passwords.

Create a .dockerignore file in the same directory as your Dockerfile:

node_modules/
.git/
.env
*.log
coverage/
dist/
.DS_Store
.vscode/
README.md

Always set this up before your first real build. It's a small step that prevents a lot of headaches.


A Complete Real-World Example: Python API

Here's a production-ready Dockerfile for a Python web API that demonstrates all of the best practices together:

FROM python:3.12-slim

# Install system packages needed to compile some Python dependencies
RUN apt-get update && \
    apt-get install -y --no-install-recommends gcc && \
    rm -rf /var/lib/apt/lists/*

WORKDIR /app

# Copy and install Python dependencies first (cache optimization)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copy application source code
COPY ./src ./src

# Don't run as root - create and switch to a regular user
RUN useradd -m appuser
USER appuser

EXPOSE 8000

CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000"]

A few things this example does that are worth noting: it installs dependencies before copying the app code (so Docker can cache the dependency layer and skip reinstalling when you only change code), and it switches to a non-root user before running the app, which is a basic but important security practice.


Key Takeaways

  • A Dockerfile is a recipe, Docker reads it top to bottom and each instruction creates a layer in the final image.
  • FROM sets your starting point, WORKDIR sets where you work, COPY brings in your files, RUN installs and configures things, and CMD defines what runs when the container starts.
  • docker build -t name:tag . builds your image. The . tells Docker to use the current directory as the build context.
  • Combine related RUN commands with && to reduce unnecessary layers and keep your image size down.
  • Always create a .dockerignore file to exclude node_modules, .env, and other files that shouldn't end up in your image.
  • Copy your dependency manifest (package.json, requirements.txt) and install before copying the rest of your code - this lets Docker cache the dependency layer and speeds up rebuilds.

Frequently Asked Questions

Does the order of instructions in a Dockerfile matter?

Yes, a lot. Docker processes instructions top to bottom, and each one builds on the previous. Order also affects caching, if an early instruction changes, all layers after it are rebuilt. This is why you copy and install dependencies before copying your app code.

What's the difference between RUN and CMD?

RUN executes during the build. It is how you install software and set things up. CMD executes when someone runs a container from the finished image. A Dockerfile can have many RUN instructions but only one effective CMD.

What's the difference between CMD and ENTRYPOINT?

CMD is the default command and can be easily overridden at runtime. ENTRYPOINT locks in the executable and if someone running the container can change the arguments but not the program itself. When in doubt, use CMD.

Why does my build take so long the first time but faster after that?

Docker caches each layer. On the first build everything is downloaded and executed fresh. On subsequent builds, Docker reuses cached layers for any instruction that hasn't changed. If you change your app code but not your dependencies, Docker skips reinstalling them entirely.

What does the . mean at the end of docker build?

It's the build context, the folder Docker sends to its build engine. All COPY paths in your Dockerfile are relative to this folder. It's almost always . (the current directory), but you can point it to any folder.

Should I use CMD in exec form or shell form?

Always use exec form: CMD ["node", "server.js"]. Shell form runs your command inside /bin/sh -c, which can cause issues with signal handling (like graceful shutdown) and behaves differently across environments.

PreviousPrev
Next

Continue Learning

Docker Hub

Explore Docker Hub - the world's largest container image registry. Learn to search, pull, and understand official images.

7 min·Easy

Docker Pull & Search

Learn how to pull Docker images from Docker Hub and search for the right ones using the CLI.

5 min·Easy

List & Inspect Images

Learn how to view, filter, and inspect Docker images on your local system to understand their contents and metadata.

6 min·Easy

Explore Related Topics

Ku

Kubernetes Tutorials

Orchestrate your Docker containers at scale

CI

CI/CD Tutorials

Automate Docker builds and deployments in pipelines

Try the Tool

Dockerfile Linter

Instantly lint your Dockerfile for best-practice violations and security issues.

On This Page

What Is a Dockerfile?The Core Dockerfile Instructions`FROM` - Choose Your Starting Point`WORKDIR` - Set Your Working Directory`COPY` - Bring Your Files Into the Image`RUN` - Execute Commands During the Build`ENV` - Set Environment Variables`ARG` - Build-Time Variables`EXPOSE` - Document Your Port`CMD` - The Default Startup Command`ENTRYPOINT` - Lock In the ExecutableHow to Build an ImageHands-On Practice: Build Your First ImageThe `.dockerignore` File - Don't Skip ThisA Complete Real-World Example: Python APIKey TakeawaysFrequently Asked Questions