Docker for Beginners: Containers, Images, and Compose Explained
Docker revolutionized how we build, ship, and run applications. Instead of worrying about dependencies and environment differences, you package everything into a container that runs identically everywhere — on your laptop, your colleague’s machine, or a production server.
What Is Docker?
Docker is a platform that uses containers — lightweight, isolated environments that package an application with all its dependencies. Unlike virtual machines, containers share the host OS kernel, making them fast to start and efficient with resources.
Key terms:
- Image — A read-only template containing your application and its dependencies
- Container — A running instance of an image
- Dockerfile — A text file with instructions to build an image
- Docker Compose — A tool for defining multi-container applications
Installing Docker
macOS
brew install --cask docker
Then open Docker Desktop from Applications.
Ubuntu / Debian
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER
Log out and back in for group changes.
Windows
Download Docker Desktop from docker.com/products/docker-desktop. Requires WSL 2.
Verify:
docker --version
docker run hello-world
Running Your First Container
docker run -it ubuntu:22.04 bash
This downloads the Ubuntu 22.04 image and starts an interactive shell. Type exit to leave.
Common run flags
docker run -d \
--name my-nginx \
-p 8080:80 \
-v ./html:/usr/share/nginx/html \
--restart unless-stopped \
nginx:latest
Flag breakdown:
-d— Run in background (detached)--name— Give the container a name-p 8080:80— Map host port 8080 to container port 80-v— Mount a volume (bind mount)--restart unless-stopped— Auto-restart on crash or reboot
Visit http://localhost:8080 to see Nginx running.
Managing Containers
# List running containers
docker ps
# List all containers (including stopped)
docker ps -a
# Stop a container
docker stop my-nginx
# Start a stopped container
docker start my-nginx
# Restart
docker restart my-nginx
# Remove a container
docker rm my-nginx
# Remove all stopped containers
docker container prune
# View logs
docker logs my-nginx
docker logs -f my-nginx # Follow/stream logs
# Execute command in running container
docker exec -it my-nginx bash
Working with Images
# Search for images
docker search nginx
# Pull an image
docker pull nginx:latest
# List downloaded images
docker images
# Remove an image
docker rmi nginx:latest
# Remove unused images
docker image prune -a
Docker Hub (hub.docker.com) is the default image registry where you’ll find official images for most software.
Building Custom Images with Dockerfile
Create a Dockerfile for a Node.js application:
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
Build and run:
docker build -t my-app:1.0 .
docker run -d -p 3000:3000 my-app:1.0
Dockerfile Instructions Reference
| Instruction | Purpose |
|---|---|
FROM | Base image to build on |
WORKDIR | Set working directory |
COPY | Copy files from host to image |
RUN | Execute command during build |
EXPOSE | Document which port the app uses |
CMD | Default command when container starts |
ENV | Set environment variables |
ARG | Build-time variables |
Dockerfile Best Practices
- Use specific tags —
node:20-alpinenotnode:latest - Use multi-stage builds to reduce image size
- Copy package files first to leverage layer caching
- Use
.dockerignoreto exclude unnecessary files - Run as non-root user for security
Example .dockerignore:
node_modules
.git
.env
Dockerfile
docker-compose.yml
README.md
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
CMD ["node", "dist/server.js"]
This produces a much smaller final image because build tools and source code aren’t included.
Docker Compose
Docker Compose defines multi-container applications in a single YAML file. This is ideal for apps that need a web server, database, and cache running together.
Create docker-compose.yml:
version: "3.8"
services:
web:
build: .
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgres://user:pass@db:5432/myapp
depends_on:
- db
- redis
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
POSTGRES_DB: myapp
volumes:
- pgdata:/var/lib/postgresql/data
ports:
- "5432:5432"
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
pgdata:
Compose Commands
# Start all services
docker compose up -d
# View logs
docker compose logs -f
# View logs for one service
docker compose logs -f web
# Stop all services
docker compose down
# Stop and remove volumes (deletes data!)
docker compose down -v
# Rebuild images after code changes
docker compose up -d --build
# Scale a service
docker compose up -d --scale web=3
Volumes and Data Persistence
Containers are ephemeral — data is lost when they’re removed. Use volumes to persist data:
# Create a named volume
docker volume create my-data
# Use it in a container
docker run -v my-data:/app/data my-app
# List volumes
docker volume ls
# Inspect a volume
docker volume inspect my-data
# Remove unused volumes
docker volume prune
Volume types
- Named volumes (
my-data:/app/data) — Managed by Docker, best for databases - Bind mounts (
./local-dir:/app/data) — Maps a host directory, good for development - tmpfs — Stored in memory only, useful for sensitive data
Docker Networking
# Create a custom network
docker network create my-network
# Run containers on the same network
docker run -d --name api --network my-network my-api
docker run -d --name web --network my-network my-web
# Containers can reach each other by name
# From 'web': curl http://api:3000
Docker Compose automatically creates a network for your services, so containers can communicate using service names.
Useful Docker Commands
| Task | Command |
|---|---|
| Run a container | docker run -d -p 8080:80 nginx |
| List running containers | docker ps |
| Stop a container | docker stop <name> |
| View container logs | docker logs -f <name> |
| Shell into a container | docker exec -it <name> bash |
| Build an image | docker build -t name:tag . |
| List images | docker images |
| Start Compose stack | docker compose up -d |
| Stop Compose stack | docker compose down |
| View resource usage | docker stats |
| Clean up everything | docker system prune -a |
Real-World Example: WordPress with MySQL
version: "3.8"
services:
wordpress:
image: wordpress:latest
ports:
- "8080:80"
environment:
WORDPRESS_DB_HOST: db
WORDPRESS_DB_USER: wp_user
WORDPRESS_DB_PASSWORD: wp_pass
WORDPRESS_DB_NAME: wordpress
volumes:
- wp_data:/var/www/html
depends_on:
- db
db:
image: mysql:8.0
environment:
MYSQL_DATABASE: wordpress
MYSQL_USER: wp_user
MYSQL_PASSWORD: wp_pass
MYSQL_ROOT_PASSWORD: root_pass
volumes:
- db_data:/var/lib/mysql
volumes:
wp_data:
db_data:
Run docker compose up -d and visit http://localhost:8080 for a fully working WordPress site.
Security Tips
- Don’t run containers as root — Add
USER nodein your Dockerfile - Scan images for vulnerabilities:
docker scout cves my-image - Use official images from Docker Hub when possible
- Keep images updated — Pull new versions regularly
- Don’t store secrets in images — Use environment variables or Docker secrets
- Limit container resources:
docker run --memory=512m --cpus=1 my-app
Conclusion
Docker simplifies application deployment by packaging everything into portable, reproducible containers. Start with docker run to get comfortable, then progress to Dockerfiles for custom images and Docker Compose for multi-service applications. The consistency Docker provides between development and production environments alone makes it an essential tool in any developer’s toolkit.