Caching and production mode¶
Where a definition comes from¶
| Deploy mode | Source | Invalidate with |
|---|---|---|
| developer | The JSON files, merged on every request | nothing to do |
| default | Magento's default cache frontend, keyed by type, id and the ordered list of enabled modules, every entry tagged NEBULA_DEFINITIONS |
bin/magento cache:clean nebula_definitions |
| production | generated/nebula/definitions.php only |
bin/magento nebula:compile |
In default mode, enabling or disabling any module changes the key and invalidates every entry without a manual clean. In production mode, a definition that is not in the manifest is logged once and resolved through the cached live path instead of throwing; bin/magento nebula:compile stays the deploy step, because that fallback is slower and warns on every request until the manifest exists.
cache:disable nebula_definitions does not turn definition caching off
The resolver writes through Magento's default cache frontend and tags each entry NEBULA_DEFINITIONS. The nebula_definitions cache type is a tag scope over that tag rather than a store of its own, so disabling the type changes nothing about what is written or read. Disabling it also stops bin/magento cache:clean nebula_definitions doing anything, because Magento short-circuits a clean on a disabled type. To see a live JSON edit in default mode, switch to developer mode, or re-enable the type and clean it, or run bin/magento cache:flush.
On installs that map cache types to separate backends in app/etc/env.php, leave nebula_definitions on the default frontend. If cache/type/nebula_definitions/frontend points elsewhere, the clean targets that frontend while the writes went to the default one, so nothing is ever invalidated.
The compiled manifest¶
bin/magento nebula:compile discovers every view/adminhtml/{grid,form,snippet,component}/*.json across enabled modules, resolves each one exactly as a request would, validates the merged result against its schema, and writes a deterministic PHP array keyed type:id. setup:di:compile does not produce it — the plugin earlier releases carried on that command could never fire, because setup commands are built by the Laminas service manager, and it has been removed — so a production deploy runs nebula:compile itself, after setup:upgrade. A deploy that forgets no longer takes the admin down: production mode warns once per definition and serves the cached live path, which is default-mode behaviour with a log line per request until the command runs.
bin/magento nebula:compile # write the manifest
bin/magento nebula:compile --verify # compile in memory and compare; exit non-zero on drift
bin/magento nebula:compile --only=grid:product_listing
--verify is the CI form: it proves that the committed manifest, if you commit one, matches the definitions. It exits non-zero on drift, so a deploy can assert the manifest before it opens the site. Whether to commit generated/nebula/definitions.php follows whatever you do with the rest of generated/: build it in the pipeline, or commit it with the build artefacts.
The manifest is an ordinary PHP file. nebula:compile writes it with file_put_contents, and in production mode the resolver loads it with require on the first definition of a request, then holds it in a static for the rest of that process.
Reset the opcache after compiling
bin/magento cache:flush cleans Magento's cache backends and does not touch the manifest, and nothing in Nebula invalidates the opcache entry after writing the file. Under a typical production OPcache with opcache.validate_timestamps=0, a manifest rewritten at a path PHP has already compiled keeps serving the previous array. Reset the opcache or reload PHP-FPM after nebula:compile:
A release-directory deploy escapes this only when every release compiles the manifest under a path PHP has not seen before. Where generated/ is a shared directory symlinked into each release, which is a common Magento layout, the path is the same every time and the reset is still required.
The layout cache¶
Route-mounted forms add nebula_form_<id> to the layout cache key, so two forms sharing one route stay apart. After adding or changing route, replaces or routeWhen on a definition, clean the layout and block cache:
Skins and configuration¶
A skin's CSS is built from the JSON on each request, so a skin file you add or edit is live immediately. The selected skin is read from nebula/theme/skin, which the configuration cache holds, so after changing that setting:
Static content¶
The theme's CSS and JavaScript are committed and deployed by setup:static-content:deploy like any theme's. In developer mode they are served on demand. A module's own CSS and JS files under view/adminhtml/web deploy the same way.
A production deploy, in order¶
On a single node, deploying in place:
composer install --no-dev --prefer-dist
bin/magento maintenance:enable
bin/magento setup:upgrade --keep-generated
bin/magento setup:di:compile # also writes generated/nebula/definitions.php
bin/magento setup:static-content:deploy -f
bin/magento nebula:compile # writes generated/nebula/definitions.php
bin/magento nebula:compile --verify # confirms the manifest matches the definitions
bin/magento cache:flush
cachetool opcache:reset # or reload PHP-FPM
bin/magento maintenance:disable
Build phase and release phase¶
A pipeline that builds on one host and swaps a release directory on another splits that list in two.
Build phase, shipped inside the release directory: pub/static and the whole of generated/, including generated/nebula/definitions.php. Run nebula:compile on the build server after setup:di:compile, then nebula:compile --verify to prove the manifest you are shipping is in sync.
Release phase, run on the live node after the swap and against the live database: bin/magento nebula:media:index, which writes rows in the Nebula media index tables and is database state rather than a build artefact. It is never shipped, and it has to run again whenever files land in pub/media from outside the admin. Then the cache cleans the change calls for, and the opcache reset or PHP-FPM reload.
The manifest must live inside the release directory
The compiler and the request-time resolver both build the path as <application root>/generated/nebula/definitions.php from DirectoryList::getRoot(), never from Magento's configured generated directory. A MAGE_DIRS remap of generated to a path outside the application root therefore does not move the manifest. A pipeline that ships only the remapped directory ships a release with no manifest, and every admin request throws Nebula compiled manifest is missing at ....
Rolling back reads the same rule backwards. Because the manifest is a file in the release directory, swapping the symlink to the previous release restores that release's manifest with it. A rollback that reverts only the code while keeping a shared or externally mounted generated/ serves the newer release's definitions against the older classes, so re-run nebula:compile on the rolled-back release, or nebula:compile --verify to confirm it does not need it.
What each command needs from its host¶
| Command | Needs |
|---|---|
nebula:compile, nebula:compile --verify |
Module directories and the enabled-module list from app/etc/config.php. No database: it reads only files. generated/ writable, or readable for --verify. Runs on a build container beside setup:di:compile. |
nebula:lint |
The same files, but it runs through bin/magento, so it needs a bootable install. See quality gates. |
nebula:lint:options |
A fully bootstrapped install. It sets the adminhtml area code and builds every option source through the object manager. |
nebula:doctor |
A running store. It emulates the adminhtml area and inspects the renderer pool and the cache type list. Run it on the store, not in the build phase. |
The build server needs production's app/etc/config.php
Both the set and the order of enabled modules feed the manifest. The set decides which definition ids exist, the order decides the order overlays merge in. A build host carrying a different config.php ships a manifest that does not match the store.
Copying a database between environments¶
A production database restored onto a staging host brings the administrators' second factors with it, and neither factor works there.
Passkey credentials are bound to a relying party id, which Nebula derives from the configured admin base URL rather than from the request's Host header. The override at nebula_passkey/general/rp_id has to be the admin host or a parent of it. A copy served from staging.example.com therefore presents a different relying party, so credentials registered on production do not assert against it.
TOTP secrets in admin_totp_secret are encrypted at rest with the install's own crypt/key from app/etc/env.php. On a host with a different key the stored secret does not decrypt, so no code the authenticator app produces ever matches.
Recovery codes are the exception. They are hashed per row with a random salt rather than encrypted, so an administrator who still holds their printed codes can spend one on the copy. That way in exists only where Nebula's own two-step is what asks, which means an install with Magento_TwoFactorAuth disabled.
Otherwise, get into the copy from the shell. On an install where Magento_TwoFactorAuth is enabled, which is the stock case, clear the administrator's provider configuration with Magento's own reset:
Use google instead where the administrator enrolled the authenticator app. Nebula's own two-step stands down whenever Magento_TwoFactorAuth is enabled, so bin/magento nebula:passkey:reset-two-step --user=<admin username> is the command only on an install where that module is disabled. See the CLI reference.
That command clears one administrator's Nebula TOTP secret, their remaining recovery codes and their two-step session. It leaves their registered passkeys and Magento_TwoFactorAuth's own per-user configuration alone, and it never turns the requirement off for the install. Where twofactorauth/general/force_providers still names the providers, the administrator is asked to enrol again on the staging host. Security covers the rest of the two-step story.
Never copy crypt/key into a lower environment
Making the production secrets decrypt on staging hands staging the ability to mint production codes. Reset the administrators on the copy instead.
What to clean after which change¶
| You changed | Clean |
|---|---|
| A grid, form or snippet JSON | nebula_definitions in default mode. In production, nebula:compile followed by an opcache reset |
A route, replaces or routeWhen key |
the above, plus layout block_html |
A di.xml binding |
setup:di:compile in production; config cache otherwise |
| A skin file or the Color Scheme setting | config |
| A template or a module stylesheet | block_html and full_page; redeploy static content in production |