Shutdown Check

Search documentation

Find a page or section

Kubernetes and containers

Match the Node.js drain to pod termination, container signals, and the grace period.

Kubernetes graceful shutdown for Node.js requires the server to stop receiving traffic, finish active requests, and exit before the pod's grace period ends. shutdown-check tests the server-side part locally or in CI; it does not emulate the Kubernetes control plane.

What happens when a pod stops?

During deletion, a rolling update, scale-down, or node drain:

  1. Kubernetes marks the pod as terminating.
  2. Endpoint removal begins so Services and load balancers can stop routing to the pod.
  3. The kubelet runs a configured preStop hook.
  4. The container runtime sends SIGTERM to the container's main process.
  5. Kubernetes waits for the container to exit.
  6. At the end of terminationGracePeriodSeconds, remaining processes receive SIGKILL.

Endpoint updates and container termination begin around the same time. Traffic can still arrive after the signal because kube-proxy, ingress controllers, and external load balancers need time to observe the change.

What must the service do?

The server should:

  • change readiness immediately;
  • reject new work that still arrives;
  • preserve requests that began before the drain;
  • close shared resources after active handlers finish;
  • exit with code 0 before the grace period ends.

Closing the listener immediately may be enough when every client retries and traffic routing updates quickly. A safer zero downtime deploy often uses a short drain window that returns 503 before closing the listener.

Configure readiness and preStop

deployment.yaml
spec:
  template:
    spec:
      terminationGracePeriodSeconds: 30
      containers:
        - name: api
          image: example/api:1.4.2
          command: ["node", "dist/server.js"]
          ports:
            - containerPort: 3000
          readinessProbe:
            httpGet:
              path: /health
              port: 3000
            periodSeconds: 2
            failureThreshold: 1
          lifecycle:
            preStop:
              exec:
                command: ["sleep", "5"]

The preStop delay gives endpoint updates time to propagate before SIGTERM. It uses part of the 30-second grace period; it does not add extra time.

The exec form requires a sleep binary in the image. Some Kubernetes versions offer a built-in sleep action. Confirm support in the cluster before depending on it.

An application-level drain can replace or supplement preStop. The readiness route returns 503 as soon as SIGTERM arrives, and the server keeps listening briefly before calling server.close().

Match Kubernetes to shutdown-check

Kubernetes setting or behaviorshutdown-check setting
Container commandcommand
readinessProbe.httpGet.pathreadiness.path
Pod grace periodUpper bound for shutdown.deadlineMs
preStop durationTime to subtract from the server's available deadline
Requests arriving during terminationshutdown.newRequests
Slowest valid application requestworkload

For a 30-second grace period with a 5-second preStop, a 20-second test deadline leaves about 5 seconds of safety margin:

shutdown-check.json
{
  "command": ["node", "dist/server.js"],
  "env": { "NODE_ENV": "production", "PORT": "3000" },
  "baseUrl": "http://127.0.0.1:3000",
  "readiness": { "path": "/health" },
  "workload": {
    "path": "/test/slow",
    "bodyIncludes": "work complete",
    "concurrent": 3,
    "started": { "type": "response-headers" }
  },
  "shutdown": {
    "deadlineMs": 20000,
    "readinessWithdrawal": true,
    "newRequests": {
      "path": "/test/new-work",
      "rejectStatuses": [503]
    },
    "repeatSignalAfterMs": 1000
  }
}

shutdown-check begins at SIGTERM, so it does not run or time the preStop hook. The deadline should describe the time available to the application after the hook.

Why does PID 1 matter?

The container command becomes PID 1. Signal and child-process behavior around PID 1 is different from an ordinary process, so choose the command carefully.

Use exec form

Preferred Dockerfile command:

CMD ["node", "dist/server.js"]

Avoid shell form:

CMD node dist/server.js

Shell form starts /bin/sh -c as PID 1. The shell may not forward SIGTERM to Node.js.

Avoid unnecessary package-manager wrappers

This adds another process between Kubernetes and the server:

CMD ["npm", "start"]

Starting node directly makes signal ownership clear. If a wrapper is required, it must forward signals and wait for the child.

Consider a minimal init process

An init such as tini can forward signals and reap orphaned child processes. It is useful when the application spawns children. Configure it intentionally and test the exact container command used in production.

Diagnose wrapper problems

Use the production command in shutdown-check. The result often reveals the failure before you build an image:

SymptomCode
Wrapper exits while child server keeps portSC302
Wrapper is killed by the signalSC301
Process remains alive through the deadlineSC300
Active request loses its connectionSC201

Run shutdown-check inside a container

You can run the tool inside a Linux image when it contains Node.js 22 or later and the package. shutdown-check starts the configured service as a child and connects to it on loopback.

This verifies:

  • the built image can start the service;
  • its runtime files and environment are present;
  • the Node.js process handles SIGTERM;
  • active local HTTP requests drain;
  • the process and port close.

It does not verify:

  • Kubernetes endpoint removal;
  • an actual preStop hook;
  • ingress or external load-balancer timing;
  • sidecar termination order;
  • cluster-specific grace-period behavior.

Use a staging cluster for those integration checks.

Handle sidecars and service meshes

A sidecar proxy can continue or stop forwarding traffic independently of the Node.js process. shutdown-check talks directly to the local service and does not model sidecar shutdown order.

When using a mesh, confirm:

  • readiness reflects both proxy and application state;
  • the proxy keeps forwarding existing connections during the app drain;
  • sidecar termination does not shorten the application deadline;
  • retries do not hide repeated failures.

Set a realistic grace period

Budget the full termination window:

preStop + routing delay + slowest active request + resource cleanup + margin

If the total exceeds terminationGracePeriodSeconds, Kubernetes will send SIGKILL before the service finishes. Raising the grace period can be valid, but also fix handlers, queries, or cleanup operations that can hang forever.