The Container Model for Node.js
A container is not a small virtual machine, and treating it as one is where most confusion about Docker starts.
Search across all documentation pages
A container is not a small virtual machine, and treating it as one is where most confusion about Docker starts.
It is a Linux process, isolated by kernel features that already existed before Docker gave them a friendly interface, packaged from a filesystem snapshot that never changes once built.
For a Node.js service, the packaging problem is specific: npm run start reaches into system libraries, filesystem paths, and the exact node_modules tree your lockfile resolved, and a container is the mechanism the industry converged on to make that reach reproducible everywhere the service runs.
Docker Basics shows the day-to-day commands and Dockerfile patterns for a Node API; this page is the model underneath them - what a container actually is, and why the practices in the rest of this section exist.
Before containers, "it runs on my machine" meant something narrower than teams wanted it to.
A Node service assumes a specific runtime, specific system libraries, and a specific dependency tree, and any mismatch between where code was written and where it ran produced bugs that had nothing to do with the code itself.
Virtual machines solved this by virtualizing an entire computer - CPU, disk, a full guest operating system - which guarantees isolation but costs minutes to boot and gigabytes per instance.
Containers solve the same problem far more cheaply by isolating at the process level instead of the hardware level, using kernel features - namespaces and control groups - that had existed in Linux for years before Docker packaged them into a workflow in 2013.
An image is the blueprint: a read-only, layered filesystem snapshot that never changes once built, distributed through a registry the same way npm packages are distributed through the npm registry.
A container is a living instance of that image: the same read-only layers, plus a thin writable layer on top, plus a set of kernel isolation rules that make the process inside believe it has the machine to itself.
A useful way to picture an image is a stack of transparent overlays, each recording only what changed from the one beneath it - a base OS layer, then a dependencies layer, then an application-code layer, stacked and viewed together as one filesystem.
Running a container adds one more overlay on top, writable, that disappears the moment the container is removed - which is exactly why containers are meant to be disposable and images are not.
Two Linux kernel primitives do essentially all of the isolation work.
Namespaces give a process its own view of shared resources instead of the real one: a PID namespace makes it the only process it can see (and it becomes PID 1 inside its own tree), a network namespace gives it its own loopback and interfaces, a mount namespace gives it its own filesystem view.
Cgroups (control groups) do the opposite job - not hiding resources, but capping them, enforcing a CPU share or a memory ceiling so one noisy container cannot starve the host or its neighbors.
Together, a namespace-isolated, cgroup-limited process is a container - there is no separate "container runtime" magic beyond orchestrating these two primitives and a filesystem.
Layering matters operationally, not just conceptually: each Dockerfile instruction that changes the filesystem produces a new, content-addressed layer, and Docker caches and reuses layers that haven't changed.
That's the real reason COPY package*.json ./ and RUN npm ci come before COPY src ./src in a well-written Dockerfile - it isn't superstition, it's making sure the slow dependency-install layer stays cached across builds where only application code changed.
┌─────────────────────────────┐
│ writable layer (container) │ discarded when the container is removed
├─────────────────────────────┤
│ layer: app source │ from `COPY src ./src`
├─────────────────────────────┤
│ layer: production deps │ from `RUN npm ci --omit=dev`
├─────────────────────────────┤
│ layer: base image │ e.g. node:24-bookworm-slim
└─────────────────────────────┘
all layers share one Linux kernel with the host
When a container starts, the kernel doesn't boot anything - it applies namespaces and cgroup limits to a process and hands it the merged view of those layers as its filesystem.
That's also why containers start in milliseconds where VMs take minutes: there's no operating system to boot, just a process to isolate.
For Node specifically, the convention of one process per container pairs directly with the PID namespace's PID 1 rule: your Node process becomes responsible for behavior a real init system would otherwise handle, including reaping signals correctly on shutdown - the reason graceful SIGTERM handling matters as much as it does once a service is containerized.
Because containers share the host kernel, they are not a hard security boundary the way a VM is - a kernel-level vulnerability can, in principle, cross a container boundary that a hypervisor would have stopped.
That single fact motivates most of the hardening practices in this section: running as a non-root user limits what a compromised process can do to the host even if it escapes application-level constraints, and a minimal base image (distroless or Alpine) shrinks the amount of code an attacker has to work with in the first place.
It's also worth separating "Docker" from "containers" as a technology - Docker popularized the developer-facing workflow, but the actual runtime work today is typically done by lower-level tools (containerd, runc) implementing the OCI (Open Container Initiative) standard, which is why Kubernetes no longer needs a Docker daemon at all to run containers built by Docker.
Multi-stage builds exist because the naive approach - installing every dependency, including dev tools, into one image - produces bloated, slow-to-pull, larger-attack-surface artifacts; Multi-Stage Builds covers the pattern that keeps build-only tooling out of the runtime image entirely.
A single container is also not, by itself, a production deployment story - it has no restart policy, no scheduling across machines, no rolling updates.
That's the boundary where this page's model ends and orchestration begins: a scheduler that decides where containers run and keeps them running is a different layer of concern, covered in The Container Orchestration Model.
| Isolation Approach | Strength | Weakness | Best Fit |
|---|---|---|---|
| Virtual machine | Strong isolation - separate kernel per guest | Slow boot, heavy per-instance overhead | Multi-tenant hosting, untrusted workloads |
| Container (namespaces + cgroups) | Fast start, small footprint, portable image | Shares host kernel - weaker isolation than a VM | Packaging and deploying application services |
| Bare process on a host | No isolation overhead at all | No dependency or resource isolation between apps | Single-tenant, tightly controlled hosts |
An image is an immutable, layered filesystem snapshot - a blueprint that never changes once built. A container is a running instance of that image: the same read-only layers plus a writable layer and a set of kernel isolation rules applied to a live process.
A VM has to boot a full guest operating system before anything can run inside it. A container skips that entirely - the kernel just applies namespaces and cgroup limits to a process and hands it a merged filesystem view, so "starting" a container is closer to starting a process than booting a machine.
Namespaces give a process its own private view of shared resources - its own process list, its own network interfaces, its own filesystem mounts - instead of the real host-wide view. Cgroups cap what that process can consume, like CPU share or memory ceiling, so one container can't starve its neighbors.
No - Docker popularized the workflow, but the OCI (Open Container Initiative) standard means other tools (containerd, runc, Podman) can build and run the same images. Kubernetes, for example, runs containers without a Docker daemon at all.
Docker caches each layer and reuses it across builds if nothing that produced it has changed. Putting slow, rarely-changing steps (installing dependencies) before fast, frequently-changing ones (copying application source) means most builds only re-run the cheap final layers.
Weaker than a virtual machine's, because containers share the host kernel rather than running their own. That's exactly why non-root users, read-only filesystems, and minimal base images matter more for containers than they would for a fully virtualized guest.
It lives in the container's writable top layer, which is discarded the moment the container is removed. Anything that needs to persist beyond a single container's lifetime has to go to an external volume or a backing service outside the container entirely.
It pairs directly with the PID namespace, where the container's main process becomes PID 1 inside its own isolated process tree. Keeping that to one clear process (your Node server) also keeps restart policies, health checks, and log streams simple and unambiguous.
This page covers what a single container is. Orchestration - deciding where containers run, restarting failed ones, scaling replica counts - is a separate layer built on top of that, covered in The Container Orchestration Model.
Every package and binary in a base image is something an attacker could potentially use if they get code execution inside the container. A smaller base image reduces that attack surface and typically produces a smaller, faster-to-pull artifact as a side benefit.
No - once a container hits its cgroup memory ceiling, the kernel intervenes, typically killing the process (an OOM kill) rather than letting it exceed the limit. This is why setting realistic memory limits, informed by actual usage, matters for stability.
An individual image layer is built for a specific CPU architecture (e.g., x86-64 or arm64), though registries commonly host multi-architecture image manifests that let the same tag resolve to the right variant for the pulling machine automatically.
Stack versions: This page is conceptual and not tied to a specific stack version.
Reviewed by Chris St. John·Last updated Jul 19, 2026