✦ For everyone, free.

Practical knowledge for real and everyday life

Home

17.1.2.1 Dependency Version Pinning

A focused guide to Dependency Version Pinning, connecting core concepts with practical Docker and container operations.

Dependency version pinning is the deliberate choice of how precisely a dependency's version is specified, ranging from a loose range that automatically picks up new compatible releases to an exact, fixed version that never changes without explicit action, and the right choice for a given dependency balances build stability and predictability against the risk of missing important security patches if pinned too rigidly with no process to revisit it.

The pinning spectrum

Most package managers support several distinct levels of version specificity, each representing a different point on the stability-versus-currency trade-off:

{
  "dependencies": {
    "express": "^4.18.0",
    "lodash": "~4.17.21",
    "left-pad": "1.3.0"
  }
}

A caret range (^4.18.0) permits any version considered backward-compatible under semantic versioning, typically any 4.x.x release; a tilde range (~4.17.21) permits only patch-level updates within the same minor version; an exact version (1.3.0) permits no automatic movement at all without an explicit, deliberate change to the declared version.

Why a lockfile matters more than the manifest's own range

Regardless of how loose or strict the manifest's declared version ranges are, a lockfile records the exact, specific version actually resolved and installed at the time it was generated, and a build that respects the lockfile faithfully reproduces that exact version on every subsequent build, regardless of what newer versions might satisfy the manifest's looser range in the meantime:

npm ci

This is why a lockfile, committed to version control and respected by a strict installation command, is the actual mechanism providing build reproducibility, while the manifest's own version ranges primarily govern what happens specifically when the lockfile is intentionally regenerated, during a deliberate dependency update, rather than affecting every individual build.

Pinning base images: tag versus digest

The same spectrum applies to base images, where a major-version tag like node:20 tracks ongoing patch updates automatically, while a full digest reference fixes the base image to one specific, immutable point:

FROM node:20
FROM node:20.11.0
FROM node:20@sha256:3f29a8c1d8e2b4f6a9c7d5e8b1f3a6c9d2e5f8b1a4c7d0e3f6a9c2d5e8b1f4a7

A major-version tag automatically incorporates security patches on every rebuild without requiring any explicit action, which is convenient but means the exact base content can differ between builds performed at different times; a digest reference provides complete certainty about content but requires an explicit, deliberate process to ever incorporate an upstream update at all.

The risk of pinning too rigidly with no update process

Pinning every dependency and base image to an exact, fixed version provides maximum build stability and reproducibility, but if no process exists to periodically revisit and update those pinned versions, the project gradually accumulates increasingly outdated, potentially vulnerable dependencies that never get refreshed simply because nothing forces anyone to look at them again:

npm outdated
npm audit

Running these kinds of checks periodically, ideally on an automated schedule, surfaces exactly which pinned dependencies have since had newer versions, or known vulnerabilities, published, which is the necessary complement to strict pinning: pinning provides stability and reproducibility, while a deliberate, scheduled review process provides the currency that strict pinning alone does not.

Automated dependency update tooling

Tools like Dependabot and Renovate automate the otherwise manual process of detecting outdated, pinned dependencies and proposing an update, typically as an automatically generated pull request that can be reviewed, tested, and merged through the normal code review process:

version: 2
updates:
  - package-ecosystem: "npm"
    directory: "/"
    schedule:
      interval: "weekly"

This combination, strict pinning for stability and reproducibility, paired with automated, scheduled update proposals for currency, generally provides a better balance than either extreme alone: neither perpetually outdated, rigidly pinned dependencies, nor unpredictable, automatically shifting versions with no review step at all.

Choosing the right pinning strategy per dependency

Not every dependency warrants the same level of pinning strictness; a security-sensitive dependency, such as a cryptography or authentication library, often warrants tighter, more deliberate version control and review than a low-risk utility library where automatic minor version updates carry little practical risk:

{
  "dependencies": {
    "jsonwebtoken": "9.0.2",
    "lodash": "^4.17.21"
  }
}

Applying a uniform pinning policy across every dependency regardless of its actual risk profile and update frequency is simpler to reason about but misses the opportunity to apply tighter control specifically where it matters most.

Reviewing pinned versions as part of routine maintenance

Treating dependency version review as routine, scheduled maintenance, rather than something only revisited reactively after a vulnerability disclosure or an unrelated incident draws attention to it, keeps pinned versions from drifting too far out of date before anyone notices:

0 3 * * 1 npm outdated > outdated-report.txt

A scheduled check like this, even if it only produces a report for manual review rather than an automated update, ensures the question of dependency currency is asked regularly rather than left entirely to chance or external prompting.

Common mistakes

  • Relying solely on loose version ranges for stability, when the lockfile, not the manifest's range, is actually what determines build reproducibility from one build to the next.
  • Pinning every dependency and base image rigidly with no scheduled process to ever revisit and update them, accumulating outdated, potentially vulnerable versions silently over time.
  • Applying a uniform pinning strategy across every dependency regardless of its actual security sensitivity or update frequency.
  • Not adopting automated dependency update tooling, leaving the detection of outdated pinned versions to manual, infrequent, and easily forgotten checks.
  • Treating dependency version review as a reactive exercise triggered only by an incident or vulnerability disclosure, rather than as routine, scheduled maintenance.

Dependency version pinning works best as a deliberate, calibrated balance, strict enough through lockfiles and digest-pinned base images to guarantee reproducibility, but paired with a genuine, scheduled process, whether manual review or automated tooling like Dependabot or Renovate, that ensures pinned versions are periodically revisited and updated rather than left to silently accumulate staleness indefinitely.