Docker for Developers: Images, Containers and Compose

DevOps2026-10-12TryQuickToolBox

You cloned a project, ran the setup instructions, and hit a wall: a missing system library, a database that won't start, or a Node version mismatch. Docker exists to remove that class of problem. Instead of installing services directly on your machine, you describe them once and run them in isolated containers that behave the same everywhere.

This guide explains the three ideas you actually need: images, containers, and Compose. It stays practical, with commands you can run as you read.

Images vs containers: the mental model

An image is a read-only template. It contains a filesystem snapshot plus metadata such as the default command and exposed ports. A container is a running (or stopped) instance of that image with a thin writable layer on top.

If you come from object-oriented programming, the analogy is close: an image is a class, a container is an object. You can start many containers from one image, and changes inside a container do not modify the image.

ConceptWhat it isTypical command
ImageImmutable templatedocker build, docker pull
ContainerRunning instancedocker run, docker ps
VolumePersistent storagedocker volume create
ComposeMulti-container definitiondocker compose up

Your first image with a Dockerfile

A Dockerfile is a recipe. Each instruction creates a layer, and Docker caches layers so rebuilds are fast when nothing changed. Here is a small Node.js example:

FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]

Two habits matter here. First, copy dependency manifests before the rest of the source so the npm ci layer is cached when only application code changes. Second, prefer a slim base image such as alpine to reduce size and attack surface.

Build and run it:

docker build -t myapp:1.0 .
docker run --rm -p 3000:3000 myapp:1.0

The -p 3000:3000 flag maps host port 3000 to container port 3000. --rm deletes the container when it exits, which keeps your machine tidy during development.

Everyday container commands

These cover most daily work:

When a container exits immediately, read the logs first. The most common causes are a wrong CMD, a missing environment variable, or the process binding to 127.0.0.1 instead of 0.0.0.0 inside the container.

Why Compose exists

A real application usually needs more than one process: an API, a database, maybe a cache. Running three docker run commands with long flag lists is error-prone. Docker Compose lets you declare all services in one YAML file and start them together.

services:
  api:
    build: .
    ports:
      - "3000:3000"
    environment:
      DATABASE_URL: postgres://app:secret@db:5432/app
    depends_on:
      - db
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: app
    volumes:
      - db_data:/var/lib/postgresql/data
volumes:
  db_data:

Notice that the API connects to db, not localhost. Compose creates a private network where each service is reachable by its service name. This is the single most common source of confusion for newcomers.

Start everything with docker compose up, or docker compose up -d to run in the background. Rebuild after code changes with docker compose up --build. Tear down with docker compose down, and add -v if you also want to delete the named volumes.

Volumes, bind mounts, and live reload

Containers are ephemeral; data inside them disappears when removed. Two mechanisms solve this:

  1. Named volumes — managed by Docker, ideal for database files. They persist across container restarts and are declared in the volumes: section.
  2. Bind mounts — map a host directory into the container, ideal for source code during development.
services:
  api:
    build: .
    volumes:
      - ./src:/app/src

With a bind mount, editing files on your host is reflected inside the container. Combine this with a file watcher (nodemon, uvicorn --reload, air) and you get live reload without rebuilding the image.

A practical development workflow

  1. Write a Dockerfile for your app and keep it minimal.
  2. Add a docker-compose.yml with the app plus its dependencies.
  3. Use bind mounts for source code and named volumes for data.
  4. Keep secrets out of the file; use a .env file and reference variables with ${VAR}.
  5. Add a .dockerignore containing node_modules, .git, and build output to speed up builds.
  6. Run docker compose up --build and iterate.

If your service produces logs you need to inspect later, remember that container logs are just text. Tools that parse server logs, such as an nginx log analyzer, pair well with containers because you can pipe or copy logs out without installing anything on the host.

Common pitfalls

FAQ

What is the difference between an image and a container?

An image is an immutable template containing a filesystem and metadata. A container is a running instance of that image with an added writable layer. You can start many containers from a single image.

Do I need Docker Compose for a single service?

No. A single docker run command is enough for one container. Compose becomes valuable when you have multiple services, shared networks, or environment variables you want to declare in one place.

How do containers talk to each other?

Compose puts all services on a shared network where each service is reachable by its service name. Use that name as the hostname, for example db:5432, instead of localhost.

Docker rewards a small upfront investment. Once images, containers, and Compose click, onboarding a teammate or switching machines stops being a setup ritual and becomes a single command.