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.
"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,-alpineor distroless) and pin versions (notlatest). - Add a
.dockerignoreto excludenode_modules,.git, local env files and build artifacts. - Run as a non-root user:
dockerfile
RUN 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:8000publishes 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.