17.2.1 One Process Practice
A focused guide to One Process Practice, connecting core concepts with practical Docker and container operations.
The one-process practice is the convention of designing each container around a single, well-defined concern rather than bundling multiple, independently meaningful services into one container, and understanding precisely what it actually means, and does not mean, clarifies both why it matters and where genuine, justified exceptions exist.
What "one process" actually means
The principle is not a literal restriction against multi-threaded applications or applications that spawn helper child processes as part of their normal, internal operation; a Node.js application using a worker thread pool, or a web server forking several worker processes internally, is still consistent with this practice, since all of that activity serves one single, coherent concern, handling that one application's workload:
CMD ["node", "--max-old-space-size=512", "cluster-server.js"]
The actual principle is about not combining genuinely separate, independently meaningful services, a web server and a completely unrelated database, or an API and a separate background job processor, into a single container, rather than about literally restricting a container to exactly one OS-level process under all circumstances.
Why independent scaling matters
Two genuinely separate concerns bundled into one container can never be scaled independently of each other; if a web-facing API needs three replicas to handle its load while a background worker needs only one, bundling them together forces both to scale in lockstep, wasting resources on unnecessarily duplicated worker instances or under-provisioning the API:
services:
api:
deploy:
replicas: 3
worker:
deploy:
replicas: 1
Separating these into distinct containers, as shown here, allows each to scale according to its own actual load characteristics, which is simply not possible once they are combined into a single container that must scale as one indivisible unit.
Why logging and health checking become muddled when combined
A container running two genuinely separate services produces interleaved, harder-to-attribute log output, and a single health check for the container as a whole cannot distinguish between "the web server is healthy but the worker has crashed" and "everything is fine," since the container's overall running state reflects neither service's actual condition individually:
docker logs combined-container
[web] Request received
[worker] Processing job 123
[web] Response sent
Separating these into distinct containers gives each its own clean, individually attributable log stream and its own health check accurately reflecting that specific service's actual condition, rather than an ambiguous, combined signal that obscures which specific component is actually responsible for any given symptom.
Why restart and recovery granularity suffers when combined
If one of two bundled services crashes, the typical mechanism for recovering it, restarting the container, also restarts the other, entirely unrelated and possibly still healthy service, which is unnecessary disruption that independent containers would avoid entirely:
docker restart combined-container
A crashed worker process inside a combined container generally cannot be restarted in isolation without restarting the entire container and, with it, the web server that may have been functioning perfectly well throughout.
Legitimate exceptions: genuine sidecar patterns
A process that exists specifically to support another process within the same container, rather than serving its own, independently meaningful purpose, can be a legitimate exception, though it is worth being deliberate about whether this genuinely warrants combination into one container versus being better served as a separate, sidecar container sharing a network namespace:
CMD ["sh", "-c", "fluent-bit & exec node server.js"]
services:
api:
image: my-api
log-shipper:
image: fluent-bit
network_mode: "service:api"
The second approach, a genuinely separate sidecar container sharing the main container's network namespace, generally provides cleaner separation, independent updates, and clearer logging attribution than combining the two directly within a single container's process tree, and is usually the better default even for tightly coupled, support-oriented secondary processes.
Init processes are not a violation
Running a minimal init process like tini as PID 1, specifically to correctly forward signals and reap zombie processes for the actual application running as its child, is not a violation of the one-process principle; the init process exists purely to support the single, genuine application process running underneath it, not as a second, independently meaningful service:
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["node", "server.js"]
Decomposing an existing, bundled container
For a legacy container bundling several genuinely separate concerns, identifying the actual, independent services within it and separating them into distinct containers, each with its own image, lifecycle, and scaling configuration, is generally worth the migration effort, since it directly addresses every one of the specific drawbacks, scaling, logging, health checking, restart granularity, described above:
CMD ["sh", "-c", "nginx & node api-server.js & python worker.py & wait"]
services:
proxy:
image: nginx
api:
image: my-api
worker:
image: my-worker
Migrating from the combined pattern to separate containers, each independently built, deployed, and scaled, is a structural improvement that pays off considerably more as the application and its operational demands grow over time.
Common mistakes
- Interpreting the one-process principle as a literal restriction against multi-threaded applications or internal worker processes serving the same single concern.
- Bundling genuinely separate, independently meaningful services into one container purely for convenience, then encountering scaling, logging, and restart granularity problems as a direct consequence.
- Choosing to combine a support process directly within a container's process tree when a separate, sidecar container sharing the network namespace would have provided cleaner separation.
- Treating a necessary init process like
tinias a violation of the principle, when it exists specifically to support, not compete with, the single genuine application process. - Leaving a legacy, multi-concern combined container unaddressed indefinitely rather than migrating it toward separate, independently scalable containers as the application's operational demands grow.
The one-process practice is fundamentally about keeping each container scoped to a single, independently meaningful concern, not about literally restricting it to one OS process, and the genuine benefits, independent scaling, clean logging and health signals, precise restart granularity, become increasingly valuable as an application grows beyond the simplest, smallest possible deployment.