Non-Root Containers
Run Node.js as a non-root user so a container escape or RCE does not grant host-level privileges.
Search across all documentation pages
Run Node.js as a non-root user so a container escape or RCE does not grant host-level privileges.
Quick-reference recipe card - copy-paste ready.
FROM node:24-bookworm-slim
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY dist ./dist
RUN chown -R node:node /app
USER node
ENV NODE_ENV=production
CMD ["node", "dist/main.js"]When to reach for this: Every production Node image. CIS Docker benchmarks and most K8s policies require non-root UIDs.
FROM node:24-bookworm-slim AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY tsconfig.json src ./
RUN npm run build
FROM node:24-bookworm-slim AS prod
WORKDIR /app
# Install as root, then hand off ownership
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY --from=build /app/dist ./dist
# Writable dirs the app needs at runtime
RUN mkdir -p /app/tmp /app/logs \
&& chown -R node:node /app
USER node
EXPOSE 3000
CMD ["node", "dist/main.js"]// src/main.ts - bind non-privileged port
import express from "express";
const app = express();
const port = Number(process.env.PORT ?? 3000);
app.get("/health", (_req, res) => res.json({ status: "ok" }));
app.listen(port, "0.0.0.0");What this demonstrates:
node image includes a node user (UID 1000)chown before USER so the app can read node_modules and write logsUSER node drops to UID 1000 for the CMD process and childrensecurityContext.runAsNonRoot: true rejects images that start as UID 0apiVersion: apps/v1
kind: Deployment
spec:
template:
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 1000
containers:
- name: api
image: my-api:1.2.3
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
volumeMounts:
- name: tmp
mountPath: /app/tmp
volumes:
- name: tmp
emptyDir: {}readOnlyRootFilesystem requires writable mounts for temp files and Unix sockets.
Some platforms require arbitrary UIDs. Build with group-writable dirs:
RUN chgrp -R 0 /app && chmod -R g=u /app
USER 1001OpenShift runs as a random UID in the root group; g=u grants write access.
| Path | Permission need |
|---|---|
node_modules/ | read + execute |
dist/ | read + execute |
/app/tmp | write (uploads, pid files) |
/app/logs | write if file logging (prefer stdout) |
USER node before COPY - files owned by root, app cannot write. Fix: chown after all copies.node in build - cache permission errors. Fix: install as root in build stage, chown, then USER node.fsGroup in K8s or initContainer chown./app - read-only root FS breaks. Fix: mount emptyDir at /app/data.node user exists on distroless - use nonroot user instead.| Alternative | Use When | Don't Use When |
|---|---|---|
USER node (UID 1000) | Standard K8s and ECS | OpenShift arbitrary UID required |
| Custom app user | Multi-tenant image hardening | Unnecessary for most internal APIs |
Distroless nonroot | Minimal image + non-root | You need shell debugging in container |
| Root + dropped capabilities | Legacy images migrating | New images (use non-root from day one) |
Production containers should not run npm install at runtime. Install in build stage as root, run app as node.
Document 1000 for official Node images. If using custom UIDs, document in the Helm chart and Dockerfile comments.
Yes. Primary and workers all run as node. Use readOnlyRootFilesystem with a tmp volume for IPC if needed.
docker run --rm my-api id
# uid=1000(node) gid=1000(node)Not strictly, but AWS security best practices and customer audits expect it. Set user in task definition to 1000.
Use an initContainer in K8s for migrations (prisma migrate) with elevated perms; app container stays non-root.
nonroot userStack versions: This page was written for Node.js 24.18.0 (Active LTS), npm 10+, TypeScript 5.6+, Express 5, Fastify 5, and NestJS 11.
Reviewed by Chris St. John·Last updated Jul 19, 2026