What Is Docker? Containers Explained
Docker is a platform for packaging an application together with its dependencies into a portable unit called a container, then running that unit consistently across machines. It became popular because it solved a long-standing problem: code that worked on one developer's laptop but failed in testing or production because the surrounding environment differed.
What a container actually is
A container is an isolated process running on a host operating system. From inside, it looks like it has its own filesystem, network interface, and set of installed software. In reality it shares the host's kernel with every other container and with the host itself.
This is the key distinction from a virtual machine. A VM runs a complete guest operating system on a hypervisor, including its own kernel. A container does not boot a kernel at all — it relies on the host's. That makes containers far lighter: they start in a fraction of a second and use much less memory.
On Linux, this isolation is built from native kernel features. Namespaces give each container its own view of process IDs, network, mounts, and users. Control groups (cgroups) limit how much CPU and memory a container may consume. Docker did not invent these primitives; it packaged them behind a repeatable workflow.
Images, containers, and the registry
The unit you build and share is an image: a read-only template containing a filesystem snapshot plus metadata about how to run it. A container is a running instance of an image — you can start many containers from one image, and each gets its own writable layer on top.
Images are built in layers. Each build instruction adds a layer, and layers are cached and shared. If two images start from the same base, that base is stored once on disk and reused. This layering is why rebuilds are fast and why pulling a new image often downloads only the changed layers.
Images live in a registry. Docker Hub is the default public registry, but organizations commonly run private ones such as GitHub Container Registry or Amazon ECR. The lifecycle is simple: build an image, push it to a registry, then pull and run it anywhere.
How you build an image: the Dockerfile
An image is defined by a Dockerfile, a text file of build instructions. A minimal example for a Node.js app:
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
Each line matters. RUN executes a command at build time and bakes the result into a layer, and CMD defines the default process the container runs at start. The ordering is deliberate: copying package.json and installing dependencies before the rest of the source keeps the dependency layer cached when only application code changes. Our Dockerfile Builder can scaffold a starting point, and our Docker configuration guide covers the deeper details.
The Docker architecture
Docker uses a client–server model. The Docker daemon (dockerd) is a background process that builds images, runs containers, and manages networks and volumes. The Docker CLI is a client that sends instructions to the daemon over an API; when you type docker run, the daemon does the work.
Under the daemon sits a lower-level container runtime (containerd, which in turn uses runc) that talks to the kernel. You rarely touch these directly, but knowing they exist explains why Docker is one option among several that all produce and run the same standardized (OCI) container images.
On macOS and Windows there is no Linux kernel to share, so Docker Desktop runs a lightweight Linux VM in the background and your containers run inside it. This is invisible day to day, but it explains why file-sharing and networking can behave differently than on native Linux.
Running multiple containers: Compose
Real applications are rarely one container — a typical stack might be a web app, a database, and a cache. Docker Compose describes that set in one YAML file so you can start everything with a single command:
services:
web:
build: .
ports:
- "3000:3000"
depends_on:
- db
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: secret
volumes:
- dbdata:/var/lib/postgresql/data
volumes:
dbdata:
Compose creates a private network so services reach each other by name (web connects to db on its hostname), wires up ports, and manages startup order. You can generate a starting file with our Docker Compose Builder, and a YAML ↔ JSON converter helps when validating or transforming these YAML files.
Persisting data and exposing services
Containers are ephemeral by design: when one is removed, the changes written to its writable layer are gone. To keep data, mount a volume (Docker-managed storage) or a bind mount (a host directory) into the container. Databases and uploads belong there.
Networking works through port publishing. A container listening on port 3000 is not reachable from outside until you map it, for example -p 8080:3000 to expose it on the host's port 8080. Containers sharing a user-defined network reach each other without published ports.
Why developers use it, and where it fits
The practical payoffs are consistency and speed. A new teammate runs one command instead of a page of setup instructions, CI runs tests in the same image that ships to production, and microservices each get an isolated runtime without conflicting dependencies. Containers are also the foundation orchestrators such as Kubernetes scale across machines, and they slot naturally into CI/CD — for example, building and pushing images from a GitHub Actions workflow.
Docker is not a fit for everything. It does not magically make software faster, it adds a layer to learn and operate, and GPU or kernel-level workloads need extra configuration. For a single small static site, a container can be more overhead than benefit.
Common pitfalls
A few mistakes recur often enough to be worth naming:
- Storing data in the container. Without a volume, your database vanishes on the next rebuild. Map persistent paths to volumes from the start.
- Baking secrets into images. Anything in an image layer is visible to anyone who can pull it. Pass secrets at runtime via environment variables or a secrets mechanism, never
COPYa credentials file into the image. - Huge images. Starting from a full OS base and copying build tooling into the final image bloats it. Use slim or Alpine bases and multi-stage builds to keep only what runtime needs.
- Running as root. Containers default to the root user inside, which widens the blast radius if one is compromised. Add a non-root user in the Dockerfile.
- Ignoring the build context. Without a
.dockerignore, Docker sends your entire directory — includingnode_modulesand.git— to the daemon, slowing builds and risking leaked files.
Get those right and Docker delivers on its core promise: the same application behaving the same way from your laptop to production.
Frequently Asked Questions
An image is a read-only template that bundles a filesystem and run instructions. A container is a running instance of that image, with its own writable layer. You can start many containers from a single image.
No. A virtual machine runs a full guest operating system with its own kernel on a hypervisor, while a container shares the host's kernel and isolates only the process. Containers are therefore much smaller and start in a fraction of a second.
Containers rely on the Linux kernel, so on macOS and Windows Docker Desktop runs a lightweight Linux VM behind the scenes and runs your containers inside it. From the command line the experience is nearly identical across platforms.
Use a volume or a bind mount to store data outside the container's writable layer. Anything written only inside the container is lost when it is removed, so databases and uploads should always live on a volume.
Docker Compose defines a multi-container application in one YAML file so you can start the whole stack, such as a web app plus a database, with a single command. It also creates a shared network so the services can reach each other by name.