InfrastructureAll levels

Docker

Docker packages an application with everything it needs to run into one image, and runs that image as a container. A container is not a small virtual machine. It is an ordinary process on the host kernel, given a restricted view of the filesystem, the network and the process table, and a limit on what it may consume. The image it starts from is a stack of read-only layers. Those two facts explain almost everything: why containers start in milliseconds, why the order of your Dockerfile decides your build time, why the state inside one disappears, and what isolation actually protects you from.

A container is
A process, not a machine
An image is
Stacked read-only layers
State inside
Goes when it goes

A process with a restricted view

When you start a container, the kernel starts a process — the same kind of process as any other on the machine. What makes it a container is that the kernel gives it its own namespaces: its own view of the filesystem, its own network stack, its own process numbering, so the program inside sees itself as process 1 on a machine containing only what the image provided. Alongside that, control groups cap how much CPU, memory and I/O it may take. There is no second operating system anywhere in this picture, which is precisely why a container starts in milliseconds and a virtual machine takes half a minute.

A virtual machine

  • Runs its own kernel, so it can run a different operating system entirely.
  • Boots in tens of seconds and costs gigabytes before your code is in it.
  • A kernel bug inside it stays inside it, which is a real security boundary.
  • Right when you need strong isolation between tenants you do not control.

A container

  • Shares the host kernel, so a Linux container needs a Linux kernel underneath.
  • Starts in milliseconds and adds only what your application actually needs.
  • Isolation is a kernel feature, not a wall — a kernel escape reaches the host.
  • Right for packaging and running your own services many times over.

Three consequences follow immediately and they surprise people in production rather than in development. The container matches the host architecture unless you build for another one, so an image built on an ARM laptop can fail to start on an x86 server. The kernel is shared, so a container cannot use a kernel module the host does not have, and a kernel vulnerability is a host-level problem regardless of how many containers are running. And isolation is strong enough to separate your own services from each other, but running untrusted code from strangers on a shared kernel is a different risk class — that is what a virtual machine or a sandboxed runtime is for.

Layers, and why the order matters

An image is built one instruction at a time, and each instruction that changes the filesystem produces a layer: a read-only diff on top of everything before it. When you rebuild, the daemon reuses a cached layer only if that instruction and every instruction above it are unchanged. Everything below a changed layer is thrown away and rebuilt — which turns the order of your Dockerfile into a performance decision rather than a stylistic one.

An image is built one instruction at a time, and each instruction that changes the filesystem produces a read-only layer. On a rebuild, a cached layer is reused only if that instruction and everything above it are unchanged — so everything below a changed layer is thrown away and rebuilt. That turns the order of a Dockerfile into a performance decision: copying the dependency manifest and installing dependencies before copying the source means an ordinary commit reuses the expensive layer, while a single COPY of the whole directory near the top rebuilds everything on every commit. Layers also only add, so a file copied in and deleted later is still readable in the layer that added it. The container itself adds one thin writable layer, which is deleted with the container — state belongs in a volume or a database.

That gives one rule with an outsized payoff: copy the dependency manifest and install dependencies before copying the source. Your source changes on every commit and your dependency list changes once a month, so putting the install above the source copy means an ordinary build reuses the expensive layer instead of reinstalling everything. Copying the whole directory first — a single COPY . . near the top — is the most common reason a build that should take twenty seconds takes four minutes, and it is one line to fix.

Multi-stage builds solve the other half of the size problem. Compilers, build tools and development dependencies are needed to produce the artifact and are pure liability in the thing you ship — every one of them is code an attacker can use and a vulnerability scanner will report. Build in one stage, copy only the resulting binary or bundle into a small final stage, and the image drops from hundreds of megabytes to tens. Choose the base image the same way: a slim or distroless base is smaller and has less to patch, at the cost of not having a shell when you want to debug inside it.

Running one well

A container runs one process and lives exactly as long as it does. That is the contract, and most of the operational advice follows from it directly. Run one thing per container, because when the container is the unit of restarting, scaling and logging, two programs inside one make all three ambiguous. Write logs to standard output rather than to a file, because the platform collects the stream and nobody will retrieve a file from a container that no longer exists. Take configuration from environment variables, so the same image runs in every environment. And put anything that must survive on a mounted volume or in a database, because the writable layer is deleted with the container.

Two details about that single process cause real incidents. It runs as PID 1, and PID 1 has no default signal handlers — so a program that does not explicitly handle SIGTERM will not shut down gracefully, it will be killed after the grace period with its in-flight requests. Handle the signal, or use an init process that forwards it. And by default the process runs as root inside the container, which is unnecessary for almost every application and is one kernel bug away from being root outside it. Create a user in the image and run as it.

Set memory and CPU limits explicitly, and know what each one does when it is hit, because they behave completely differently. Exceeding the memory limit gets the process killed by the kernel — abruptly, with no exception your code can catch, appearing in your logs as a container that simply vanished. Exceeding the CPU limit does not kill anything; the process is throttled, which looks like unexplained latency rather than a crash. Both need a runtime that reads the container limit rather than the host's total, which several language runtimes only learned to do in relatively recent versions.

How this shows up in real delivery

A tag is a mutable pointer, not an identity. Two machines that both pulled app:latest a week apart may be running different images, and so may the same machine after a restart — which is how an environment drifts without any deploy having happened. A digest is the content hash and it is the only thing that names an exact image. Deploy by digest, or by an immutable tag your pipeline never reuses, and pin base images the same way so a rebuild is reproducible.

Scanning belongs in the pipeline, and the trick is making the result actionable rather than a wall of findings. A slim base image reduces the report to something a person can read, most findings live in the base rather than in your code, and rebuilding on a fresh base fixes more of them than any individual patch. Fail the build on new critical findings, record the rest, and rebuild regularly — an image that has not been rebuilt in six months is running six months of unpatched libraries no matter how clean it was on the day it shipped.

The claim containers actually make is narrower than the slogan suggests, and knowing the boundary saves an argument. They guarantee that the same filesystem and the same dependencies are present everywhere, which removes an entire category of environment bugs. They do not guarantee the same kernel, the same architecture, the same clock, the same available memory or the same network. "Works on my machine" becomes "the same bytes are on both machines" — a real improvement, and not the same sentence.

Where it degrades

  • Copying the whole source tree before installing dependencies, so every commit reinstalls everything.
  • A secret copied in and deleted later, which stays readable in the layer that added it.
  • Shipping the build toolchain in the final image, which is size and attack surface for nothing.
  • Deploying by a mutable tag, so two machines run different code and nobody deployed anything.
  • Running as root because it was the default, on an image that never needed any privilege.
  • Ignoring SIGTERM, so every deploy kills in-flight requests after the grace period expires.
  • State written to the container filesystem, which disappears with the next restart.
  • Several processes in one container, which makes restarting, scaling and logging ambiguous.
  • No memory limit, so one leaking service takes the whole host down with it.

When to use it

Use it when

  • Packaging a service so the same artifact runs on a laptop, in CI and in production.
  • Local dependencies — a database, a broker, a mock — that would otherwise be installed by hand and drift.
  • Build environments in a pipeline, where a pinned image is a reproducible toolchain.
  • Anything heading for an orchestrator later, since the container is the unit it schedules.

Avoid it when

  • Untrusted third-party code on a shared kernel, where a virtual machine or a sandbox is the right boundary.
  • A stateful database on a single host with no volume strategy, where the first restart is the incident.
  • Desktop or GUI software, where the packaging problem it solves is not the problem you have.
  • A single small application on one server that a package and a service unit already run reliably.

Found this useful?

Share it with someone who is working on the same problem.