Building Node.js Docker Images with Amazon Linux
Running containerised applications often means dealing with vulnerability scanners. If you need to keep systems clear of known CVEs, the base image you choose matters.
Amazon Linux images tend to have fewer reported vulnerabilities than many alternatives. For Java apps, Amazon provides Corretto images that work well out of the box. For Node.js, though, there is no official Amazon Linux image. You have to install Node.js yourself.
Here is a multi-stage Dockerfile that installs Node.js 22 on Amazon Linux 2023:
FROM amazonlinux:2023 AS builder
ENV NODE_OPTIONS="--max-old-space-size=8192"
RUN dnf update --security -y && dnf clean all
RUN dnf install -y shadow-utils
RUN groupadd -r node && useradd -r -g node -s /bin/bash node
RUN curl -fsSL https://rpm.nodesource.com/setup_22.x | bash -
RUN dnf install -y nodejs
WORKDIR /usr/src/app
USER node
COPY --chown=node:node package*.json ./
RUN npm ci
COPY --chown=node:node . .
RUN npm run test
RUN npm run build
FROM amazonlinux:2023
ENV NODE_ENV=production
RUN dnf update --security -y && dnf clean all
RUN dnf install -y shadow-utils
RUN groupadd -r node && useradd -r -g node -s /bin/bash node
RUN curl -fsSL https://rpm.nodesource.com/setup_22.x | bash -
RUN dnf install -y nodejs && dnf clean all
WORKDIR /usr/src/app
USER node
COPY --chown=node:node package*.json ./
RUN npm ci --omit=dev
COPY --chown=node:node --from=builder /usr/src/app/build ./build
EXPOSE 3000
CMD ["node", "build/index.js"]
A few things worth noting:
Base image
FROM amazonlinux:2023 pulls from Docker Hub. You can also use public.ecr.aws/amazonlinux/amazonlinux:2023 to pull directly from Amazon’s registry. Pinning to specific versions like amazonlinux:2023.6.20250107.0 is possible but creates extra work when you need to update.
Multi-stage build
The Dockerfile uses two stages. The builder stage installs all dependencies (including devDependencies), runs tests, and builds the application. The final stage starts with a clean Amazon Linux image, installs only production dependencies with npm ci --omit=dev, and copies just the built artifacts from the builder. This keeps the final image smaller and removes build tools, compilers, and test frameworks that are not needed at runtime.
Production environment
Setting NODE_ENV=production in the final stage tells Node.js to run in production mode. This makes many libraries optimise their behaviour (Express disables verbose errors, React serves optimised builds). Combined with npm ci --omit=dev, it ensures devDependencies are excluded from the runtime image.
Memory settings
The NODE_OPTIONS environment variable in the builder stage increases the V8 heap size. This helps if you are bundling TypeScript or running memory-intensive build steps. See this Stack Overflow discussion for more detail. Make sure this aligns with any container memory limits you set. The final runtime stage does not need this unless your application itself requires extra heap space.
Security updates
dnf update --security -y applies security patches released after the base image was published. Amazon publishes updated images regularly, but sometimes there is a gap between a CVE fix being available and a new image appearing.
User permissions
Running as root is risky. Creating a dedicated node user and switching to it reduces the damage an attacker could do if they compromise the application.
Node.js installation
The NodeSource repository provides recent Node.js versions for Amazon Linux. The two-step process adds the repository, then installs Node.js through dnf.
npm ci vs npm install
npm ci gives you reproducible builds by installing exactly what is in your lockfile. See the npm documentation for the full comparison.
Optional configurations
If you work behind a corporate proxy or need custom certificates, you might need extra steps:
COPY cert.cer /etc/pki/ca-trust/source/anchors
RUN update-ca-trust
For private npm registries:
RUN npm config set @your-scope:registry https://your-registry.example.com
Kubernetes deployment
In Kubernetes, you often define the start command in your deployment manifest rather than the Dockerfile:
apiVersion: apps/v1
kind: Deployment
metadata:
name: node-app
spec:
replicas: 2
selector:
matchLabels:
app: node-app
template:
metadata:
labels:
app: node-app
spec:
containers:
- name: node-container
image: my-node-image:1.0.0
command: ["node"]
args: ["build/index.js"]
ports:
- containerPort: 3000
This approach means more setup than using an official Node.js image. But if you need to minimise OS-level vulnerabilities, it can save time dealing with security compliance. Whether that tradeoff works for you depends on your environment and requirements.