Configuration in a Deploy
config.php is committed and locks the admin, env.php holds the secrets, and app:config:import is what makes either take effect.
bin/magento app:config:import [-n|--no-interaction]A release should not need anyone to open the admin and retype settings. Magento supports that with two deployment configuration files. app/etc/config.php is shared configuration and belongs in source control: enabled modules (1 or 0), websites, store groups and store views, registered themes, and non-sensitive system values such as the JS and CSS merge and minify flags. app/etc/env.php is per machine and stays out of source control: database and cache hosts, the encryption key, MAGE_MODE, and secrets.
Dump once, import everywhere
bin/magento app:config:dump scopes themes system writes the current database configuration into those files. Commit config.php. On each target machine, bin/magento app:config:import loads the files into that machine's database; -n (long form --no-interaction) skips the confirmation in a script. setup:upgrade runs the same import for you, so a release with schema changes needs only the one command.
Locked, and the one thing that must not be in the file
Any path present in these files is read-only in the admin: the field greys out, which is the point of dumping it. Secrets are the exception that must never travel in the committed file. Set those with bin/magento config:sensitive:set, which writes them to env.php instead. On a non-production box, static_content_on_demand_in_production in env.php lets a production-mode server build static files on first request and skip the long deploy step; leave it off on a real store, where the first visitor would pay for it.
Example
<?php
// app/etc/config.php - written by app:config:dump, committed to source control
return [
'modules' => [
'Magento_Store' => 1,
'Vendor_Module' => 1,
'Magento_InventoryAdminUi' => 0,
],
'scopes' => [
'websites' => [
'base' => [
'website_id' => '1',
'code' => 'base',
'name' => 'Main Website',
'default_group_id' => '1',
],
],
// 'groups' (store groups) and 'stores' (store views) follow the same shape
],
'system' => [
'default' => [
'dev' => [
'js' => ['merge_files' => '1', 'minify_files' => '1'],
'css' => ['merge_css_files' => '1', 'minify_files' => '1'],
],
],
],
'themes' => [
// one entry per registered theme, written by the themes config type
],
];When to use it
- Shipping a new store view without anyone clicking through the admin on production.
- Keeping payment credentials out of git while still deploying the rest of the configuration.
- Explaining why a configuration field in the admin is greyed out and cannot be edited.
- Cutting deploy time on a review environment by building static files on demand.
More examples
Declare one of your own paths sensitive
TypePool is how a module tells Magento which of its configuration paths are secret and which are machine-specific. A sensitive path is never written to config.php by app:config:dump; you set it per machine with bin/magento config:sensitive:set vendor_module/api/secret '...', which writes to env.php. In developer mode run bin/magento cache:flush after editing di.xml; on a production box the change arrives through the build, because di.xml is only read by setup:di:compile.
<?xml version="1.0"?>
<!-- app/code/Vendor/Module/etc/di.xml -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
<type name="Magento\Config\Model\Config\TypePool">
<arguments>
<argument name="sensitive" xsi:type="array">
<item name="vendor_module/api/secret" xsi:type="string">1</item>
</argument>
<argument name="environment" xsi:type="array">
<item name="vendor_module/api/endpoint" xsi:type="string">1</item>
</argument>
</arguments>
</type>
</config>The per-machine half of the configuration
Values under 'system' were written by config:sensitive:set or by config:set with -e (long form --lock-env), and they are read-only in the admin. The sibling option -c (--lock-config) writes to config.php instead and wins if you pass both. static_content_on_demand_in_production makes a production-mode box generate static files when they are first requested, which removes the longest step from a staging deploy. On Adobe Commerce on cloud infrastructure, the SCD_ON_DEMAND variable in .magento.env.yaml is what writes this key.
<?php
// app/etc/env.php (abbreviated) - never in source control
return [
'MAGE_MODE' => 'production',
'system' => [
'default' => [
'vendor_module' => [
'api' => ['secret' => '0:3:encrypted-value-here'],
],
],
],
// review and staging boxes only
'static_content_on_demand_in_production' => 1,
];Where the import sits in a release
setup:upgrade finds and runs the config import command, so you do not need both; app:config:import on its own is for releases that change configuration and nothing else. Note what the import does not do: it ignores values supplied as environment variables. On multi-node blue/green deploys the hash check that produces the 'configuration file has changed' error can be suppressed with deployment/blue_green/enabled in env.php, which is the only supported way to silence it.
# on the box where configuration is authored, then commit the file
bin/magento app:config:dump scopes themes system
git add app/etc/config.php
# on every target box, inside the maintenance window
bin/magento setup:upgrade --keep-generated # runs app:config:import itself
bin/magento cache:flush
# configuration-only release, no schema change
bin/magento app:config:import -n
bin/magento cache:flush
Discussion