DeployEasy
DockerBeginner

What Is Docker? Images, Containers, Volumes, and Compose Explained

Learn how Docker works through images, containers, Dockerfiles, volumes, networks, and Compose with a practical Node.js example.

· 3 min read· 578 words
Table of contents

Docker packages an application and its dependencies into a repeatable image. A container is a running instance of that image with an isolated process, filesystem view, and network namespace.

Docker does not replace application architecture or operations. It gives teams a consistent unit for building, testing, shipping, and running.

Image, container, registry, and daemon

  • An image is an immutable template made of filesystem layers.
  • A container is a runtime instance of an image.
  • A registry stores and distributes images.
  • The Docker Engine daemon creates networks, containers, volumes, and images.

Unlike a full virtual machine, a container shares the host kernel. It usually starts faster and uses fewer resources, but it still needs correct isolation, updates, and limits.

A simple Node.js Dockerfile

FROM node:22-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:22-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
RUN npm ci --omit=dev
COPY --from=build /app/dist ./dist
EXPOSE 3000
CMD ["node", "dist/server.js"]

FROM selects a base image, WORKDIR sets the working directory, COPY adds files, RUN executes build steps, and CMD defines the default process. Multi-stage builds leave build tools out of the runtime image.

Build and run it

docker build -t my-api:1.0 .
docker run --rm -p 3000:3000 --env-file .env my-api:1.0

The -p mapping publishes host port 3000 to container port 3000. Environment variables and secrets should come from the runtime environment, not be baked into the image.

Layers and .dockerignore

Docker caches layers. Copy lockfiles and install dependencies before copying frequently changing source files so builds stay fast. Use .dockerignore to exclude .git, node_modules, local logs, and secrets:

node_modules
.git
.env*
dist
*.log

Volumes preserve data

The writable container layer is disposable. Use a named volume for stateful data:

docker volume create postgres_data
docker run -v postgres_data:/var/lib/postgresql/data postgres:16

Back up important data separately. A volume is persistence across container replacement, not a backup.

Networks let services find one another

Compose services on the same network can use service names as hostnames. An API should connect to db:5432, not localhost:5432, when PostgreSQL is another service.

Docker Compose

Compose describes multiple services in YAML:

services:
  api:
    build: .
    ports: ["3000:3000"]
    depends_on: [db]
  db:
    image: postgres:16-alpine
    volumes: ["postgres_data:/var/lib/postgresql/data"]

volumes:
  postgres_data:

Useful commands include:

docker compose up -d
docker compose ps
docker compose logs -f api
docker compose exec api sh
docker compose down

Use docker compose down -v only for disposable data because it removes project volumes.

Development and production are different

Development may mount source code and use hot reload. Production should use a built, pinned image, protected secrets, health checks, restart policy, log limits, private networks, backups, and a rollback plan.

Do not expose databases or admin ports publicly by default. Scan and update base images, run as a non-root user when supported, and set CPU or memory limits for noisy workloads.

Common mistakes

  • Using latest and losing track of the deployed version.
  • Copying secrets into an image layer.
  • Treating a volume as a backup.
  • Connecting between containers through localhost.
  • Publishing every internal port to the internet.
  • Forgetting .dockerignore and sending a huge build context.
  • Assuming depends_on alone means an application is ready.

Docker is most useful when the image is reproducible, the runtime is observable, and data recovery is planned before a container fails.

Continue reading