The Problem with "Standard" Images
If you are a Node.js developer, your Dockerfile probably starts with one of two lines:
-
FROM node:24orFROM node:25(Debian-based) -
FROM node:24-alpineorFROM node:25-alpine(Alpine-based)
The Debian image is massive (~400MB+ uncompressed) and often carries a backlog of CVEs (Common Vulnerabilities and Exposures) in system libraries you never use.
The Alpine image is much better (~130MB uncompressed), but it still includes a shell (/bin/sh), a package manager (apk), and tools like wget or curl.
Ask yourself: Does your production container really need a shell? Does it need to be able to install new packages at runtime?
If the answer is no, then those tools represent attack surface. If a hacker manages to inject code into your app, providing them with /bin/sh and wget is like rolling out the red carpet.
The Solution: Going "Distroless" with Scratch\
I decided to engineer a "Gold Standard" runtime image. The goal was simple: Zero Bloat.
I call the project Runtime Node.
Here is the architecture of how I built it.
1. The Foundation: FROM scratch
In Docker, scratch isn't a Linux distribution. It is null. It is an empty file system with absolutely nothing in it.
By starting here, we opt out of the "Supply Chain" risks associated with Debian or Alpine. There is no OS to patch because there is no OS.
2. The Surgery: Extracting Only What Matters
Node.js cannot run in a vacuum; it needs shared libraries. Using a multi-stage build, I used the official Alpine image as a donor to extract only the specific libraries Node needs to function:
-
libstdc++: For C++ standard library support. -
libgcc_s: For GCC runtime support. -
ld-musl: The dynamic linker. -
ca-certificates: Essential for making HTTPS requests.
Here is a snippet of the Dockerfile logic:
# Stage 1: The Donor
FROM node:25.7.0-alpine3.22 AS builder
# Stage 2: The Runtime
FROM scratch
# Copy only the essentials
COPY --from=builder /lib/ld-musl-*.so.1 /lib/
COPY --from=builder /usr/lib/libstdc++.so.6 /usr/lib/
COPY --from=builder /usr/lib/libgcc_s.so.1 /usr/lib/
COPY --from=builder /usr/local/bin/node /usr/local/bin/node
3. The Hardening: chmod 555
Most images leave system binaries as writable (755). This allows a root user (or an attacker with root escalation) to modify the binary.
In Runtime-Node, I implemented a strict permission model. All binaries and libraries are copied with chmod 555 (Read/Execute, No Write).
COPY --from=builder --chmod=555 /usr/local/bin/node /usr/local/bin/
This makes the runtime immutable. Even if the container runs as root, the filesystem fights back against modification.
4. The "Gotchas": DNS and /tmp
Running on scratch usually breaks two things: DNS Resolution and Temporary Files. I fixed both:
-
DNS: I injected a minimal
nsswitch.conffile so the runtime knows how to resolvegoogle.comor your database host. -
Tmp: I created a
/tmpdirectory with1777permissions (Sticky Bit), allowing the app to write temp files (uploads/processing) without needing a full OS.
The Result: v2.0.0 (Node 25)
I just released version v2.0.0-25.7.0, which brings the bleeding-edge Node.js 25.7.0 to this infrastructure.
The Stats:
- Size: ~48 MB
- Vulnerabilities: 0
- Base: Scratch
- Architecture: AMD64 & ARM64 (Works on Apple Silicon/Raspberry Pi)
How to use it
It is designed to be a drop-in replacement for your final stage. You build your app, and then copy it onto this runtime.
# 1. Build your app
FROM node:25.7.0-alpine3.22 AS builder
WORKDIR /app
COPY . .
RUN npm ci --omit=dev
# 2. Run it on Runtime-Node
FROM runtimenode/runtime-node:v2.0.0-25.7.0
COPY --from=builder --chown=1000:1000 --chmod=770 /app /app
WORKDIR /app
# Run as non-root
USER 1000:1000
ENTRYPOINT ["/usr/local/bin/node", "index.js"]
Conclusion
You don't need a full Operating System to run a JavaScript file. By stripping away the shell and package manager, we gain performance, reduce storage costs, and significantly improve security posture.
The project is fully Open Source (Apache 2.0) and available on Docker Hub and GitHub Container Registry.
- GitHub: Runtime-Node
- Docker Hub: runtimenode/runtime-node
Give it a star if you dig the minimalism! ⭐



Top comments (0)