LPI DevOps Tools Engineer study materials
LPI DevOps Tools Engineer study resources

Node.js Image Size, Signal Handling, and Aggressive Probes

LPI DevOps Tools Engineer · Put your knowledge to work in context.

All case studies

Node.js Image Size, Signal Handling, and Aggressive Probes

An internal Node.js 18 service is packaged as a single-stage image based on node:18. The image is 1.24 GB, runs as root, and includes devDependencies, test fixtures, and a full compiler toolchain. In production the pod receives SIGTERM during rolling updates but takes the full 30-second termination grace period because the process does not handle the signal; the entrypoint is npm start, so npm is PID 1 and does not forward signals to node reliably. A HEALTHCHECK in the Dockerfile curls /healthz, but Kubernetes also has a liveness probe pointed at the same endpoint with a 1-second timeout and a 3-second period. Under load, /healthz performs a database ping and occasionally exceeds 1 second, causing restarts. The team wants smaller images, faster cold starts, and fewer restart loops.

1. Design a multi-stage Dockerfile strategy that reduces image size and attack surface, and explain which runtime artifacts must remain.

Read the worked answer

Use a builder stage based on a full Node image to install dependencies and compile native modules, then copy only the production artifacts into a minimal runtime stage. The builder stage runs npm ci, including devDependencies, and any build step. The runtime stage should copy package.json, the lockfile, and only production dependencies, for example by running npm ci --omit=dev in the runtime stage or by copying a pruned node_modules from a dedicated production-dependencies stage. Copy the built application output, not test fixtures, source maps if not needed, or the compiler toolchain.

Choose a runtime base that matches operational constraints. A slim Debian or Alpine image reduces size, but Alpine uses musl and can break native modules or produce different DNS behavior; if native dependencies or glibc are involved, a slim Debian variant is often safer. Create a non-root user and USER it in the runtime stage. Add only the runtime packages needed, such as CA certificates and curl if health checks use it. Remove the Dockerfile HEALTHCHECK in favor of Kubernetes probes, or keep one consistent mechanism.

The remaining runtime artifacts are the Node runtime, production node_modules, compiled application code, configuration files, and certificates. The expected result is an image in the low hundreds of megabytes, a smaller CVE surface because compilers and devDependencies are absent, and faster pulls and cold starts.

2. Explain why SIGTERM handling is unreliable and how you would change the entrypoint and application code to achieve graceful shutdown within the grace period.

Read the worked answer

With npm start as PID 1, npm spawns node as a child process. npm is not a proper init and does not reliably forward SIGTERM to its child, so node may never see the signal and keeps running until Kubernetes sends SIGKILL after the grace period. Even if npm forwarded the signal, shell-form entrypoints can add another process layer.

Use exec form and start node directly: ENTRYPOINT ["node", "server.js"] or CMD ["node", "server.js"]. If you need npm for scripts, use exec npm start or a small init such as tini with ENTRYPOINT ["tini", "--", "node", "server.js"]. Avoid sh -c without exec.

In the application, register handlers for SIGTERM and SIGINT. On SIGTERM, stop accepting new connections, mark readiness as failing so the endpoint is removed from Service endpoints, wait for in-flight requests up to a bounded timeout shorter than terminationGracePeriodSeconds, close database and message-broker connections, then exit with code 0. Set the grace period with margin: for example, a 20-second application shutdown budget inside a 30-second grace period. Kubernetes also sends the endpoint removal concurrently, so the readiness flip and a short pre-stop delay can reduce dropped requests. Make sure the HTTP server's keep-alive connections are closed explicitly, and test by sending SIGTERM to the container and observing logs and exit timing.

3. The liveness probe uses `/healthz`, which performs a database ping and sometimes exceeds the 1-second timeout. Recommend a probe design that avoids restart loops while still detecting real failures.

Read the worked answer

A liveness probe should answer a narrow question: is this process deadlocked or unable to make progress? It should not depend on a downstream database, because a slow database would cause Kubernetes to restart healthy application pods, amplifying the outage. The current 1-second timeout with a 3-second period is also aggressive for an endpoint that does a network round trip under load.

Split the endpoints. Use a liveness endpoint that checks only process-local state, such as whether the event loop is responsive and the HTTP server can answer, and return quickly without external calls. Use a readiness endpoint to check dependencies needed to serve traffic, such as database connectivity or cache initialization. If the database is down, readiness should fail and remove the pod from Service endpoints, but liveness should stay green so the pod is not restarted into the same dependency failure. A startup probe can protect slow-starting applications from liveness restarts.

Set realistic thresholds: timeoutSeconds of 2-3 seconds, periodSeconds of 10, and failureThreshold of 3 for liveness; readiness can be more sensitive, such as a 2-3 second period. Do not point both probes at the same expensive endpoint. Also remove the Dockerfile HEALTHCHECK if Kubernetes probes are authoritative, to avoid two competing health mechanisms with different timeouts.