Skip to content

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:

cachetool opcache:reset
# or: systemctl reload php-fpm

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:

bin/magento cache:clean layout block_html

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:

bin/magento cache:clean config

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:

bin/magento security:tfa:reset <admin username> nebula_passkey

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