Skip to content

15. When to use Docker and when not

Docker is close to universal now, which makes this chapter harder than the equivalent chapter for the other tools. “Should we use Postgres” is a live question. “Should we use containers” mostly is not, and the reflex is to say yes.

But the reflex hides real costs, and there are cases where containers are a net loss. This chapter tries to name both sides honestly.

Local development environments. This is the least debatable case in the guide. One compose.yaml in the repository, and a new contributor gets the right Postgres version, the right Redis, the right everything, in one command, on any operating system. It replaced a category of onboarding documentation that was permanently out of date, and nothing else does the job as well.

CI. Every job in a clean, identical environment, with real dependencies rather than mocks — a real Postgres for integration tests, a real Kafka for consumer tests. Testcontainers has made this a normal practice rather than an ambitious one.

Anything that will be orchestrated. Kubernetes, ECS, Nomad, Cloud Run and every serverless container platform take OCI images. If your deployment target is one of those, the question is settled — the only choice left is which tool builds the image.

Polyglot systems. Five services in four languages, each with its own runtime and dependency manager. A container is the only artifact format all of them agree on. Without containers this is where the operations burden becomes genuinely painful.

Reproducing a specific environment. Testing against three PostgreSQL versions, or reproducing a customer’s exact stack, or running a tool that needs an ancient library you refuse to install on your machine. Disposable and exact.

Distributing software that has dependencies. If you ship a tool with a complicated runtime, docker run yourorg/tool is kinder to your users than a twelve-step install document.

A single static binary. Go and Rust already produce one file with no dependencies. scp it and run it under systemd. You get: no image builds, no registry, no daemon, no layer cache, faster deploys, and simpler debugging.

The honest counter-argument is real, though: if everything else you run is containerized, making one service special costs more than the container does. Consistency has value. But “we containerize because we containerize” is worth noticing when the binary is the entire artifact.

Desktop and GUI applications. X11 forwarding, audio, GPU passthrough and file dialogs through a container boundary are all solvable and all annoying. Flatpak, Snap and AppImage were built for this and are better at it.

Very small teams on one machine. If you have one VPS and three services, systemd units and a package manager work, and they involve one fewer layer to understand when something breaks at 2 a.m. Docker still buys you dependency isolation, which is worth something — just less than the usual framing implies.

Latency-critical or hardware-adjacent work. High-frequency trading, kernel-bypass networking, real-time audio, anything with GPUs and specific drivers. It is all possible in containers and it is all more complicated in containers. The overhead of the container itself is near zero — the overhead is in the configuration and the debugging.

Stateful systems where you have not decided about the data. Chapter 9’s position: run stateless things in containers freely, and stateful things only when you have answered what happens to 400 GB when the machine dies. A containerized Postgres on a single host with a bind mount is fine, and it is not meaningfully better than a Postgres installed from a package — you have moved the problem, not solved it. The Kubernetes storage chapter is the longer version of this argument.

Windows-native applications. Windows containers exist and work, and the ecosystem is a fraction of the Linux one. Evaluate carefully rather than assuming parity.

PODMAN daemonless, rootless by default, same images,
nearly the same CLI. Chapter 4 covered it.
Choose it when: the daemon-as-root model is a
problem, you are on RHEL/Fedora, or you want
rootless without caveats.
Cost: Docker Desktop's polish, some Compose
edge cases, tools that assume a socket.
CONTAINERD + the layer under Docker, driven directly.
NERDCTL A nearly drop-in CLI. Choose it when you want
the runtime without the daemon's extra surface —
common on Kubernetes nodes.
LIMA / COLIMA / Docker Desktop replacements on macOS.
ORBSTACK OrbStack in particular is noticeably faster for
filesystem-heavy work and uses far less battery.
Also relevant if Docker Desktop's licensing
applies to your company.
BUILDAH daemonless image building. Scriptable in shell
without a Dockerfile at all.
KANIKO builds inside a container with no privileges
and no daemon. The standard answer for building
images in Kubernetes without mounting the socket
(chapter 11).
KO Go-specific. No Dockerfile, no daemon, seconds
to build. If you ship Go, try it before writing
another Dockerfile.
JIB the same idea for Java, from Maven or Gradle.
BUILDPACKS (Paketo, Heroku) detect your language and produce
an optimized image with no Dockerfile. Excellent
defaults, automatic base image patching without
rebuilding your app layer. Cost: less control,
and when you need to customize, the model is
unfamiliar.
NIX nix2container / dockerTools build genuinely
reproducible images with exact dependency
graphs. The most correct option here, and the
steepest learning curve by a wide margin.
gVisor a userspace kernel. Cheaper than a VM, stronger
than a container. Syscall-heavy workloads pay.
KATA a real VM per container, OCI-compatible.
FIRECRACKER microVMs, ~125 ms boot. What AWS Lambda and
Fargate run on. If you are building a platform
that executes other people's code, this is the
category you want, not containers.
WASM a genuinely different model: sandboxed by
design, microsecond starts, tiny artifacts.
Real today for edge functions and plugins;
not yet a general replacement — the system
interface (WASI) is still filling in.
SYSTEMD units, resource limits via the same cgroups
Docker uses, socket activation, restart policies,
journald. On one machine this covers more of
Docker's runtime job than people expect.
NIX / GUIX reproducible builds and atomic rollbacks without
images. Solves "works on my machine" at a
deeper level.
A PaaS Fly.io, Render, Railway, Heroku, Cloud Run.
Some take a repository and never mention images.
For a small team this is very often the right
answer and it is not close.
deploying to Kubernetes / ECS / Cloud Run? → Docker images. Settled.
a local dev environment with a database? → Compose. Settled.
CI needing real dependencies? → containers. Settled.
polyglot, several services? → containers.
a single Go/Rust binary on one server? → maybe just the binary.
a desktop app? → Flatpak/Snap/AppImage.
running untrusted user code? → Firecracker/gVisor/Kata,
not plain containers.
a small team wanting to stop thinking about
servers entirely? → a PaaS.
need bit-for-bit reproducibility? → Nix, then export
an image from it.
shipping Go and tired of Dockerfiles? → ko.
uneasy about a root daemon? → Podman or rootless.

Worth stating, because it explains why the answer is so often “yes” despite the costs above.

Before containers, the unit of deployment was a machine configured to run an application. Configuration drifted, machines became unique, and “rebuild that server” was a project. The industry’s phrase was pets.

After containers, the unit is an immutable artifact that runs anywhere. You do not fix a broken container; you replace it. That single change is what made everything downstream possible:

immutable artifact
→ identical dev/staging/prod
→ deploys are "replace", not "modify"
→ rollback is "run the previous image"
→ a machine can be rebuilt from nothing in minutes
→ a scheduler can move workloads freely
→ autoscaling becomes mechanical
→ the reconciliation model of Kubernetes becomes possible

The Kubernetes reconciliation loop assumes it can destroy and recreate your application at will. That assumption is only reasonable because containers made applications cheap to start and identical every time.

So the fair summary is: Docker’s lasting contribution was not the runtime. It was making immutable, portable application artifacts normal. The runtime has already been substantially replaced — Kubernetes does not use dockerd, and plenty of teams build with Kaniko or ko and never run the daemon at all. The image format won completely.

Docker and the other tools in this collection

Section titled “Docker and the other tools in this collection”

Every database in this collection can run in a container, and the considerations are the same each time — chapter 9’s position, applied:

PostgreSQL / MySQL fine in a container with a real volume and
explicit memory settings. shared_buffers and
innodb_buffer_pool_size must be set from the
cgroup limit, not the host's RAM.
Redis excellent in a container — it is a single
process with a small footprint. Set
maxmemory explicitly; the same /proc/meminfo
problem applies.
Kafka workable, but the storage question is sharp:
partitions are large and node identity
matters. KRaft made this considerably easier.
Elasticsearch needs vm.max_map_count on the HOST — a
kernel setting a container cannot change.
The most common first-run failure.
Cassandra / MongoDB the same identity-plus-large-disk shape as
Kafka. Fine with care, and an operator if
you are on Kubernetes.
ClickHouse large local disks and heavy merges; the
container adds nothing and takes nothing away.
Nginx ideal. Stateless, tiny, config from a mount.
The canonical container workload.

That last row is the pattern: the more stateless a thing is, the more containers help. Nginx in a container is strictly better than nginx on a host. A 2 TB Cassandra node in a container is neutral at best. For the reasoning on each, the relevant chapters are Elasticsearch tuning, Kafka tuning, PostgreSQL tuning, and Redis memory — every one of them has a section on the setting you must pass explicitly because the process cannot see its own limit.

  • Clearly right: local development environments, CI, anything destined for an orchestrator, polyglot systems, reproducing exact environments, distributing software with heavy dependencies.
  • A tax: a single static binary on one server, desktop applications, very small single-machine setups, latency-critical or hardware-adjacent work, and stateful systems where the data question is unanswered.
  • Podman removes the root daemon; containerd + nerdctl removes the extra surface; OrbStack/Colima replace Docker Desktop on macOS and are faster.
  • You do not need Docker to build Docker images. Kaniko builds unprivileged in Kubernetes, ko and jib skip Dockerfiles entirely for Go and Java, Buildpacks give you good defaults and automatic base patching, and Nix gives real reproducibility at a real learning cost.
  • Containers are not a boundary for untrusted code. That is Firecracker, gVisor or Kata. WASM is a genuinely different model that is real for edge and plugins and not yet general.
  • A PaaS is the right answer more often than engineers like to admit for small teams.
  • The more stateless a workload, the more containers help. Nginx in a container is strictly better; a 2 TB database node is neutral at best.
  • Docker’s lasting contribution is the immutable, portable artifact — not the runtime, which has already been widely replaced. Everything Kubernetes does downstream depends on that one change.

Next: the cheat sheet — commands, flags, Dockerfile patterns and diagnostics, on one page.

Written by Sepuri Sai Krishna

© 2026CC BY 4.0Source