devops

Docker Basics

Containers, images, Dockerfiles, and essential Docker commands for DevOps


What is Docker?

Docker lets you package an application and all its dependencies into a container β€” a lightweight, portable, isolated unit that runs the same everywhere.

β€œIt works on my machine” β†’ β€œShip your machine.”

Container vs Virtual Machine

ContainerVirtual Machine
SizeMBsGBs
StartupSecondsMinutes
OSShares host kernelFull guest OS
IsolationProcess-levelHardware-level
Use caseApp packagingFull OS isolation

Core Concepts

Image

A read-only template. Like a class in OOP. Built from a Dockerfile.

Container

A running instance of an image. Like an object instantiated from a class.

Dockerfile

Instructions to build an image, layer by layer.

Registry

A storage server for images. Docker Hub is the public one. AWS ECR, GitHub Container Registry, and self-hosted options exist.

Volume

Persistent storage that survives container restarts.

Network

How containers talk to each other or the outside world.


Essential Docker Commands

Images

Terminal window
docker pull nginx # download image from registry
docker images # list local images
docker build -t myapp:1.0 . # build from Dockerfile in current dir
docker rmi myapp:1.0 # delete image
docker push myregistry/myapp:1.0 # push to registry

Containers

Terminal window
docker run nginx # run a container (foreground)
docker run -d nginx # run detached (background)
docker run -d -p 8080:80 nginx # map host port 8080 β†’ container port 80
docker run -d --name web nginx # give it a name
docker run -d -v /data:/app/data nginx # mount a volume
docker ps # list running containers
docker ps -a # list all (including stopped)
docker stop web # stop a container
docker start web # start a stopped container
docker rm web # delete container
docker logs web # view logs
docker logs -f web # follow logs (tail -f style)
docker exec -it web bash # open shell inside container

Cleanup

Terminal window
docker system prune # remove unused containers, networks, images
docker volume prune # remove unused volumes

Writing a Dockerfile

# Base image
FROM node:20-alpine
# Set working directory inside container
WORKDIR /app
# Copy dependency files first (cache optimization)
COPY package*.json ./
# Install dependencies
RUN npm ci --only=production
# Copy the rest of the source
COPY . .
# Build the app
RUN npm run build
# Expose the port the app listens on
EXPOSE 3000
# Command to run when container starts
CMD ["node", "dist/server.js"]

Layer Caching

Docker caches each layer. Put things that change least often at the top:

  1. Base image (changes rarely)
  2. System packages
  3. Dependencies (package.json)
  4. Source code (changes most)

This way, npm install is only re-run when package.json changes, not on every code change.


Multi-Stage Builds

Build the app in one stage, copy only the output to a smaller final image:

# Stage 1: Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 2: Production image
FROM node:20-alpine AS production
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
EXPOSE 3000
CMD ["node", "dist/server.js"]

Final image only contains the built output β€” no build tools, no source code. Much smaller and more secure.


Docker Compose

Run multiple containers together with a single docker-compose.yml:

version: '3.9'
services:
web:
build: .
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgres://user:pass@db:5432/mydb
depends_on:
- db
db:
image: postgres:15-alpine
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
POSTGRES_DB: mydb
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
Terminal window
docker compose up -d # start all services in background
docker compose down # stop and remove containers
docker compose logs -f # follow logs from all services

.dockerignore

Like .gitignore but for Docker builds. Keeps image sizes small:

node_modules
.git
.env
dist
*.log
README.md

Best Practices

  1. Use official base images β€” node:20-alpine not ubuntu + manual installs
  2. Minimize layers β€” chain RUN commands with &&
  3. Run as non-root β€” add USER node before CMD
  4. Never bake secrets into images β€” use environment variables or secrets at runtime
  5. Tag images properly β€” use semantic versions, not just latest
  6. Scan images β€” use docker scout or trivy for vulnerabilities
  7. Use multi-stage builds β€” keep production images lean