Skip to main content
โšก Calmops

Docker Fundamentals: Containers, Images, and Dockerfiles

Introduction

Docker packages your application and its dependencies into a container โ€” a lightweight, portable unit that runs identically on any machine. This eliminates “works on my machine” problems and makes deployments reproducible.

Prerequisites: Docker installed (docs.docker.com/get-docker), basic command line knowledge.

Core Concepts

Image    = a read-only template (like a class)
Container = a running instance of an image (like an object)
Registry  = a storage service for images (Docker Hub, ECR, GCR)
Dockerfile = instructions to build an image

Your First Dockerfile

# Start from an official base image
FROM node:20-alpine

# Set working directory inside the container
WORKDIR /app

# Copy dependency files first (for layer caching)
COPY package*.json ./

# Install dependencies
RUN npm ci --only=production

# Copy application code
COPY . .

# Document which port the app uses
EXPOSE 3000

# Command to run when container starts
CMD ["node", "src/index.js"]

Build and run:

# Build the image
docker build -t myapp:1.0 .

# Run a container from the image
docker run -p 3000:3000 myapp:1.0

# Run in background (detached)
docker run -d -p 3000:3000 --name myapp myapp:1.0

# Check running containers
docker ps

# View logs
docker logs myapp
docker logs -f myapp  # follow

# Stop and remove
docker stop myapp
docker rm myapp

Multi-Stage Builds

Multi-stage builds produce smaller production images by separating build tools from runtime:

# Stage 1: Build
FROM node:20-alpine AS builder

WORKDIR /app
COPY package*.json ./
RUN npm ci                    # install ALL deps (including devDeps)
COPY . .
RUN npm run build             # compile TypeScript, bundle, etc.

# Stage 2: Production image
FROM node:20-alpine AS production

WORKDIR /app

# Only copy what's needed to run
COPY package*.json ./
RUN npm ci --only=production  # production deps only

COPY --from=builder /app/dist ./dist

# Run as non-root user (security)
USER node

EXPOSE 3000

HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
    CMD node -e "require('http').get('http://localhost:3000/health', r => process.exit(r.statusCode === 200 ? 0 : 1))"

CMD ["node", "dist/index.js"]

Size comparison:

Without multi-stage: ~1.2GB (includes build tools, devDependencies)
With multi-stage:    ~180MB (only runtime + production deps)

.dockerignore

Always create .dockerignore to exclude unnecessary files:

# .dockerignore
node_modules
npm-debug.log
.git
.gitignore
.env
.env.*
dist
coverage
*.test.ts
*.spec.ts
README.md
docker-compose*.yml
.dockerignore
Dockerfile*

Without this, COPY . . copies node_modules (hundreds of MB) into the build context.

Essential Docker Commands

# Images
docker images                          # list local images
docker pull node:20-alpine             # download image
docker rmi myapp:1.0                   # remove image
docker image prune                     # remove unused images

# Containers
docker ps                              # running containers
docker ps -a                           # all containers (including stopped)
docker run -it node:20-alpine sh       # interactive shell
docker exec -it myapp sh               # shell in running container
docker inspect myapp                   # detailed container info
docker stats                           # live resource usage

# Build
docker build -t myapp:1.0 .
docker build -t myapp:1.0 -f Dockerfile.prod .  # custom Dockerfile
docker build --no-cache -t myapp:1.0 .           # ignore cache

# Registry
docker tag myapp:1.0 registry.example.com/myapp:1.0
docker push registry.example.com/myapp:1.0
docker pull registry.example.com/myapp:1.0

Environment Variables

# Set defaults in Dockerfile
ENV NODE_ENV=production
ENV PORT=3000

# Override at runtime
docker run -e NODE_ENV=staging -e PORT=8080 myapp:1.0

# Load from file
docker run --env-file .env.production myapp:1.0

Volumes: Persisting Data

# Named volume (managed by Docker)
docker run -v myapp-data:/app/data myapp:1.0

# Bind mount (maps host directory)
docker run -v $(pwd)/data:/app/data myapp:1.0

# Read-only bind mount
docker run -v $(pwd)/config:/app/config:ro myapp:1.0

Networking

# Create a network
docker network create myapp-network

# Run containers on the same network (they can reach each other by name)
docker run -d --name db --network myapp-network postgres:16
docker run -d --name app --network myapp-network -e DB_HOST=db myapp:1.0

# List networks
docker network ls
docker network inspect myapp-network

Dockerfile Best Practices

Layer Caching

Docker caches each layer. Put frequently-changing files last:

# GOOD: dependencies cached separately from code
COPY package*.json ./
RUN npm ci
COPY . .          # code changes don't invalidate npm ci cache

# BAD: any code change invalidates npm ci
COPY . .
RUN npm ci

Minimize Layers

# BAD: 3 separate RUN layers
RUN apt-get update
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*

# GOOD: single layer, smaller image
RUN apt-get update && \
    apt-get install -y curl && \
    rm -rf /var/lib/apt/lists/*

Use Specific Tags

# BAD: latest can change unexpectedly
FROM node:latest

# GOOD: pin to specific version
FROM node:20.11.0-alpine3.19

Non-Root User

# Create and use a non-root user
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser

Debugging Containers

# Get a shell in a running container
docker exec -it myapp sh

# Get a shell in a stopped container
docker run -it --entrypoint sh myapp:1.0

# Copy files out of a container
docker cp myapp:/app/logs/error.log ./error.log

# View filesystem changes
docker diff myapp

# Check resource usage
docker stats myapp

# Inspect image layers
docker history myapp:1.0

Complete Node.js Example

# Dockerfile
FROM node:20-alpine AS base
WORKDIR /app
COPY package*.json ./

FROM base AS development
RUN npm install
COPY . .
CMD ["npm", "run", "dev"]

FROM base AS builder
RUN npm ci
COPY . .
RUN npm run build

FROM node:20-alpine AS production
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force
COPY --from=builder /app/dist ./dist
USER node
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=5s CMD wget -qO- http://localhost:3000/health || exit 1
CMD ["node", "dist/index.js"]
# Development
docker build --target development -t myapp:dev .
docker run -v $(pwd)/src:/app/src -p 3000:3000 myapp:dev

# Production
docker build --target production -t myapp:prod .
docker run -p 3000:3000 myapp:prod

Resources

Comments