Article

The composer.lock that only builds on your machine

A git-ignored path repository that symlinks a registry module to a local checkout writes a path dist into composer.lock. Commit that lock and every CI job and image build fails, because the sibling directory only exists on your machine. Relock with the override set aside, and add a CI guard that rejects a path dist.
July 14, 2026
Topics:Dependency ManagementCI/CD
Tags:ComposerPHP

You develop a contrib module against a local checkout, commit, and CI goes red on every job — or worse, the production image build fails. The culprit is one line in composer.lock that only makes sense on your laptop.

The setup

To hack on a module that is published to a registry, it is common to point Composer at a local checkout with a path repository in a git-ignored override:

// composer.local.json (git-ignored)
"repositories": { "my_module": { "type": "path", "url": "../my_module" } }

Composer symlinks the module from the sibling directory. Great for development. The trap is what it writes into the lock.

The trap

With the override active, composer.lock records the module with a path dist pointing at ../my_module:

"dist": { "type": "path", "url": "../my_module" }

That path exists only on your machine. Commit the lock and every environment that runs composer install from it — CI, the image build, a teammate — looks for a sibling directory that is not there, and fails. The registry version never gets installed, because the lock says to use the path.

The fix

Regenerate the lock with the override out of the way, so it records the registry dist:

mv composer.local.json composer.local.json.aside
composer update my_module --no-install   # relock from the registry
mv composer.local.json.aside composer.local.json

Confirm the lock now shows a real registry dist ("type": "zip", an https URL), not a path. Then commit only the lock — never the override.

Guard it in CI

This is easy to do by accident, so make it impossible to merge. A one-line check that fails the build when the lock contains a path dist catches every case before it lands:

grep -q '"type": "path"' composer.lock && { echo 'path dist in lock'; exit 1; } || true

Takeaway

A local path override is a development convenience that must never reach the committed lock. Relock with it set aside, verify the dist is a registry URL, and add a lockfile guard so "works on my machine" cannot become "breaks every build."