Composer workflow for Drupal upgrades

A Drupal upgrade through Composer succeeds or fails on three choices: which core package the site tracks, how contributed module constraints resolve, and whether composer.lock reproduces the same build everywhere. This page explains each choice and the order to make them in.

On this page

How a Drupal upgrade moves through composer.json and composer.lock

A Drupal core or contributed module upgrade is decided in the version constraints in composer.json and in the composer.lock file that Composer regenerates from them, not by copying files onto a server. The team changes what the project is allowed to install, runs Composer to resolve a new set of exact versions, and then tests that resolved set. If the constraints are wrong or a contributed module has no release that fits them, Composer stops and reports the conflict, which is the point at which the work should be planned rather than forced through.

The timeline makes this predictable work worth doing carefully. According to the Drupal core release schedule, Drupal 10 reaches end of life on December 9, 2026, and Drupal 12 is scheduled for the week of December 7, 2026. Drupal 12 requires PHP 8.5 or later, so the PHP runtime and the build environment have to move before the composer require step can succeed for the new line. Teams should confirm the current release dates on the schedule themselves, since a site that still runs Drupal 10 has a short window between those two dates to pass through Drupal 11.

Before changing any constraint, read the Drupal.org Composer documentation on managing dependencies to confirm which core package the project should require. Commit the current composer.lock as the rollback point, run the update in a branch, and review the diff in both files, including the packages Composer changed that the team did not name directly. The reproducible result depends on committing the regenerated lock file so that every environment installs the same versions.

Choosing between drupal/core-recommended and drupal/core

The drupal/recommended-project template is the starting point most teams use for a new Drupal codebase. It creates a project layout with drupal/core-recommended already required, plus the scaffolding for Drush, the web root and the directories Composer and deployment tooling expect. The template sets a sensible baseline, but it does not decide how core and its dependencies move over time after the first install.

  • What the team gains: a documented project layout that developers, CI jobs and hosting scripts can all rely on from the first commit.
  • What the team must manage: the composer.json and composer.lock files after creation, since the template only sets the baseline. The team still owns contributed module choices and their version constraints.
  • Dependency ownership: Symfony components and other libraries that core depends on are resolved by Composer, so review the lock file diff whenever core or a contributed module changes.
  • When to move: the template suits almost every new project. Move to a different core package once the project is in production and core changes need controlled timing.

Before you change a production project, confirm the current template contents and package behavior on the Drupal.org Composer documentation.

Which Composer changes affect an upgrade and what each one touches

This table compares each Composer-level change in an upgrade with what it affects and the action it requires. Read each row as a checkpoint to clear before the code changes merge. The main takeaway is that core package choice, PHP platform requirements and contributed module constraints must all be resolved in the same lock file update.

ChangeImpactAction needed
Switching between drupal/core-recommended and drupal/coreChanges which core packages Composer pulls in and how dev and scaffold dependencies are managed during the updateConfirm the current package list against the Drupal.org Composer documentation before editing composer.json
Drupal 10 target with PHP 8.1 or laterSets the minimum PHP platform the build and runtime must meetVerify the PHP version on build agents and web servers before running the update
Drupal 11 target with PHP 8.3 or laterRaises the PHP floor, so older runtimes fail the platform checkUpgrade PHP on every environment first, including CI and cron hosts
Drupal 12 target with PHP 8.5 or laterRaises the PHP floor again, affecting every environment that runs the sitePlan the PHP step as its own change and confirm current requirements on Drupal.org
Contributed modules whose constraints do not allow the new coreComposer cannot resolve the update until the module constraint or release changesIdentify the blocking packages, find compatible releases, and decide whether to replace or hold them
Regenerated lock file entriesEvery resolved version of core and contributed packages changes, which can alter behavior across the whole siteReview the lock file diff, commit it with the code change, and rebuild from it in every environment
Monthly bugfix release on the first WednesdayCreates a recurring batch of core and contributed updates that may conflict with a pending upgrade branchSchedule update batches around the release and rebase the upgrade branch after each one
Security release on the third WednesdayCan require an out-of-cycle update that interrupts a planned upgrade windowAgree in advance how security patches are applied to the upgrade branch and the live site

Four version conflicts that stop a Composer upgrade

Most failed Composer upgrades for Drupal trace back to a small set of conflicts that Composer reports in similar ways. Each card below names one pattern, the likely cause, and the first check to run before changing constraints, so the team can narrow the problem before touching the lock file.

No contributed release for the target core

A contributed module may have no release that accepts the core version you are moving to. Composer then refuses to resolve the requirement, even though the rest of the project looks correct. The usual cause is that the maintainer has not yet tagged a release for the new core, or the module has been abandoned. Check the project page on Drupal.org for a release line that lists the target core, and review its issue queue for compatibility work. If none exists, decide between a replacement module, a fork or delaying the upgrade, and confirm the choice with your team before editing composer.json.

Constraint pins a shared dependency

One module's version constraint can hold a shared library or Drupal component at a version another module needs. Composer reports a conflict between two packages that both appear in your project, and the message names the constraint chain rather than a single broken module. The cause is often a tight pin added during an earlier fix that nobody revisited. Run Composer's why-not analysis for the blocked package to see which requirement holds it back, then decide whether that pin is still needed before loosening it.

Patch no longer applies

A patch declared in composer.json may fail when the upstream code it modifies has changed between releases. Composer stops during the install or update step while patching, and the failure looks like a build problem rather than a version problem. The likely cause is a patch written against an older release, or a patch whose issue has been fixed upstream and should be removed. Check the patch's linked issue for a merged fix, then either drop the patch or regenerate it against the new release and record the change in your release notes.

PHP version mismatch with the target

The build server or CI image may run a PHP version below what the target Drupal release requires. Composer then reports that the platform does not satisfy a requirement, and it may resolve against the wrong versions if the platform setting is overridden. The cause is usually a build image that has not been updated alongside the application. Confirm the PHP minimum for your target core on the Drupal.org PHP requirements page, check the php version Composer uses on the build host, and align the build image and any platform config before running the update.

Run the core and contributed upgrade on a branch, with rollback points

This sequence moves core and contributed packages to their target versions on a branch, with a restorable point after each change, so the team can stop at any step and return the site to its last known state. The output is one reviewed lock file that deploys unchanged to staging and production.

  1. Back up the database, files and current lock file

    Before any change, the site owner or platform engineer takes a database dump and a copy of the files directory, then tags the current composer.lock in version control. The tag is the first rollback point: restoring that lock file and the matching code returns Composer to the exact versions running in production. Confirm the database snapshot restores into a scratch environment before continuing, because a backup that has never been restored is not a rollback point.

  2. Create the upgrade branch and staging copy

    The developer creates a feature branch from the tagged commit and provisions a staging environment from the production database snapshot and files. All Composer changes happen on this branch, so production code stays on the tagged lock file until the upgrade passes review. The rollback point is the branch itself: deleting it discards the work without touching the running site. The step is done when staging loads the unchanged site from the snapshot.

  3. Update the PHP runtime on the build image

    The platform engineer changes the PHP version in the build image and CI runner before touching Composer constraints, because a target core release can refuse to resolve on an older PHP. Confirm the required version against the Drupal.org PHP requirements page for the target core. The rollback point is the previous container image tag, which the deployment can re-point to if the new runtime breaks the current build. The step is done when the existing lock file installs cleanly on the new runtime with no changes to packages.

  4. Change core and contributed constraints in composer.json

    The developer edits the version constraints in composer.json for drupal/core or drupal/core-recommended and for the contributed projects being moved, following the Drupal.org Composer documentation for switching between the two core packages. Changes are kept in a single commit so the diff shows exactly which constraints moved. The rollback point is that commit, which can be reverted on its own. The step is done when composer validate reports no errors for the edited file.

  5. Run Composer update for the intended packages only

    The developer runs composer update limited to the named core and contributed packages, not a bare composer update, so unrelated dependencies keep their locked versions. Packages that Composer cannot resolve appear as conflicts in the output and must be fixed in composer.json, not forced. The rollback point is the tagged composer.lock, which is restored with git checkout if the resolution is unacceptable. The step is done when Composer finishes without conflicts and the lock file is written.

  6. Review the composer.lock diff

    A second engineer reviews the lock file diff and checks that only the intended packages and their direct dependencies changed, and that each new version matches a release supporting the target core. Unexpected changes to unrelated packages are a stop signal, and the team returns to the previous step with narrower package names. The reviewer confirms the diff is reproducible by running composer install from the new lock file on a clean checkout. The step is done when the review is signed off and the clean install succeeds.

  7. Run the test suite and site smoke tests

    The team runs the automated test suite on the branch, then runs smoke tests against staging covering login, content editing, key templates, search and any integrations with identity or payment providers. Failures identify whether the cause is a core deprecation, a contributed module incompatibility or a custom module, and the fix goes back into the same branch. The rollback point is the previous container image together with the tagged lock file. The gate is a green test run and smoke checklist; any failing check blocks promotion.

  8. Deploy the same lock file to staging and production

    The release engineer merges the reviewed branch and deploys the identical composer.lock to staging, then to production, using the same build artifact or image for both environments. Deploying the same lock file, rather than running composer update again in each environment, is what keeps the two sites reproducible. After production deployment, the team checks the Drupal status report and logs, and keeps the database snapshot and previous image until those checks pass. The rollback point is the previous image and lock file.

Keeping the lock file reproducible across developers, CI and production

The composer.lock file records the exact version of every package Composer resolved, so it is the single record that makes a build repeatable. Commit it to version control alongside composer.json, and have developers, CI and production install from it with composer install rather than running composer update. An update resolves the version constraints again and can pull newer releases of Drupal core and contributed modules, which means two people running it on the same day may end up with different code. During an upgrade, that drift is hard to diagnose because a failing test may come from a package nobody meant to change.

The lock file is only as reliable as the environment that produces it. A developer who regenerates the lock on a laptop with a different PHP version, a different Composer version or leftover local packages can write resolutions that CI and production never see. Drupal core's PHP requirements change between major versions, so the resolver's view of what is installable depends on the PHP version it runs under. Fix the resolution environment in CI: pin the PHP and Composer versions in the pipeline image, run the lock update only in one controlled job, and fail the build if composer validate or a fresh composer install disagrees with the committed lock. The Drupal.org documentation on managing dependencies with Composer is the starting point for these checks.

Confirm three things with your own team before the first upgrade branch is cut. First, your policy on committing vendor code: some organizations commit vendor/ for deployment speed and audit trails, others build it in CI from the lock file, and each choice changes what a reviewer can see in a merge request. Second, how the Drupal.org project template handles the lock file in the version you use, including whether it is ignored by default and what your team has changed. Third, who reviews dependency updates, how a lock change is checked against the upgrade plan, and whether security-driven updates follow the same path as planned ones. Write these answers into the repository's contribution guide so they are enforced, not remembered.

What to prepare before the first Composer change

Composer will resolve whatever you ask it to, so the upgrade is only as predictable as the state you start from. The five items below give you a known starting point, a safe place to roll back to, and a named person accountable for what changes.

Inventory every contributed project

List each contributed project and each patch declared in composer.json, with the release it currently resolves to in composer.lock. Note which projects are pinned to a dev branch, which use a patch from a Drupal.org issue, and which are required by custom code. This list tells you which constraints Composer must relax and which projects may have no compatible release for the target core version. Confirm each project's supported core range on its Drupal.org page before you start.

Match the test environment to production

Build a staging environment that runs the same PHP version, web server, database engine and database version as production. Target versions matter here: Drupal 11 requires PHP 8.3 or later and Drupal 12 requires PHP 8.5 or later, so a PHP upgrade may need to land before or alongside the Composer change. A test run on a different PHP minor version can produce a lock file that fails in production.

Back up code, database and files

Take a database dump, a copy of the public files directory and private files, and a copy of the current composer.json and composer.lock stored outside the working tree. Verify that the backup restores on a separate host before you rely on it. The lock file backup is the fastest rollback path if a dependency resolves badly, because it lets you return the exact set of package versions that ran in production.

Map custom code to core APIs

List every custom module and theme, and mark which ones call core APIs, extend core plugins, or override core templates. Run Upgrade Status against these before the change so you know which deprecations exist. Custom code that depends on a contributed project will move with that project's version, so record those dependencies too. This map sets the order of work: code that touches many core APIs needs its own review window.

Name an owner for the lock file diff

Assign one person who is accountable for approving the lock file diff before it merges. This person reviews every changed package, checks that no contributed project moved to an unsupported release, and confirms that version changes match the change record. Agree the approver, the review window and the escalation path in writing. A named owner prevents a routine-looking update from reaching production without anyone checking what changed.

What makes a Composer-based Drupal upgrade take longer

This table lists the project conditions that add effort to a Composer-driven core and contributed upgrade, with the reason each one matters and what to request in a quote. Use it to check whether a proposed scope breaks effort down by these drivers or hides them inside one lump sum. The main takeaway is that the same Drupal version can take very different effort depending on how the codebase is pinned, patched and tested.

DriverWhy it adds effortWhat to ask for in a quote
Contributed modules and themes in composer.jsonEach contributed project needs a release compatible with the target core and PHP version, and some will need replacing or updating before the core change can resolve.Effort itemized per contributed project, with a note on which ones have no compatible release yet.
Patches carried in composer.jsonPatched packages may conflict with upstream releases, so each patch must be re-tested, rebased or dropped when the dependency moves.A list of every patch, its upstream issue status and the effort to rebase or retire it.
Custom code that pins a core or library versionHard-coded constraints in custom modules, or direct requirements on a library version, can block Composer from resolving the target release.Review of custom composer.json constraints and the effort to relax or update each one.
Build pipeline that does not reproduce the lock fileIf CI installs without the committed composer.lock or resolves dependencies fresh, builds drift and upgrade failures appear only in some environments.Confirmation that the build installs from the committed lock file and time for pipeline fixes in scope.
PHP and database upgrades before or with the code changeThe target Drupal version sets a minimum PHP version, and server or database upgrades must be sequenced with the Composer change, which adds coordination work.Effort for server and database work listed separately, with the required PHP version for each target release confirmed.
Size of automated and manual test coverageThin tests mean more manual regression checks after each dependency change, and gaps only show up late in the upgrade.Test scope stated as automated suites and manual test cases, with effort for writing missing tests included or excluded explicitly.

Questions teams ask before a Composer-based Drupal upgrade

Can our in-house team run a Composer-based Drupal upgrade on its own?

Yes, many in-house teams can, provided they have someone who knows Composer, Drush and the Drupal codebase well enough to read dependency conflicts and custom code. The Composer step itself is short. The effort sits in resolving contributed module versions, fixing deprecated code in custom modules and themes, and testing. Ask yourself whether the team has time to run the upgrade on a staging copy, repeat it when conflicts appear, and keep the lock file in review. If one of those is missing, a partner can cover that gap while your team keeps ownership of the repository and the release.

What drives the cost of a Composer upgrade, and how does a quote follow from scope?

The main cost drivers are the number of contributed modules, how many of them need a version change, how much custom code must be reviewed, and how many environments must be tested. Costs are one-time for the upgrade itself and recurring if you retain ongoing updates. DeployOpen does not publish a price list. A fixed-scope quote follows from a written scope that lists the modules in the composer.json file, the custom modules and themes in scope, the environments to upgrade, and the deliverables and acceptance tests. Ask for the quote to show each of these items separately so you can see what changes if the scope changes.

How long does a Composer upgrade take?

Duration depends mostly on the number of contributed modules that need a new release, the amount of custom code that uses deprecated APIs, and the size of your test plan. A site with few contributed dependencies and a clean custom codebase takes far less time than one with many dependencies that have no release for the target core. Upgrade Status and a dry run on a copy of production will show the scale of the work early. Ask for a timeline that is tied to these counts and to your approval windows, and treat any date given before that audit as provisional.

What does the client need to provide for the upgrade?

The upgrade needs four things from your side. First, access to the Git repository that holds composer.json and composer.lock, with permission to create a branch. Second, a staging environment that matches production in PHP version, database and web server, so test results carry over. Third, realistic test data or a recent sanitized copy of the production database and files. Fourth, a named approver who can accept the upgrade against the agreed test cases and decide on trade-offs, such as dropping a module that has no compatible release. Agree these owners and dates in the written scope before work starts.

Should we use drupal/core-recommended or drupal/core?

Use drupal/core-recommended if you want Composer to pin a set of versions of dependencies that Drupal's maintainers have tested together, which reduces conflicts with packages such as Symfony components. Use drupal/core if you need tighter control over those dependencies and accept that you will manage their versions yourself. Drupal.org's Composer documentation covers both options and how to move between them. Choose one based on how your team handles dependency updates, record the decision in the repository, and keep it consistent across environments so the lock file stays reproducible.

What should we do when a contributed module has no release for the target core?

First confirm on the module's Drupal.org project page whether a release or development branch supports the target version. If none exists, you have three practical options: wait for a maintainer release, replace the module with a supported alternative, or keep the module by moving it into custom code or sponsoring the maintainer's work. Each option changes the cost and the risk. Do not force a constraint in composer.json to make the install pass, because that hides the incompatibility until a later update fails. Record the decision, the owner and the date in the upgrade plan, and retest the affected features.

How DeployOpen scopes, delivers and supports a Composer upgrade

DeployOpen plans and implements Drupal sites inside the client's own infrastructure, and Composer changes for an upgrade are handled as a written, fixed-scope piece of work. The engagement covers the Composer project template, contributed module constraints, the lock file and the handover to the team that will run the site.

How we scope

  • Inventory the current composer.json, including whether the site uses drupal/recommended-project or a core-only setup.
  • List every contributed module and theme with its current constraint and whether a compatible release exists for the target version.
  • Agree in writing which repositories, branches and environments the Composer changes will touch and who approves each step.

How we build

  • Make Composer changes on a branch, moving between drupal/core-recommended and drupal/core only as the agreed plan requires.
  • Resolve contributed module conflicts with documented constraint changes, replacements or patches, and record each decision in the change log.
  • Commit the regenerated lock file, then run the build in a clean environment to confirm the same versions install every time.

How we hand over

  • Deliver the final composer.json, composer.lock and a short runbook covering the update commands, approval steps and rollback path.
  • Walk your developers through the dependency decisions, including which packages are pinned and why.
  • Document the test results from the staging build so your team can repeat the process for later releases.

How we support

  • Engagements are fixed-scope, with scope, deliverables and timelines agreed in writing before work starts.
  • Ongoing support, upgrades and continuous testing are available as a retainer, which can cover later Composer updates and security releases.
  • Ask for the support terms, response expectations and handover acceptance criteria to be written into the quote before work begins.

Sources and further reading

Talk to our team.

Tell us what you're working on, whether it's a deployment, an audit, a security test or a cyber range. You'll speak with an engineer who can help you scope it.

  • 30-minute call: free, with no obligation.
  • NDA on request: we can sign before you share details.
  • Clear next steps: a scope and plan after the call.