Disabling symlinks for local Composer package repositories
This documentation needs review. See "Help improve this page" in the sidebar.
When a local Drupal module has no composer.json file, you must use a package-type Composer repository (as described on Tricks for using Composer in local development). With a package-type repository, the dist type is set to path, and Composer installs the package by symlinking to the local directory by default.
Symlinks can cause problems in several environments:
- Docker with bind mounts - the symlink target may not be accessible from inside the container.
- CI/CD pipelines - the build step that packages a deployable artifact (zip, tar, rsync) does not follow symlinks, so the linked module files are missing from the artifact.
- Deployment environments - platforms that serve from a read-only or containerised filesystem (PaaS hosts, cloud functions, read-only image layers) require real files, not symlinks.
There are two ways to configure Composer to copy the package files instead of symlinking them: set transport-options in composer.json, or use the COMPOSER_MIRROR_PATH_REPOS environment variable.
Using composer.json
Add a transport-options key with "symlink": false directly inside the package definition:
"repositories": [
{
"type": "package",
"package": {
"name": "drupal/your_project_name",
"version": "dev-1.0.x",
"type": "drupal-module",
"transport-options": {
"symlink": false
},
"dist": {
"url": "path/to/your/local/copy",
"type": "path"
}
},
{
"type": "composer",
"url": "https://packages.drupal.org/8"
}
]Composer will copy the package files instead of symlinking. You will see Mirroring from in the output when the setting is in effect.
Warning
transport-options.symlink is an internal Composer field and is not part of Composer's documented public API. It works as of Composer 2.x but could change in a future version without notice.
Using an environment variable
The COMPOSER_MIRROR_PATH_REPOS environment variable tells Composer to prefer mirroring (copying) for all path-based packages:
COMPOSER_MIRROR_PATH_REPOS=1 composer installYou can set this in your shell profile, a .env file, or your CI configuration.
If a package explicitly sets symlink to true in its transport-options, that package-level setting still takes precedence.
Which option to use
Use transport-options in composer.json when:
- You need per-package control — for example, some packages should symlink while others should copy.
- You want the behavior committed to
composer.jsonand consistent across all environments without relying on an environment variable being set.
Use COMPOSER_MIRROR_PATH_REPOS when:
- You want mirroring for all path packages without modifying
composer.json. - You are concerned about relying on the undocumented
transport-optionsfield. - Your CI or deployment platform already sets this variable, meaning mirroring may already be active with no additional configuration.
Why transport-options works
transport-options.symlink is not documented as a valid key in a package repository definition, but it works because it replicates exactly what Composer sets internally for path-type repositories.
For a path-type repository, PathRepository::loadPackages() automatically copies the options.symlink setting from the repository definition into the package object's transport-options array (PathRepository.php, line 188):
$package['transport-options'] = array_intersect_key($this->options, ['symlink' => true, 'relative' => true]);For a package-type repository there is no PathRepository involved, so that automatic step does not happen. Instead, ArrayLoader::load() reads transport-options directly from the inline package definition in composer.json (ArrayLoader.php, lines 302-303). By setting it explicitly, you are replicating exactly what PathRepository would have set automatically.
In both cases the end result is the same: PathDownloader::computeAllowedStrategies() reads $transportOptions['symlink'] from the package object to decide the install strategy (PathDownloader.php, lines 275-291):
| Value | Behavior |
|---|---|
null (not set) |
Prefer symlink; fall back to mirror if symlinking fails. |
true |
Symlink only; no fallback. |
false |
Mirror (copy) only. |
Setting transport-options.symlink explicitly in a package repository definition therefore produces exactly the same runtime behavior as configuring options.symlink in a path repository.
Help improve this page
You can:
- Log in, click Edit, and edit this page
- Log in, click Discuss, update the Page status value, and suggest an improvement
- Log in and create a Documentation issue with your suggestion