# Kubernetes and containers

> Kubernetes graceful shutdown for Node.js: how pods stop, preStop and terminationGracePeriodSeconds, PID 1 in Docker, and a matching shutdown-check config.

Source: https://shutdown.jscrate.dev/docs/guides/kubernetes
Last updated: 2026-09-23

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

```yaml title="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 behavior       | shutdown-check setting                                |
| ------------------------------------ | ----------------------------------------------------- |
| Container `command`                  | `command`                                             |
| `readinessProbe.httpGet.path`        | `readiness.path`                                      |
| Pod grace period                     | Upper bound for `shutdown.deadlineMs`                 |
| `preStop` duration                   | Time to subtract from the server's available deadline |
| Requests arriving during termination | `shutdown.newRequests`                                |
| Slowest valid application request    | `workload`                                            |

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

```json title="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:

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

Avoid shell form:

```dockerfile
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:

```dockerfile
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:

| Symptom                                     | Code                       |
| ------------------------------------------- | -------------------------- |
| Wrapper exits while child server keeps port | [SC302](https://shutdown.jscrate.dev/docs/codes/sc302) |
| Wrapper is killed by the signal             | [SC301](https://shutdown.jscrate.dev/docs/codes/sc301) |
| Process remains alive through the deadline  | [SC300](https://shutdown.jscrate.dev/docs/codes/sc300) |
| Active request loses its connection         | [SC201](https://shutdown.jscrate.dev/docs/codes/sc201) |

## 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:

```text
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.

## Related

- [Readiness and draining](https://shutdown.jscrate.dev/docs/guides/readiness-and-draining)
- [Graceful shutdown in Node.js](https://shutdown.jscrate.dev/docs/guides/graceful-shutdown-nodejs)
- [Run shutdown-check in CI](https://shutdown.jscrate.dev/docs/ci)
- [Compatibility and limits](https://shutdown.jscrate.dev/docs/compatibility)
- [SC302: Port still open](https://shutdown.jscrate.dev/docs/codes/sc302)
