Docker Explained: Containers, Images and Why They Matter

A practical introduction to Docker — containers vs virtual machines, images and layers, writing good Dockerfiles, multi-stage builds, volumes, networking, Compose and security best practices.

By Danyal Ahmed - Data Scientist · · 4 min read

"It works on my machine" used to be one of software's most expensive phrases. Different operating systems, library versions and configurations made every environment slightly different. Containers solve this by packaging an application with everything it needs to run into a single, portable unit. Docker made containers mainstream, and today they're the standard building block for deploying software.

Key takeaways

  • A container is an isolated process with its own filesystem, network and resource limits, sharing the host's kernel.
  • An image is the immutable, layered template containers are created from; a Dockerfile describes how to build it.
  • Containers are lighter and faster than virtual machines but provide less isolation.
  • Good Dockerfiles use small base images, multi-stage builds, layer caching and non-root users.
  • Keep containers stateless; store data in volumes or external services.

Containers vs virtual machines

textVirtual machines                        Containers
┌────────┐ ┌────────┐                   ┌──────┐ ┌──────┐ ┌──────┐
│ App A  │ │ App B  │                   │App A │ │App B │ │App C │
│ Libs   │ │ Libs   │                   │ Libs │ │ Libs │ │ Libs │
│Guest OS│ │Guest OS│                   └──────┘ └──────┘ └──────┘
└────────┘ └────────┘                   ─────── Container runtime ───────
──────── Hypervisor ────────            ──────────── Host OS kernel ─────
──────── Hardware ──────────            ──────────── Hardware ───────────
Virtual machine Container
Isolation Strong (separate kernel) Process-level (shared kernel)
Startup Seconds–minutes Milliseconds–seconds
Size GBs MBs–hundreds of MBs
Density Fewer per host Many per host
Use Different OSes, strong isolation Packaging and running apps consistently

Under the hood, Linux containers rely on kernel features: namespaces (isolated views of processes, network and filesystem) and cgroups (resource limits). Docker packages these into a friendly workflow.

Images and layers

An image is built from a Dockerfile, and each instruction creates a layer. Layers are cached and shared between images, which makes builds and downloads efficient.

dockerfileFROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

Order matters for caching: copy dependency manifests and install dependencies before copying source code. Then code changes don't invalidate the (slow) dependency layer.

Essential commands

bashdocker build -t myapp:1.0 .                 # build an image
docker run -d -p 8000:8000 --name api myapp:1.0   # run a container
docker ps                                   # list running containers
docker logs -f api                          # follow logs
docker exec -it api sh                      # shell into a container
docker stop api && docker rm api            # stop and remove
docker images / docker image prune          # list / clean images

Writing production-quality Dockerfiles

Multi-stage builds

Build tools don't belong in production images. Multi-stage builds compile in one stage and copy only the output into a minimal runtime image:

dockerfile# ---- Build stage ----
FROM node:20-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build

# ---- Runtime stage ----
FROM nginx:1.27-alpine
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80

The final image contains static files and nginx. No Node.js, no source code, no dev dependencies.

Best practices checklist

  • Use small, official base images (-slim, -alpine or distroless) and pin versions (not latest).
  • Add a .dockerignore to exclude node_modules, .git, local env files and build artifacts.
  • Run as a non-root user:
    dockerfileRUN useradd --create-home appuser
    USER appuser
  • One main process per container; let the orchestrator handle restarts.
  • Configure via environment variables, not baked-in config files.
  • Never bake secrets into images. Inject them at runtime.
  • Add health checks (HEALTHCHECK, or orchestrator probes).
  • Handle signals for graceful shutdown (SIGTERM), and use the exec form of CMD.
  • Scan images for vulnerabilities in CI (CI/CD pipeline guide).

Data: volumes and statelessness

Container filesystems are ephemeral: remove the container and its changes disappear. For persistent data:

  • Volumes: managed by Docker, the preferred way to persist data (such as a local development database).
  • Bind mounts: map a host directory into the container, which is useful for live-reload in development.

In production, keep application containers stateless. Put state in managed databases, object storage and caches, so containers can be replaced and scaled freely (horizontal vs vertical scaling).

Networking basics

  • -p 8000:8000 publishes a container port on the host.
  • Containers on the same user-defined network can reach each other by service or container name.
  • In production, a load balancer or ingress routes traffic to containers (how load balancers work).

Docker Compose for local development

Compose defines multi-container setups in one file:

yamlservices:
  api:
    build: ./backend
    ports: ["8000:8000"]
    environment:
      DATABASE_URL: postgresql://app:secret@db:5432/app
    depends_on:
      db:
        condition: service_healthy
  db:
    image: postgres:16
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: app
    volumes: ["pgdata:/var/lib/postgresql/data"]
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app"]
      interval: 5s
      retries: 10
volumes:
  pgdata:

docker compose up starts the whole stack, so new developers can be productive in minutes.

From Docker to production

Containers need something to run them reliably in production:

  • A single VPS with Docker Compose and a reverse proxy is fine for small apps (deploying React + FastAPI on a VPS).
  • Managed container services (serverless containers, app platforms) handle scaling and infrastructure for you.
  • Kubernetes orchestrates many containers across clusters, for teams that need its power (Kubernetes explained).

Security considerations

Containers share the host kernel, so isolation is weaker than with VMs:

  • Run as non-root, drop unnecessary Linux capabilities, and use read-only filesystems where possible.
  • Keep base images patched; rebuild regularly.
  • Don't mount the Docker socket into containers unless absolutely necessary. It effectively grants root on the host.
  • Use minimal images to reduce the attack surface.
  • Sign and verify images in sensitive environments.

FAQ

Is Docker the same as a container?

Docker is a platform and toolset for building and running containers. Containers are a broader technology based on Linux kernel features and open standards (OCI). Other tools can build and run the same container images.

Do I need Docker for development?

Not strictly, but it makes environments reproducible, simplifies running databases and other dependencies locally, and aligns development with production. Most teams benefit from at least using Docker Compose for dependencies.

Are containers secure?

They can be, with good practices: minimal images, non-root users, patched bases, restricted capabilities and image scanning. For strong multi-tenant isolation, sandboxed runtimes or VMs may be required.

What's the difference between an image and a container?

An image is a read-only template (the application plus its dependencies). A container is a running instance of an image, with its own writable layer and process.