The phrase "it works on my machine" is often not just a joke in the software development world. One of the reasons is dependencies—third-party packages or libraries used by the application—are installed in different versions between the developer's laptop, the testing server, and production.
This is where the lockfile comes into play. Files like package-lock.json, composer.lock, or pinned dependency versions in requirements.txt are not just additional decorations in the repository. They serve as detailed records of which packages are actually used, including transitive dependencies that may never have been directly written by the team.
Package manifest and lockfile have different roles
In a Node.js project, the package.json file typically states requirements in the form of version ranges, such as ^4.18.0. This means npm can choose a version that is still considered compatible according to those rules. The problem is that the versions available in the registry can change after a few weeks.
The lockfile stores the resolution results more specifically. npm documentation explains that package-lock.json describes the dependency tree generated so that subsequent installations can produce the same package arrangement. This file also records information such as version, package source, and integrity data for downloaded artifacts. npm package-lock documentation explains these details.
A simple analogy: package.json is a list of ingredients and tolerances for a recipe, while the lockfile is an actual shopping list—the brands, sizes, and quantities that are actually used.
Why can small differences become big problems?
Dependencies rarely stand alone. Your application might use framework A, which requires library B, which then depends on library C. When one of the packages changes, the end result can also change even if the main code is untouched.
- The API of a library can change and cause old functions to fail.
- Validation or parsing rules can become stricter.
- Transitive dependencies can introduce behavioral changes that are not visible from the main configuration file.
- Builds in CI can fail because the runtime and package versions no longer match.
- Local test results may not reflect server conditions.
This does not mean that every dependency update is dangerous. Routine updates are actually important to obtain bug fixes and security patches. The issue is that updates should occur consciously, tested, and documented—not appear accidentally when someone runs an installation command.
Best practices for Node.js, PHP, and Python
1. Node.js: commit package-lock.json
For Node.js applications, keep package-lock.json in version control alongside package.json. When setting up a server or CI pipeline, use npm ci if your need is to install based on the lockfile consistently, rather than changing dependencies directly.
Do not edit the lockfile manually unless you fully understand the format and its implications. To update packages, use the appropriate npm commands, run tests, and then review the lockfile changes in the pull request. Significant changes to this file are not automatically a problem, but they need to be checked as they may include many transitive dependencies.
2. PHP and Composer: distinguish applications from libraries
In PHP application projects, composer.lock generally needs to be stored in the repository. Composer explains that running install with the lockfile will install the versions recorded there, rather than automatically fetching the latest versions that still match composer.json. This helps developers, CI, and servers use the same dependencies.
The composer update command should be treated as a dependency change activity. Run it when you actually want to update packages, not as a routine step every time you deploy. After that, run tests and check for any configuration changes, PHP compatibility, or application behavior.
For libraries that will be used by other projects, the rules may differ. The library's lockfile does not control the dependencies of the applications that use it. Therefore, the decision to keep composer.lock needs to be adjusted according to the project's goals and the team's workflow.
3. Python: do not let requirements.txt be too loose
Python has many dependency management patterns. One simple approach is to pin versions, such as requests==2.32.0, and then install them with python -m pip install -r requirements.txt. Pip documentation states that requirements files can be used for repeatable installs, especially when package versions are specifically recorded.
For tighter security needs, pip also provides a hash-checking mode. In this mode, dependencies need to be pinned and accompanied by hashes so that packages differing from the expected artifacts will be rejected. This approach does add maintenance work, but it is worth considering for sensitive builds or production environments.
Lockfile is not a substitute for security updates
A common misconception is to think that a lockfile means dependencies should not change. In fact, its function is to make changes controlled. A lockfile that is never updated can leave an application behind on important security fixes.
Use a reasonable update schedule, such as reviewing dependencies every week or every two weeks. Differentiate between patch, minor, and major updates. Major updates usually require more attention as they can introduce incompatible changes.
If using an automated bot like Dependabot, do not merge all pull requests immediately. Ensure the pipeline runs unit tests, integration tests, lint checks, and production builds. Bots can help find updates; the decision to merge them still requires project context.
Checklist to implement now
- Ensure the manifest and lockfile are stored together in the repository for application projects.
- Use consistent toolchain versions, such as the same version of Node.js, PHP, or Python locally and in CI.
- Use installation commands that respect the lockfile during builds and deployments.
- Do not run dependency update commands without reviewing the resulting changes.
- Add automated tests before new dependencies enter the main branch.
- Document the reasons for major updates, especially if they touch frameworks, database drivers, or authentication libraries.
- Regularly review old dependencies so that consistency does not turn into risky stagnation.
What does this mean for us?
The lockfile is a small form of discipline that reduces big surprises. It does not guarantee that applications are bug-free, does not replace security audits, and does not make all machines identical in every aspect. However, the lockfile narrows the problem space: when a build fails, the team can more easily distinguish whether the cause comes from code, configuration, runtime, or dependency changes.
By treating the lockfile as part of the engineering process—not a file ignored during merges—teams can update software more calmly. Dependencies can still evolve, but changes happen with a clear trail, can be tested, and are easier to roll back when the results are not as expected.
Sources & further reading
- npm Documentation: package-lock.json
- Composer Documentation: Basic Usage
- pip Documentation: User Guide
- pip Documentation: Secure Installs
– Rio Yotto @rioyotto
