Safeguard
Containers

Running Node.js on Ubuntu in Docker, Securely

A guide to the Node Ubuntu Docker pattern: when an Ubuntu base makes sense for Node.js, how to build it safely, and the security trade-offs versus slim images.

Karan Patel
Platform Engineer
6 min read

Building a Node.js container on an Ubuntu base is a valid choice when you need specific system libraries or glibc compatibility, but it produces a larger image with more attack surface than a slim or distroless base, so you should only reach for the Node Ubuntu Docker pattern when you actually need what Ubuntu provides. The default temptation is to start FROM ubuntu because it feels familiar, then install Node.js by hand. That works, but it usually gives you a bigger, less secure image than you intended unless you build it deliberately.

This guide covers when Ubuntu is the right base, how to build a Node.js image on it without common mistakes, and how it compares to the alternatives.

When Ubuntu actually makes sense

Most Node.js apps do not need Ubuntu. The official node images are built on Debian and already handle the runtime cleanly. Reach for Ubuntu specifically when:

  • A native module or system dependency ships or is tested primarily against Ubuntu.
  • You need glibc (which rules out Alpine's musl) plus Ubuntu-specific packages.
  • You are matching a production host or a base image mandated by your platform team for patching and compliance reasons.
  • You need a recent Ubuntu LTS for its kernel or library versions.

As of 2026, the current LTS releases are Ubuntu 24.04 LTS (Noble Numbat), supported into 2029, and Ubuntu 26.04 LTS (Resolute Raccoon), released in April 2026 and supported until April 2031. Pin to one of these; never build on an interim or end-of-life release, because that determines whether you keep getting security patches for the OS layer.

Installing Node.js on Ubuntu the right way

The wrong way is apt install nodejs, which on Ubuntu often lands an old version from the distribution repositories. Use NodeSource or the official binaries to get a current, supported Node.js. A NodeSource-based install pinned to an LTS major:

FROM ubuntu:24.04

RUN apt-get update && \
    apt-get install -y --no-install-recommends curl ca-certificates && \
    curl -fsSL https://deb.nodesource.com/setup_24.x | bash - && \
    apt-get install -y --no-install-recommends nodejs && \
    apt-get clean && rm -rf /var/lib/apt/lists/*

Two details matter for security and size here. --no-install-recommends stops apt from pulling in a pile of suggested packages you never asked for, each one attack surface. Clearing the apt lists in the same RUN layer keeps package metadata out of the final image — doing it in a later layer would not shrink anything, since layers are immutable.

Build small with multi-stage

Even on Ubuntu you can keep the runtime image lean by separating build from runtime. Compile in a full stage, then copy artifacts into a clean Ubuntu runtime with only production dependencies:

FROM ubuntu:24.04 AS build
RUN apt-get update && apt-get install -y --no-install-recommends curl ca-certificates && \
    curl -fsSL https://deb.nodesource.com/setup_24.x | bash - && \
    apt-get install -y --no-install-recommends nodejs && \
    rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build && npm prune --omit=dev

FROM ubuntu:24.04
RUN apt-get update && apt-get install -y --no-install-recommends curl ca-certificates && \
    curl -fsSL https://deb.nodesource.com/setup_24.x | bash - && \
    apt-get install -y --no-install-recommends nodejs && \
    apt-get clean && rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules

The runtime stage carries no source, no build tooling, and no devDependencies. If your Node.js version supports it, using the official node:24 image as the build stage and only Ubuntu as the runtime is even cleaner.

Run as a non-root user

Ubuntu base images do not create an application user for you, so add one and switch to it. Never leave the process running as root:

RUN groupadd --system --gid 1001 nodeapp && \
    useradd --system --uid 1001 --gid nodeapp nodeapp
COPY --chown=nodeapp:nodeapp . .
USER nodeapp
CMD ["node", "dist/server.js"]

If an attacker gets code execution and then escapes the container, running as an unprivileged user is what stops them landing on the host as root. This is the single most important line in the file. Enforce it at the orchestrator too — set runAsNonRoot: true in your Kubernetes pod security context so a container that forgets USER is refused.

The security trade-off versus slim and distroless

An Ubuntu-based Node.js image is convenient and compatible, but honestly assess the cost. A full Ubuntu base carries a shell, apt, and a large set of OS packages, every one of which is a potential CVE and a tool an attacker can use after a breakout. A node:24-slim image is smaller with fewer packages; a distroless image has no shell or package manager at all.

If you can meet your dependency requirements on slim or distroless, they are the more secure default. Use Ubuntu when compatibility genuinely requires it, and then compensate: scan aggressively, rebuild often, and drop capabilities at runtime. Scan every build in CI and fail on high-severity findings:

trivy image --severity HIGH,CRITICAL --exit-code 1 myapp:ubuntu

Because Ubuntu carries more packages, it will typically show more CVEs than a minimal base, which makes disciplined scanning and a regular rebuild cadence more important, not less. A software composition analysis tool can track the npm dependency side alongside the OS layer so both sources of risk stay visible.

FAQ

Should I use Ubuntu or the official Node.js image as my base?

Prefer the official node image (Debian-based) or its slim variant for most apps — it is smaller, maintained for Node.js specifically, and ships a non-root user. Choose an Ubuntu base only when you need Ubuntu-specific packages, glibc compatibility Alpine cannot provide, or parity with a mandated platform image.

Why not just apt install nodejs on Ubuntu?

Ubuntu's default repositories often carry an older Node.js version that may be out of LTS support and missing security patches. Use NodeSource or the official binaries pinned to a current LTS major so you control the version and keep receiving updates.

Which Ubuntu version should I pin to?

Use a current LTS: Ubuntu 24.04 (Noble Numbat) or 26.04 (Resolute Raccoon) as of 2026. Pin the exact tag rather than a floating one, and never build on an interim or end-of-life release, since that governs whether the OS layer keeps getting security fixes.

Does an Ubuntu base make my container less secure?

Not inherently, but it has more attack surface than a minimal base because it ships more packages and a full shell. That means more potential CVEs and more post-breakout tooling for an attacker. If you use Ubuntu, offset it with non-root execution, frequent rebuilds, dropped capabilities, and rigorous scanning.

Never miss an update

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