Skip to main content
Docker beginner Lesson 2 of 9

Docker Images and Dockerfiles

Pull images from registries, write Dockerfiles, build your own images, and understand layer caching.

Image Basics

An image is a read-only stack of layers. Each layer represents a filesystem change from a Dockerfile instruction. Layers are cached and shared across images, so if two images use the same base (node:18-alpine), they share those layers on disk.

# Pull an image from Docker Hub
docker pull node:18-alpine

# List all local images
docker images

# Show image history (each layer)
docker history node:18-alpine

# Remove an image
docker rmi node:18-alpine

# Remove all unused images
docker image prune -a

Writing a Dockerfile

A Dockerfile is a plain text file named exactly Dockerfile. Each instruction creates a layer.

# Base image — always start here
FROM node:18-alpine

# Set working directory inside the container filesystem
WORKDIR /app

# Copy dependency manifests first (cache optimization)
COPY package*.json ./

# Install dependencies (this layer is cached until package.json changes)
RUN npm ci --only=production

# Copy application source code
COPY . .

# Document which port the app listens on
EXPOSE 3000

# Drop root privileges
USER node

# Default command when container starts
CMD ["node", "src/index.js"]

Key Instructions

InstructionPurpose
FROMBase image — every Dockerfile starts here
WORKDIRSets the working directory (creates it if absent)
COPYCopy files from host into image
RUNExecute a command during build
ENVSet environment variables
EXPOSEDocument a port (does not publish it)
USERSwitch to a non-root user
CMDDefault command when container starts
ENTRYPOINTExecutable that always runs
ARGBuild-time variable (not available at runtime)

Building Images

# Build from Dockerfile in current directory, tag as myapp:1.0
docker build -t myapp:1.0 .

# Build with a different Dockerfile path
docker build -f docker/Dockerfile.prod -t myapp:prod .

# Pass a build argument
docker build --build-arg NODE_ENV=production -t myapp:prod .

# Build without using cache
docker build --no-cache -t myapp:fresh .

Layer Caching

Docker caches each layer. When a layer changes, all subsequent layers are invalidated and rebuilt. This is why dependency installation should come before copying source code:

# SLOW — changes to any source file invalidates npm install
COPY . .
RUN npm ci

# FAST — npm install only re-runs when package.json changes
COPY package*.json ./
RUN npm ci
COPY . .

.dockerignore

Like .gitignore — prevents files from being sent to the Docker build context:

node_modules
.git
*.log
.env
dist
coverage

Always include node_modules — you want Docker to install fresh dependencies, not copy your local ones.

Tagging and Pushing

# Tag an existing image
docker tag myapp:1.0 myusername/myapp:1.0

# Push to Docker Hub (requires docker login)
docker login
docker push myusername/myapp:1.0

# Pull it on another machine
docker pull myusername/myapp:1.0

Inspecting Images

# Detailed image metadata as JSON
docker inspect myapp:1.0

# Check image size and layers
docker history myapp:1.0 --no-trunc

# Run a shell inside to explore the image filesystem
docker run --rm -it myapp:1.0 sh

Frequently Asked Questions

What is the difference between CMD and ENTRYPOINT?
ENTRYPOINT sets the executable that always runs. CMD sets default arguments that can be overridden. If you define both, CMD becomes the default args passed to ENTRYPOINT. Most app images use ENTRYPOINT for the binary and CMD for flags.
Why does my Docker image rebuild take so long?
Likely a cache miss. Docker rebuilds from the first changed instruction downward. Put COPY package.json before COPY . . so dependency installation is cached separately from source code changes.
What does the colon mean in 'node:18-alpine'?
It separates the image name from the tag. Tags are version identifiers. 'alpine' is a minimal Linux variant. Without a tag, Docker uses 'latest' — which is fine for learning but should be pinned in production.