← All Articles ⚙️ Configuration & DevOps Medium By Nishant Kumar Verma Updated

Unpinned Docker Images: Why :latest Tag Breaks Production Builds

Using FROM node:latest in your Dockerfile means your builds are not reproducible. Learn how to pin Docker image versions properly.

CFG-004

Quick Answer

Using FROM node:latest in a Dockerfile means a different base image may be pulled on every build. A build that works today can fail tomorrow when latest resolves to a new Node.js major version with breaking changes or removed APIs. Pin your images to a full version (e.g. FROM node:24.22.0-alpine3.24), and add the image digest when the build must never change.

What "Unpinned" Means and Why It Matters

A Docker image tag like :latest is a mutable pointer. When you run docker pull node:latest today, you get Node.js 26. When a CI server pulls the same tag six months from now, it may get Node.js 27 - or a patch release of Node.js 26 that includes a breaking change in a native module.

The Kubernetes documentation warns against it directly: "You should avoid using the :latest tag when deploying containers in production", because it is harder to track which version is running and to roll back. Yet AI tools like Cursor, Bolt, and Lovable generate FROM node:latest as default boilerplate because it's the simplest example and always resolves to something that will build initially.

Reproducible builds make every deploy predictable: the image you tested is the image you ship. Unpinned base images are a common cause of non-reproducible builds in containerised applications.

The AI-Generated Dockerfile Pattern

# ❌ BAD - Unpinned base image (resolves to unknown future version)
FROM node:latest

WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
CMD ["node", "dist/server.js"]
# ❌ BAD - Tag pinned to major version only (still resolves to latest patch/minor)
FROM node:24

WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
CMD ["node", "dist/server.js"]

The second example is better than :latest but still allows minor and patch updates to change the build environment. If a patch update to Node.js 24 introduces a change in its OpenSSL or native module handling, your build breaks without any change to your source code.

The Correct Pattern: Pinned Version With Digest

# ✅ GOOD - Full version tag pinned (recommended minimum)
FROM node:24.22.0-alpine3.24

WORKDIR /app

# Copy only package files first for better layer caching
COPY package*.json ./
RUN npm ci

COPY . .
RUN npm run build
RUN npm prune --omit=dev

# Use non-root user for security
USER node
CMD ["node", "dist/server.js"]
# ✅ BEST - Version tag plus SHA256 digest (fully immutable)
FROM node:24.22.0-alpine3.24@sha256:44fce696f38ba8c987c4db89954051000bbd05ffd8d8659ba15ed4a8602e937f

WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build && npm prune --omit=dev
USER node
CMD ["node", "dist/server.js"]

The SHA256 digest approach is the most secure. Docker's documentation notes that image tags are mutable, and official images are rebuilt with security patches under the same tag, so even 24.22.0-alpine3.24 can point to a new image later. The digest identifies one exact image. Get the current one with docker buildx imagetools inspect node:24.22.0-alpine3.24. The tradeoff is that a pinned digest also stops automatic security fixes. For most production applications, full version pinning (without digest) is sufficient and more maintainable.

Image Tag Strategy by Stability Requirement

Tag Pattern Example Mutability Recommended For
:latest node:latest Changes on every major/minor/patch release Never use in production
Major version node:24 Changes on minor and patch releases CI experimentation only
Minor version node:24.22 Changes on patch releases Development environments
Full version node:24.22.0-alpine3.24 Same Node.js version; rebuilt for OS security patches Production builds
Full version + digest node:24.22.0@sha256:abc... Completely immutable High-security production

Alpine vs. Debian-Based Images

AI-generated Dockerfiles often omit the OS variant entirely, defaulting to the Debian-based image (e.g. node:24 resolves to Debian bookworm). Alpine-based images (node:24-alpine) are significantly smaller - on Docker Hub the compressed linux/amd64 download is about 60MB against about 410MB for the Debian image - which reduces pull times, attack surface, and container registry storage costs. For most Node.js applications, Alpine works correctly. The exception is native modules that require glibc (Alpine uses musl libc), which occasionally causes compatibility issues.

# ❌ BAD - Full Debian base, ~410MB compressed download
FROM node:24.22.0

# ✅ GOOD - Alpine base, ~60MB compressed download
FROM node:24.22.0-alpine3.24

# ✅ ALSO GOOD - Slim Debian variant, ~80MB compressed, better glibc compatibility
FROM node:24.22.0-slim

How to Find and Fix Unpinned Images in Your Project

  1. Search for FROM statements with unpinned tags: Look for FROM image:latest or FROM image (no tag) in all Dockerfiles, including docker-compose.yml service images.
  2. Find the current version: Visit Docker Hub for your base image and look at the "Tags" tab to find the current full-version tag. For Node.js, the LTS versions are the safest choice for production.
  3. Update and test: Replace the unpinned tag with the current full version, rebuild, run your test suite, and verify the application starts correctly.
  4. Set up Dependabot for Docker: GitHub's Dependabot can automatically open pull requests when newer versions of your pinned base images are available. This gives you controlled, reviewed updates rather than silent drift.
  5. Run automated scanning: VibeDoctor's Vibe Check flags every FROM or image: line that uses the :latest tag in the Dockerfile or docker-compose.yml at your repo root (CFG-004), with the file and line number. It does not flag a missing tag or a major-only tag like node:24, so step 1 still matters. Free to sign up.

A real example: on 21 October 2026 PHP released fixes for two critical remote code execution bugs, and a container keeps the PHP version it was built with. Our PHP2Shell guide shows how to check the PHP inside your image and rebuild on a fixed version.

FAQ

Does Vercel use Docker for deployments?

Vercel manages its own build infrastructure and does not use your Dockerfile for Next.js deployments by default. It only builds a container from a file named Dockerfile.vercel, through its Container Runtime (in beta), and the same pinning advice applies to that file. Vercel's build system handles Node.js version management separately - you specify the Node.js version in your package.json engines field or via the Vercel dashboard. Dockerfiles are relevant if you're self-hosting on Railway, Fly.io, AWS ECS, or any container-based platform.

Should I use the same Node.js version in my Dockerfile as in package.json engines?

Yes, and they should match precisely. If your package.json specifies "engines": { "node": ">=24.0.0" }, your Dockerfile should pin to a Node.js 24.x image. Mismatches between the local development Node.js version, the Dockerfile version, and the CI environment version are a frequent source of "works on my machine" bugs in AI-generated projects.

What is a multi-stage Docker build and should AI-generated apps use one?

Multi-stage builds use multiple FROM statements to separate the build environment from the runtime environment. You install dependencies and compile TypeScript in a full Node.js image, then copy only the compiled output into a minimal runtime image, so the final image holds only what the app needs to run. AI tools rarely generate multi-stage builds, but they're straightforward to add and meaningfully improve both image size and security.

How do I update pinned versions without breaking things?

The recommended process is: create a branch, update the base image version in your Dockerfile, rebuild, run your full test suite, deploy to a staging environment, verify application behaviour, then merge. This process - which Dependabot can automate for the pull request creation step - gives you controlled updates rather than the silent surprise of :latest pulling a new version on a production deployment.

Does pinning Docker images protect against supply chain attacks?

Partial protection. Pinning to a specific version tag keeps the Node.js version fixed, but the tag itself is mutable: Docker Hub tags can be pushed again, and official images are rebuilt under the same tag. For true supply chain security, pin to the SHA256 digest - this is cryptographically immutable and cannot be replaced even if the tag is overwritten. For most applications, full version pinning provides sufficient protection; digest pinning is more appropriate for regulated industries or high-security deployments.

Diagnose your codebase - free

VibeDoctor checks for CFG-004 and 148 other issues across 21 diagnostic areas - security, performance, code quality, and more.

SCAN MY APP →