Docker Explained Simply
Containers, Dockerfiles, and docker-compose - everything you actually need to know.
What is Docker?
Docker packages your application and everything it needs - code, runtime, libraries, system tools - into a single unit called a container. Think of it as a lightweight, portable box that runs the same way on your laptop, your teammate's machine, and in production.
Before Docker, you'd hear "it works on my machine" constantly. Docker fixes that. Everyone runs the same container, so the environment is identical everywhere.
Containers vs Virtual Machines
A virtual machine emulates an entire computer - it has its own operating system, which makes it heavy (gigabytes) and slow to start. A container shares the host's operating system kernel and only packages your app and its dependencies. Containers are megabytes in size and start in seconds.
You can run dozens of containers on a machine that would struggle with a handful of VMs. That efficiency is why containers took over the industry.
The Dockerfile
A Dockerfile is the recipe for building your container image. Each line is a step. Docker caches each step, so if you only change your code (not your dependencies), rebuilding is fast.
# Start from a lightweight Node image
FROM node:20-alpine
# Set working directory inside the container
WORKDIR /app
# Copy package files first (layer caching trick)
COPY package*.json ./
# Install dependencies
RUN npm ci --only=production
# Copy the rest of your code
COPY . .
# Tell Docker which port your app uses
EXPOSE 3000
# The command to run when the container starts
CMD ["node", "server.js"]Notice we copy package.json separately before the rest of the code. This way, Docker only re-runs npm install when dependencies actually change.
Docker Compose
Real applications rarely run alone. You need a database, maybe Redis for caching, perhaps a message queue. Docker Compose lets you define all these services in one YAML file and spin them up together with a single command.
version: '3.8'
services:
app:
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:Essential Commands
You'll use these daily. Memorise the first five and you can handle most situations.
# Build an image from a Dockerfile
docker build -t myapp:latest .
# Run a container from that image
docker run -d -p 3000:3000 --name myapp myapp:latest
# List running containers
docker ps
# View container logs
docker logs myapp
# Stop and remove a container
docker stop myapp && docker rm myapp
# Start everything with docker-compose
docker-compose up -d
# Tear it all down
docker-compose downKey Takeaways
- - Containers are lightweight, portable, and consistent across environments
- - Dockerfiles define how to build your image step by step
- - Docker Compose orchestrates multi-container applications locally
- - Layer caching makes rebuilds fast when you structure your Dockerfile well
- - Once you're comfortable here, Kubernetes is the next step for production orchestration