17.2.1.1 Process Responsibility Clarity
A focused guide to Process Responsibility Clarity, connecting core concepts with practical Docker and container operations.
Process responsibility clarity addresses the situations where more than one process genuinely, legitimately exists within a single container, an init process and an application, or an application and a tightly coupled helper, ensuring each process's specific role, signal handling, exit code propagation, logging, and health reporting, is explicitly defined and non-overlapping, rather than left ambiguous about which process is actually responsible for what.
Why ambiguity here causes real problems
When a container legitimately contains more than one process, an unclear division of responsibility produces specific, recognizable failure modes: a signal that should reach the application instead only reaches an intermediate wrapper process, an exit code that should reflect the application's actual success or failure instead reflects only the wrapper script's own, unrelated final command, or health check logic that checks the wrong process entirely:
CMD ["sh", "-c", "node server.js"]
exit code: 0
If the shell wrapping node server.js exits successfully regardless of whether the actual Node.js process underneath crashed or exited with an error, the exit code visible to anything monitoring the container reflects the wrapper's behavior, not the application's, which is exactly the kind of responsibility ambiguity that produces a misleading signal at the moment it matters most.
Init process: signal forwarding and zombie reaping only
When a dedicated init process like tini runs as PID 1, its responsibility should be scoped narrowly and explicitly: forwarding signals correctly to the actual application and reaping orphaned zombie processes, nothing more:
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["node", "server.js"]
The init process here has no role in the application's own logic, health, or exit code interpretation at all; it is purely a thin, transparent layer that ensures signals reach the actual application correctly, and the application's own exit code passes through unmodified as the container's overall exit code.
Verifying exit code pass-through explicitly
Confirming that an init process or wrapper script genuinely passes through the underlying application's exit code, rather than masking it with its own, unrelated result, is worth testing directly rather than assuming based on the wrapper's apparent design:
docker run my-api sh -c "node -e 'process.exit(1)'"
docker inspect my-api --format '{{.State.ExitCode}}'
A result other than 1 here indicates something in the process chain is not correctly propagating the actual application's exit status, which needs to be identified and corrected before relying on the container's reported exit code for any monitoring or restart-policy decision.
Sidecar processes: clearly scoped, non-overlapping responsibilities
When a legitimate, tightly coupled helper process runs alongside the main application within the same container, each process's specific responsibility should be documented and non-overlapping, avoiding a situation where it is unclear which process is actually responsible for a given piece of behavior:
CMD ["sh", "-c", "fluent-bit -c /etc/fluent-bit.conf & exec node server.js"]
Here, fluent-bit's responsibility is explicitly log shipping only, and the main application's responsibility is explicitly serving requests only; using exec for the main application specifically ensures it, not the wrapping shell, becomes the actual PID 1-equivalent process whose exit determines the container's own lifecycle, while the background fluent-bit process is understood explicitly as a secondary, non-critical-path helper.
Health checks should target the process actually responsible for health
When more than one process exists within a container, a health check should explicitly target whichever process actually determines the container's real readiness to serve its function, not an unrelated or secondary process whose status happens to be easier to check:
HEALTHCHECK CMD curl -f http://localhost:3000/healthz || exit 1
This health check targets the main application's own HTTP endpoint specifically, which is the correct, intentional choice; a health check that instead, by oversight, only confirmed the secondary logging helper was running would produce a misleading "healthy" status even if the actual application had crashed entirely.
Logging responsibility and attribution
When multiple processes produce log output within the same container, ensuring each line is attributable to its actual source process, through a prefix, a structured field, or separate output streams where feasible, prevents the kind of log attribution confusion that makes troubleshooting considerably harder once more than one process's output is interleaved together:
console.log(JSON.stringify({ process: 'api', level: 'info', message: 'Request handled' }));
fluent-bit --log-level info --tag fluent-bit.local 2>&1
Explicitly tagging or prefixing each process's own output, rather than leaving all of it to interleave indistinguishably, keeps responsibility for any specific log line clear even once multiple processes' output is combined into the same captured stream.
Documenting the division of responsibility explicitly
For any container legitimately running more than one process, documenting which process is responsible for what, signal handling, health, logging, exit code, directly in the Dockerfile through comments, or in accompanying documentation, makes this division of responsibility explicit and maintainable rather than something that has to be reverse-engineered from the configuration each time it needs to be understood:
# fluent-bit: log shipping only, non-critical, runs in background
# node server.js: main application process, owns container exit code and health status
CMD ["sh", "-c", "fluent-bit -c /etc/fluent-bit.conf & exec node server.js"]
Common mistakes
- Allowing a wrapper script or shell to mask the actual application's exit code, producing a misleading container-level exit status that does not reflect the application's true success or failure.
- Configuring a health check against the wrong process within a multi-process container, producing a misleading health status that does not reflect the actual application's real condition.
- Letting multiple processes' log output interleave with no attribution, making it considerably harder to determine which process actually produced a given line during an investigation.
- Not using
execfor the main application process within a shell wrapper, leaving the shell itself as the effective PID 1 rather than the actual application. - Leaving the division of responsibility between multiple legitimate processes within a container undocumented, requiring it to be reverse-engineered from configuration each time it needs to be understood.
Process responsibility clarity ensures that whenever more than one process legitimately exists within a single container, each one's specific role, signal handling, exit code ownership, health representation, logging attribution, is explicit, tested, and documented, rather than left as an ambiguous, easily misunderstood configuration that produces misleading signals exactly when accurate ones matter most.