Docker for Developers: Images, Containers, and Compose
You've probably heard about Docker and how it can simplify your development workflow. But if you're new to containers, the terminology and commands can be overwhelming. This guide cuts through the noise and gives you a practical understanding of Docker images, containers, and Compose—so you can start using them effectively today.
What Is Docker and Why Should You Care?
Docker is a platform that packages your application and its dependencies into a standardized unit called a container. Containers are lightweight, portable, and isolated from each other, which means you can run your app consistently on your laptop, a colleague's machine, or a production server.
The key benefit? "It works on my machine" becomes a thing of the past. Docker ensures that the environment your code runs in is identical everywhere.
Images vs. Containers: The Building Blocks
Before you start, you need to understand the difference between an image and a container.
- Image: A read-only template that contains your application code, runtime, libraries, and dependencies. Think of it as a snapshot or a blueprint.
- Container: A runnable instance of an image. You can start, stop, move, and delete containers. Multiple containers can run from the same image.
In short: an image is the recipe, and a container is the dish you cook from it.
Essential Docker Commands for Developers
Here are the commands you'll use most often:
docker pull <image>– Download an image from a registry like Docker Hub.docker images– List all local images.docker run <image>– Create and start a container from an image.docker ps– List running containers. Add-ato see all containers, including stopped ones.docker stop <container>– Gracefully stop a running container.docker rm <container>– Remove a stopped container.docker rmi <image>– Remove an image.docker exec -it <container> bash– Open an interactive shell inside a running container.
For example, to run a simple Nginx web server:
docker run -d -p 8080:80 nginx
This pulls the nginx image (if not already present), starts a container in detached mode (-d), and maps port 8080 on your host to port 80 in the container (-p 8080:80).
Building Your Own Image with a Dockerfile
To package your application, you create a Dockerfile—a text file with instructions. 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 /usr/src/app
# Copy package.json and install dependencies
COPY package*.json ./
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", "app.js"]
Build the image with:
docker build -t my-node-app .
Then run it:
docker run -p 3000:3000 my-node-app
Simplifying Multi-Container Apps with Docker Compose
Real-world applications often need multiple services: a web server, a database, a cache, etc. Managing them individually with docker run becomes tedious. Docker Compose lets you define and run multi-container applications using a YAML file.
Create a docker-compose.yml file:
version: '3.8'
services:
web:
build: .
ports:
- "3000:3000"
depends_on:
- db
db:
image: postgres:15
environment:
POSTGRES_PASSWORD: example
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
Then start everything with:
docker compose up -d
Compose handles networking, volumes, and dependencies for you. To stop and remove containers, networks, and volumes, run docker compose down.
Best Practices for Docker in Development
- Use .dockerignore: Exclude unnecessary files (like
node_modules) from the build context to speed up builds. - Leverage multi-stage builds: Keep your final image small by separating build-time dependencies from runtime.
- Pin versions: Specify exact image tags (e.g.,
node:18.16.0-alpine) for reproducibility. - Run as non-root: Create a user in your Dockerfile and switch to it with
USERto improve security. - Use volumes for persistent data: Databases and other stateful services should store data in volumes, not inside the container.
Common Pitfalls and How to Avoid Them
- Bloated images: Start from a minimal base image (like Alpine) and clean up package caches.
- Port conflicts: Ensure host ports are not already in use. Use
docker psto check. - Ignoring logs: Use
docker logs <container>to debug issues. - Not using Compose for local dev: Even simple apps benefit from Compose's declarative setup.
FAQ
What's the difference between a Docker 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 that image. You can create many containers from a single image.
Do I need Docker Compose for a single-container app?
Not necessarily. For simple apps, docker run is sufficient. However, Compose makes it easier to manage environment variables, volumes, and networking, even for one service, and simplifies scaling later.
How do I persist data in Docker?
Use volumes. Volumes are stored outside the container's filesystem and persist even if the container is deleted. In Compose, define a volume and mount it to a path in the container (e.g., volumes: - mydata:/var/lib/data).
Ready to optimize your Docker workflow? When you need to analyze container logs, try our Nginx Log Analyzer to quickly parse and visualize log data—perfect for debugging containerized web servers.