Docker for Developers: Images, Containers, and Compose
You've built a web app that runs perfectly on your machine, but when you deploy it to a server or share it with a teammate, it breaks. Dependencies are missing, versions conflict, or environment variables are wrong. This is the classic "it works on my machine" problem. Docker solves this by packaging your application and its dependencies into a standardized unit called a container. In this guide, you'll learn the core concepts of Docker—images, containers, and Compose—and how to use them to streamline your development workflow.
What is Docker?
Docker is a platform for building, running, and shipping applications in containers. A container is a lightweight, standalone, executable package that includes everything needed to run a piece of software: code, runtime, system tools, libraries, and settings. Containers isolate applications from each other and from the underlying infrastructure, ensuring consistency across environments.
Images vs. Containers
An image is a read-only template with instructions for creating a container. It's like a snapshot of your application and its environment. A container is a runnable instance of an image. You can create, start, stop, move, or delete containers using the Docker API or CLI.
Think of an image as a class and a container as an object (instance) of that class. You can run multiple containers from the same image, each isolated from the others.
Building an Image with a Dockerfile
A Dockerfile is a text file with instructions to build an image. Here's a simple example for a Node.js app:
# Use an official Node.js runtime as a parent image
FROM node:18-alpine
# Set the working directory
WORKDIR /app
# Copy package.json and package-lock.json
COPY package*.json ./
# Install dependencies
RUN npm install
# Copy the rest of the application code
COPY . .
# Expose the port the app runs on
EXPOSE 3000
# Define the command to run the app
CMD ["node", "server.js"]
To build the image, run:
docker build -t my-node-app .
The -t flag tags the image with a name (my-node-app). The . at the end specifies the build context (current directory).
Running Containers
Once you have an image, you can run a container:
docker run -p 3000:3000 -d my-node-app
This maps port 3000 on your host to port 3000 in the container and runs it in detached mode (-d).
Common commands:
docker ps– list running containersdocker ps -a– list all containers (including stopped)docker stop <container_id>– stop a containerdocker rm <container_id>– remove a containerdocker images– list imagesdocker rmi <image_id>– remove an image
Managing Data with Volumes
Containers are ephemeral; when they are removed, their data is lost. To persist data, use volumes. A volume is a directory outside the container's filesystem, managed by Docker.
docker run -v my_volume:/app/data my-node-app
You can also mount a host directory (bind mount) for development:
docker run -v $(pwd):/app my-node-app
Docker Compose: Multi-Container Applications
Real-world apps often require multiple services: a web server, a database, a cache, etc. Docker Compose lets you define and run multi-container Docker applications using a YAML file. Here's an example docker-compose.yml for a Node.js app with a PostgreSQL database:
version: '3.8'
services:
web:
build: .
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgres://user:password@db:5432/mydb
depends_on:
- db
db:
image: postgres:15-alpine
environment:
- POSTGRES_USER=user
- POSTGRES_PASSWORD=password
- POSTGRES_DB=mydb
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
Run the entire stack with:
docker compose up -d
This builds the web service, pulls the Postgres image, creates a network, and starts both containers. The depends_on ensures the database starts before the web service.
To stop and remove containers, networks, and volumes:
docker compose down -v
Best Practices for Docker in Development
- Use .dockerignore to exclude files like
node_modulesand.gitfrom the build context. - Leverage multi-stage builds to keep images small. For example, build your app in one stage and copy only the production artifacts to a smaller runtime image.
- Pin versions in your Dockerfile (e.g.,
node:18-alpineinstead ofnode:latest) for reproducibility. - Use volumes for persistent data and bind mounts for live code reloading during development.
- Keep containers stateless; store state in volumes or external services.
Comparison: Docker vs. Virtual Machines
| Aspect | Docker Containers | Virtual Machines |
|---|---|---|
| Startup time | Seconds | Minutes |
| Resource usage | Lightweight (shares OS kernel) | Heavy (full OS per VM) |
| Isolation | Process-level | Hardware-level |
| Portability | High (runs anywhere Docker runs) | Limited by hypervisor compatibility |
FAQ
What is the difference between an image and a container?
An image is a read-only template that defines the application and its environment. A container is a running instance of an image. You can create multiple containers from the same image.
When should I use Docker Compose?
Use Docker Compose when your application consists of multiple services (e.g., web server, database, cache) that need to work together. It simplifies orchestration for development and testing.
How do I persist data in Docker?
Use volumes to persist data outside the container's filesystem. Volumes are managed by Docker and can be shared between containers. Bind mounts can also be used to mount host directories.
Ready to containerize your next project? Start by writing a Dockerfile for a simple app, then gradually add services with Docker Compose. For more developer tools, check out our JSON Formatter to validate and beautify your configuration files.