Docker for Developers: Images, Containers, and Compose
You're a developer, and you've heard about Docker. Maybe you've tried it, but the concepts of images, containers, and Compose feel fuzzy. Or you're tired of "it works on my machine" and want a consistent environment. This guide cuts through the noise and gives you a practical understanding of Docker's core pieces and how to use them effectively.
Why Docker for Developers?
Docker solves environment inconsistency. Instead of installing dependencies directly on your machine, you package your app and its dependencies into a container. Containers run the same way everywhere: your laptop, your colleague's laptop, and production. This eliminates setup drift and makes onboarding trivial.
Images: The Blueprint
An image is a read-only template. It contains your application code, runtime, libraries, and environment variables. You build images from a Dockerfile, a text file with instructions.
Here's a simple Dockerfile for a Node.js app:
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
Each instruction creates a layer. Layers are cached, so rebuilds are fast when only code changes. Images are stored in registries like Docker Hub or a private registry.
Containers: The Running Instance
A container is a runnable instance of an image. It's isolated but shares the host OS kernel, making it lightweight. You start a container with docker run.
docker run -p 3000:3000 -d my-app
This maps port 3000 on the host to port 3000 in the container and runs it in detached mode. Containers are ephemeral: changes are lost when removed unless you use volumes.
Key commands:
docker ps– list running containersdocker stop <container>– stop a containerdocker rm <container>– remove a containerdocker images– list imagesdocker rmi <image>– remove an image
Docker Compose: Multi-Container Apps
Real apps often need multiple services: a web server, a database, a cache. Docker Compose lets you define and run multi-container applications with a single YAML file.
Here's a docker-compose.yml for a Node.js app with PostgreSQL:
version: '3.8'
services:
web:
build: .
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgres://user:pass@db:5432/mydb
depends_on:
- db
db:
image: postgres:15
environment:
- POSTGRES_USER=user
- POSTGRES_PASSWORD=pass
- POSTGRES_DB=mydb
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
Run docker compose up to start both services. Compose handles networking, so web can reach db by service name.
When to Use Docker Compose vs. Plain Docker
| Scenario | Use |
|---|---|
| Single container | docker run |
| Multiple services (app + DB) | Docker Compose |
| Production orchestration | Kubernetes, Swarm |
Best Practices for Developers
- Use .dockerignore to exclude node_modules, .git, etc., speeding up builds.
- Multi-stage builds to keep final images small.
- Pin versions in FROM statements for reproducibility.
- Don't run as root in production containers.
- Use volumes for persistent data and for live code reloading during development.
Common Pitfalls
Port conflicts: If port 3000 is taken, map to a different host port: -p 3001:3000.
Data loss: Without volumes, database data disappears when the container is removed.
Slow builds: Order Dockerfile instructions from least to most frequently changing to leverage cache.
FAQ
What's the difference between an image and a container?
An image is a read-only template; a container is a running instance of that image. You can run multiple containers from the same image.
Do I need Docker Compose for a single service?
No. Compose shines when you have multiple services. For a single container, docker run is simpler.
How do I persist data in Docker?
Use volumes. Define a named volume in Compose or use -v with docker run to mount a host directory or named volume.
Ready to streamline your Docker workflow? Check out our JSON Formatter to quickly validate and format JSON configuration files like docker-compose.yml or package.json.