Links#
https://hub.docker.com/_/node
https://docs.docker.com/build/building/best-practices/
https://docs.docker.com/build/building/secrets/
https://docs.npmjs.com/cli/v11/commands/npm-ci
https://nodejs.org/api/cli.html
1. Important Points#
Node.js container rule:
use npm ci
do not copy .env into image
run as non-root user
write logs to stdout/stderr
pass runtime config by env / secret / mounted file
common runtime env:
NODE_ENV=production
PORT=3000
LOG_LEVEL=info
NODE_OPTIONS=--enable-source-maps --max-old-space-size=512
2. Multi-stage Dockerfile#
# syntax=docker/dockerfile:1
FROM node:22-bookworm-slim AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
FROM node:22-bookworm-slim AS runtime
WORKDIR /app
ENV NODE_ENV=production \
PORT=3000 \
LOG_LEVEL=info \
NODE_OPTIONS="--enable-source-maps --max-old-space-size=512"
COPY --from=deps /app/node_modules ./node_modules
COPY package.json package-lock.json ./
COPY src ./src
RUN npm prune --omit=dev && npm cache clean --force
USER node
EXPOSE 3000
CMD ["node", "src/main.js"]
why:
deps stage uses npm ci
runtime stage keeps only production dependencies
image runs as node user
3. Runtime-only Dockerfile#
# syntax=docker/dockerfile:1
FROM node:22-bookworm-slim
WORKDIR /app
ENV NODE_ENV=production \
PORT=3000 \
LOG_LEVEL=info \
NODE_OPTIONS="--enable-source-maps --max-old-space-size=512"
COPY package.json package-lock.json ./
RUN npm ci --omit=dev && npm cache clean --force
COPY src ./src
USER node
EXPOSE 3000
CMD ["node", "src/main.js"]
use this when:
project is plain JavaScript
no compile step is required
CI already ran lint/test
4. Runtime Environment Variables#
docker run --rm \
-p 3000:3000 \
-e NODE_ENV=production \
-e PORT=3000 \
-e LOG_LEVEL=info \
-e NODE_OPTIONS="--enable-source-maps --max-old-space-size=768" \
order-api:runtime
common Node.js runtime env#
| Env |
Example |
What It Does |
NODE_ENV |
production |
application/runtime mode |
PORT |
3000 |
app listen port |
LOG_LEVEL |
info |
application log level |
NODE_OPTIONS |
--max-old-space-size=768 |
pass Node.js CLI flags |
NODE_EXTRA_CA_CERTS |
/etc/ssl/private/company-ca.pem |
add extra trusted CA |
NODE_TLS_REJECT_UNAUTHORIZED |
1 |
TLS certificate verification |
HTTP_PROXY |
http://proxy:8080 |
outbound HTTP proxy |
HTTPS_PROXY |
http://proxy:8080 |
outbound HTTPS proxy |
NO_PROXY |
localhost,127.0.0.1,.svc |
bypass proxy |
TZ |
UTC |
timezone |
common NODE_OPTIONS#
memory:
--max-old-space-size=768
set V8 heap max size in MB
diagnostics:
--enable-source-maps
better stack traces with source maps
--trace-warnings
print stack traces for process warnings
--heapsnapshot-near-heap-limit=3
write heap snapshots near heap limit
--report-uncaught-exception
generate diagnostic report on uncaught exception
--report-on-fatalerror
generate diagnostic report on fatal error
security / crypto:
--tls-min-v1.2
reject TLS versions below 1.2
--tls-cipher-list=<cipher-list>
customize TLS cipher list
--use-openssl-ca
use OpenSSL CA store
--use-bundled-ca
use Node.js bundled CA store
5. .dockerignore#
node_modules
coverage
.git
.env
.env.*
npm-debug.log
Dockerfile
docker-compose.yml
6. Build And Run#
docker build -t order-api:local .
docker run --rm \
-p 3000:3000 \
-e NODE_ENV=production \
-e PORT=3000 \
order-api:local
7. Docker Compose#
services:
order-api:
build:
context: .
ports:
- "3000:3000"
env_file:
- .env.local
environment:
NODE_ENV: local
PORT: "3000"
LOG_LEVEL: debug
NODE_OPTIONS: "--enable-source-maps --max-old-space-size=512"
8. Build Secrets#
docker build \
--secret id=npmrc,src="$HOME/.npmrc" \
-t order-api:local .
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm ci
9. Production Checklist#
image:
npm ci
production dependencies only
no .env copied
non-root user
pinned Node major version
runtime:
config injected by platform
NODE_OPTIONS reviewed
health check configured
logs to stdout/stderr
CI:
npm ci
npm run lint
npm test
docker build
image scan