Docker Introduction
Table of Contents
“It works on my machine” is the problem Docker solves. Docker packages your application with everything it needs — code, runtime, libraries, system tools — into a portable container that runs identically everywhere: your laptop, your colleague’s laptop, staging, production.
What Docker Actually Is
Docker is a platform for building and running containers. A container is a lightweight, isolated environment that shares the host OS kernel but has its own filesystem, processes, and network.
Think of it like this:
- Virtual machines — Each VM runs a full operating system. Heavy (GBs), slow to start (minutes).
- Containers — Share the host OS kernel. Lightweight (MBs), start instantly (seconds).
Key Concepts
- Image — A read-only template containing your app and its dependencies. Like a class in OOP.
- Container — A running instance of an image. Like an object created from a class.
- Dockerfile — Instructions for building an image.
- Registry — A place to store and share images (Docker Hub, AWS ECR, GitHub Container Registry).
Installing Docker
Running Your First Container
# Run an nginx web server
docker run -d -p 8080:80 --name my-nginx nginx
# -d: run in background (detached)
# -p 8080:80: map host port 8080 to container port 80
# --name: give it a friendly name
Open http://localhost:8080 — you’ll see the nginx welcome page. That’s a full web server running in an isolated container.
# See running containers
docker ps
# View logs
docker logs my-nginx
# Stop the container
docker stop my-nginx
# Remove it
docker rm my-nginx
Building Your Own Image
Create a simple Node.js app and containerize it:
// app.js
const http = require("http");
const server = http.createServer((req, res) => {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ message: "Hello from Docker!", time: new Date() }));
});
server.listen(3000, () => console.log("Server running on port 3000"));
# Dockerfile
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY . .
EXPOSE 3000
CMD ["node", "app.js"]
Build and run:
docker build -t my-app .
docker run -d -p 3000:3000 --name my-app my-app
curl http://localhost:3000
# {"message":"Hello from Docker!","time":"2026-05-18T14:00:00.000Z"}
Dockerfile Instructions
| Instruction | Purpose |
|---|---|
FROM |
Base image to build on |
WORKDIR |
Set the working directory |
COPY |
Copy files from host to image |
RUN |
Execute a command during build |
EXPOSE |
Document which port the app uses |
CMD |
Default command when container starts |
ENV |
Set environment variables |
ARG |
Build-time variables |
Layer caching
Docker caches each instruction as a layer. Order matters for build speed:
# Good — dependencies change less often than code
COPY package*.json ./
RUN npm ci --production
COPY . .
# Bad — any code change invalidates the npm install cache
COPY . .
RUN npm ci --production
Essential Commands
# Images
docker build -t name:tag . # Build an image
docker images # List images
docker rmi image-name # Remove an image
# Containers
docker run -d -p 8080:80 image # Run a container
docker ps # List running containers
docker ps -a # List all containers (including stopped)
docker stop container-name # Stop a container
docker rm container-name # Remove a container
docker logs container-name # View logs
docker exec -it container sh # Open a shell inside a running container
# Cleanup
docker system prune # Remove unused containers, images, networks
Environment Variables
Pass configuration without hardcoding:
docker run -d \
-p 3000:3000 \
-e DATABASE_URL="postgres://user:pass@host/db" \
-e NODE_ENV="production" \
my-app
Or use an env file:
# .env
DATABASE_URL=postgres://user:pass@host/db
NODE_ENV=production
docker run -d -p 3000:3000 --env-file .env my-app
Volumes (Persistent Data)
Containers are ephemeral — when they’re removed, their data is gone. Volumes persist data:
# Named volume (Docker manages the storage location)
docker run -d \
-v postgres-data:/var/lib/postgresql/data \
-e POSTGRES_PASSWORD=secret \
postgres:16
# Bind mount (map a host directory into the container)
docker run -d \
-v $(pwd)/src:/app/src \
-p 3000:3000 \
my-app
Bind mounts are useful for development — edit code on your host, see changes in the container.
Docker Compose
For multi-container applications, Docker Compose defines everything in one file:
# docker-compose.yml
services:
app:
build: .
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgres://user:pass@db:5432/myapp
depends_on:
- db
db:
image: postgres:16
environment:
- POSTGRES_USER=user
- POSTGRES_PASSWORD=pass
- POSTGRES_DB=myapp
volumes:
- postgres-data:/var/lib/postgresql/data
volumes:
postgres-data:
# Start everything
docker compose up -d
# View logs
docker compose logs -f
# Stop everything
docker compose down
# Stop and remove volumes (destroys data)
docker compose down -v
.dockerignore
Like .gitignore — exclude files from the build context:
node_modules
.git
.env
*.md
dist
coverage
This makes builds faster and keeps secrets out of images.
Best Practices
- Use specific base image tags (
node:20-alpine, notnode:latest) for reproducible builds - Use Alpine images when possible — they’re much smaller (5MB vs 900MB)
- Don’t run as root — Add
USER nodeor similar in your Dockerfile - One process per container — Don’t run your app and database in the same container
- Use multi-stage builds for compiled languages to keep final images small
- Never put secrets in images — Use environment variables or secret management tools
Multi-stage build example
# Build stage
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Production stage
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
EXPOSE 3000
USER node
CMD ["node", "dist/index.js"]
What’s Next
With Docker basics down, explore Docker Compose for multi-service development environments, or learn about container orchestration with Kubernetes for production deployments at scale.