Safeguard
DevSecOps

Is node:18-alpine Still Safe to Use in 2025?

The node:18-alpine image is small and popular, but Node.js 18 reached end of life in April 2025. Here is what that means for your containers and how to migrate cleanly.

Karan Patel
Platform Engineer
5 min read

The node:18-alpine image is no longer a safe long-term base for production containers, because Node.js 18 reached end of life on April 30, 2025 and stopped receiving security updates. Any new CVE in the Node runtime after that date will never be patched in the 18.x line, so a container built on node:18-alpine carries risk that grows quietly over time even if nothing in your own code changes.

That does not mean you have to rip it out today, but it does mean you should have a migration plan and know exactly what you are exposed to.

Why people reach for node 18 alpine in the first place

The appeal is size. A docker node 18 image based on Debian (node:18) is a few hundred megabytes, while node:18-alpine builds on Alpine Linux with musl libc and comes in around 50 MB. Smaller images pull faster, cost less to store, and present a smaller attack surface because Alpine ships almost no extra packages. For CI pipelines that rebuild images constantly, the difference in pull time adds up.

That is why docker node 18 alpine became a default choice in so many Dockerfiles. The tradeoffs are real too: Alpine uses musl instead of glibc, which occasionally breaks native modules, and it lacks some debugging tools you would expect on a full distro. But for most JavaScript workloads it just works, which is exactly why so many teams are still running it.

What the April 2025 EOL actually changes

Node.js follows a predictable release cadence. Even-numbered majors get 12 months of Active LTS after promotion, then roughly 18 months of Maintenance, then end of life. Node.js 18 reached EOL on April 30, 2025. After that date:

  • No more security releases for the 18.x line from the core project.
  • No bug fixes, no maintenance builds.
  • The node:18-alpine tag still exists and still pulls, so nothing visibly breaks. That is the trap.

An image that keeps working is easy to leave in place. But the runtime underneath it is frozen at the last 18.x release (18.20.x), and every Node vulnerability disclosed afterward stays open on that line unless you buy commercial post-EOL support from a vendor like HeroDevs, NodeSource, or TuxCare.

Two separate layers of risk

When you scan a node:18-alpine container you are really looking at two things:

The Node.js runtime. This is the layer frozen by EOL. It includes Node itself, the bundled npm, and the V8 engine. Post-EOL, this layer does not get patched upstream.

The Alpine base and OS packages. Alpine keeps shipping updates for its own packages (OpenSSL, musl, busybox, ca-certificates). Rebuilding your image pulls the latest of these, so OS-level CVEs can still be fixed. But the Node runtime pinned to 18 will not move.

This split matters because a scanner may show you a "clean" OS layer while the runtime underneath is unsupported. A green OS scan does not mean the container is current.

Migrating off node:18-alpine

The move is usually straightforward because Node's LTS lines are backward compatible for most application code. The active LTS options at time of writing are Node 20 and Node 22.

# Before
FROM node:18-alpine

# After (pick the current active LTS)
FROM node:22-alpine

Before you flip production, do this:

  1. Update your engines field so the intent is explicit and tooling can enforce it:
{
  "engines": {
    "node": ">=22.0.0"
  }
}
  1. Pin to a digest, not just a tag, so the image cannot drift under you:
FROM node:22-alpine@sha256:<digest>
  1. Rebuild native dependencies. Anything with a compiled component (node-gyp, sharp, bcrypt) needs a fresh npm install against the new runtime, and on Alpine you may need build tools in a multi-stage build:
FROM node:22-alpine AS build
RUN apk add --no-cache python3 make g++
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .

FROM node:22-alpine
WORKDIR /app
COPY --from=build /app .
USER node
CMD ["node", "server.js"]
  1. Run your test suite against the new image. Most breakage comes from removed deprecated APIs or native module ABI changes, both of which surface in tests quickly.

Making container drift visible

The reason EOL images linger is that nobody notices them. A base image gets set once and forgotten across dozens of services. Container scanning during your build catches this: it flags the runtime version, the OS packages, and known CVEs in both layers. An SCA and container scanning tool can fail a build when the base image is an EOL runtime, which turns "we'll upgrade eventually" into a hard gate. Wiring that into CI is far more reliable than a spreadsheet of images someone is supposed to review.

If you want to compare how different tools handle base-image and runtime detection, our comparison writeup covers the container scanning angle specifically.

FAQ

Is node:18-alpine broken now that Node 18 is EOL?

No, the image still pulls and runs. The risk is that the Node 18 runtime no longer receives security patches after April 30, 2025, so new runtime CVEs stay unfixed. It works today but accumulates unpatched risk over time.

Should I move to node:20-alpine or node:22-alpine?

Either is a supported LTS at the moment. Node 22 gives you a longer support window before its own EOL, so if you are upgrading anyway, going to the newest active LTS buys more runway.

Does rebuilding my node:18-alpine image fix the security issues?

Rebuilding pulls fresh Alpine OS packages, so OS-level CVEs get patched. It does not update the Node runtime itself, which stays at the frozen 18.20.x line. Only moving to a supported major fixes runtime-level exposure.

How do I stop EOL base images from creeping back in?

Enforce it in CI. Pin base images by digest, set the engines field in package.json, and use container scanning that fails the build when the base runtime is past end of life.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.