Appearance
Cells and goldens
What a cell is
A cell is a directory under the cell root (default ~/qoliber-furnace, override with FURNACE_CELLS) holding a complete Magento or Mage-OS installation served by MageBox. MageBox runs only the services around it in Docker (MySQL, MariaDB, OpenSearch, Redis, RabbitMQ, Mailpit); the cell itself is plain files on the host, served by nginx and the host's PHP-FPM for its PHP version.
bash
furnace cells # what exists
furnace cells --urls # plus the URLs derived for each store view
furnace catalog # platform/PHP combinations, cell coverage, installed themes and versionsThe fleet today:
| Cell | Platform | PHP | Database |
|---|---|---|---|
m246 | Magento 2.4.6-p14 | 8.2 | MySQL 8.0 |
m247 | Magento 2.4.7-p9 | 8.2 | MariaDB 10.6 |
m247php81 | Magento 2.4.7-p9 | 8.1 | MySQL 8.0 |
m248 | Magento 2.4.8-p4 | 8.4 | MySQL 8.0 |
m249 | Magento 2.4.9 | 8.4 | MariaDB 10.6 |
m249nomsi | Magento 2.4.9, without Multi-Source Inventory | 8.4 | MySQL 8.0 |
m248php83hyva14 | Magento 2.4.8-p5, Hyvä 1.4 | 8.3 | MySQL 8.0 |
mageos1 | Mage-OS 1.3.1 | 8.3 | MySQL 8.0 |
mageos2 | Mage-OS 2.3.0 | 8.3 | MySQL 8.0 |
mageos3 | Mage-OS 3.4.0 | 8.4 | MySQL 8.0 |
mageos3php85 | Mage-OS 3.4.0 | 8.5 | MySQL 8.0 |
Every cell has a default (Luma) and a hyva store view. m249nomsi is the one cell built with --without-msi: the 76 inventory packages are replaced in its root composer.json, so it advertises no msi capability and is the cell that proves an extension runs on the legacy stock. Everything furnace knows about a cell is read from the cell:
| Fact | Read from |
|---|---|
| name, PHP version, hostname | .magebox.yaml |
| database host, port, name, credentials | app/etc/env.php |
| platform and version, theme package versions | composer.lock |
| store views, their themes and inheritance, sample category and product URLs | the cell's own database |
| capabilities (cron, graphql, hyva, multistore, opensearch, rest, sample-data, …) | installed modules and configuration |
There is no manifest to keep in sync. A useful cell has Luma sample data, store codes in URLs, a dedicated integration database named <cell>_integration_tests, developer mode and an empty app/code.
Page cache
Every cell runs with Magento's full page cache enabled, in its built-in form. Varnish is not in the request path, so Varnish-specific behaviour (VCL purging, ESI, the tag header size limit) is not exercised. See status.
Provisioning a cell
bash
scripts/provision-cell.sh <name> <magento|mageos> <version> <php> [db-port]
scripts/provision-cell.sh m247php81 magento 2.4.7-p9 8.1
scripts/provision-cell.sh mageos3 mageos 3.4.0 8.4The script encodes every trap that produced a cell which looked fine and was not: pinning config.platform.php so Composer resolves against the cell's PHP rather than the host's; the esconfig.xsd patch for Magento 2.4.7 and older under libxml 2.12; a narrower exclude-from-classmap so PHPUnit 10 can autoload its own event classes; two-factor authentication disabled on both the cell and the integration install config; the right PHPUnit major for the platform; a per-cell OpenSearch index prefix for the integration framework. It refuses to overwrite an existing cell. Read docs/PROVISIONING.md before building a fleet on a new host.
A cell without Multi-Source Inventory
bash
scripts/provision-cell.sh m249nomsi magento 2.4.9 8.4 --without-msi--without-msi builds the cell the way merchants who drop MSI do: every inventory package (the metapackage, its composer installer and the module-inventory-* modules, listed from the repository for that exact platform) is replaced in the root composer.json before the first install, so vendor never holds the modules and setup:install never enables them. The script asserts on state afterwards: zero inventory directories in vendor, and Magento_InventoryApi absent from config.php. Such a cell advertises no msi capability; see inventory selection for how a manifest asks for it.
Hyvä lines
One installation holds one Hyvä version, so each supported line has a cell of its own, placed where it also fills a PHP tuple the policy needs:
| Cell | Platform | PHP | Hyvä | State |
|---|---|---|---|---|
m246php81hyva12 | Magento 2.4.6-p15 | 8.1 | 1.2 (latest 1.2.9) | planned: see below |
m247php83hyva13 | Magento 2.4.7-p10 | 8.3 | 1.3 (Tailwind 3, latest 1.3.23) | planned: see below |
m248php83hyva14 | Magento 2.4.8-p5 | 8.3 | 1.4 (Tailwind 4, 1.4.11 installed) | live since 2026-10-28 |
| every other cell | 1.5 | live |
Composer refuses the two older lines
Composer 2.9 blocks packages with a published security advisory, and Magento 2.4.6 and 2.4.7 require league/flysystem 2.x, which carries one. The first attempt at both cells stopped at composer install for that reason. A cell has to install what the platform release ships, so building them needs the block relaxed for that one package in the cell's composer.json (policy.advisories), which is a deliberate decision rather than something the script does on its own.
bash
FURNACE_HYVA='~1.3.0' scripts/provision-cell.sh m247php83hyva13 magento 2.4.7-p10 8.3FURNACE_HYVA pins the line. The CSP variant of the default theme exists only from 1.5, so older lines get the default theme alone; the script falls back by itself. A manifest asks for a line with a versioned theme target; see theme selection.
Keeping an instance out of the fleet
Any installed MageBox instance under the cell root is a cell. For one you develop in, with modules symlinked into app/code, create an empty .furnace-ignore file in its directory. No furnace command will discover it until the file is deleted.
Which cells are needed
config/platforms.yaml is the one deliberate policy file: the vendor-supported platform and PHP tuples that must exist, with the newest patch worth certifying against. It never configures a cell; furnace plan compares it with the facts the cells report and shows what is ready, behind (right release line, older patch) or missing. Override it per invocation with FURNACE_CATALOG.
Goldens
Resetting a cell by reinstalling Magento would take minutes. Instead each cell has a golden snapshot: a gzipped dump plus the four files that carry a cell's identity (composer.json, composer.lock, app/etc/config.php, app/etc/env.php). Restoring takes about ten seconds, which is what makes a pristine reset before and after every run affordable. Snapshots live in ~/.furnace/golden/<cell> (override with FURNACE_SNAPSHOTS).
bash
furnace golden # what exists, size, age
furnace golden --capture # capture every healthy cell
furnace golden --capture -c m248 # one cell
furnace golden --restore -c m248 # put a cell backCapture refuses a cell that fails its health checks, because every later run is restored back to whatever you capture. The checks include golden purity (no leftover extension tables or setup rows, no third-party packages in the root composer.json, no module enabled in config.php without code behind it, developer mode) and golden coherent (below). --force exists and is almost never right.
Capture after changing a cell
The snapshot restores composer.json and composer.lock. Anything installed afterwards is reverted on the next reset, which is how PHPUnit once silently disappeared from two cells. Change a cell, then recapture.
Success is measured by counting CREATE TABLE statements in the dump, never by file size: an empty gzip is 20 valid bytes. A corrupt dump is rejected before the database is dropped. The restore itself is DROP DATABASE, CREATE DATABASE, load: a dump only carries DROP TABLE for tables it contains, so anything an extension created survived a load-over-the-top. Restores hold a lock keyed on the database server, because cells share servers and two concurrent restores deadlock.
Coherence
A snapshot restores composer metadata but leaves vendor/ alone. If vendor/ is rebuilt after a capture, restoring the golden produces a cell whose metadata describes one dependency graph while its autoloader holds another, and every gate afterwards measures that instead of the extension.
Each golden therefore records a fingerprint of the dependency graph it was captured against, and a run refuses to start on a cell that no longer matches:
✗ reset Refusing to restore m248: the golden no longer describes this cell's
vendor/ (installed packages differ from the snapshot).
Recapture it: furnace golden --capture -c m248The fingerprint hashes the set of packages and versions, not the file. Composer rewrites installed.json on any dump-autoload without changing a dependency, and hashing the bytes reported that as drift.
furnace init recaptures a stale golden automatically when nothing else about the cell is wrong: the cell's vendor/ is the truth and the snapshot is what is out of date.
Getting the fleet ready
bash
furnace init # repair what can be repaired, then verify every cell
furnace init --deep # and prove each cell can actually run an integration test (~1 min per cell)
furnace init --recapture # and refresh the goldens of every cell that ends up healthy
furnace doctor # verify without repairing; exits non-zero on any failing cell
furnace doctor --deepRun init after a reboot, after an interrupted run, after changing anything about a cell, and before a certification matrix. A red cell is a blocker: a failure produced by a broken cell tells you nothing about your extension.
| Doctor check | Fails when |
|---|---|
database | the cell's database cannot be reached |
platform purity | a Mage-OS tree sits inside a Magento cell or the reverse |
app/code clean | modules furnace did not put there (warns; the next run purges them) |
golden coherent | vendor/ no longer matches the snapshot's package fingerprint |
maintenance | var/.maintenance.flag is present, which no database restore lifts |
test services | a service the integration config declares (search, AMQP) does not answer |
integration db | <cell>_integration_tests does not exist |
mode | the cell is not in developer mode |
golden purity | extension residue in tables, setup rows, root requires or config.php |
registries | a composer repository the cell declares refuses the credentials composer would send, or has none to send; a registry that is down is a warning |
phpunit | wrong major for the platform, or an extension class in phpunit.xml that will not autoload |
catalog, settings | no sample products; rewrites, store codes or timezone unset |
search index | catalogsearch_fulltext invalid, or an unpatched esconfig.xsd |
rewrites[view], renders[view] | a store view has no category rewrites, or a real category URL serves no products |
Registry credentials
Cells install packages from registries (Mage-OS, Hyvä, the vendor's private Packagist). Credentials are composer's own, read exactly where composer reads them: the cell's auth.json, the global auth.json under composer's home, or a COMPOSER_AUTH variable. Furnace stores none.
furnace doctor proves every declared repository answers with the credentials composer would send. furnace init brings a cell's auth.json up to date from the global one, but only for registries the cell declares, and only by replacing a credential the registry refuses with one it accepts. A stale global credential is reported as stale and never copied, so rotating a token is a one-place change and a stale global file cannot break a cell.
The integration sandbox
Magento's integration framework builds its own sandbox under dev/tests/integration/tmp/ and reuses it. That sandbox is derived from the module set present when it was built, so reusing it under a different one leaves the compiled DI describing classes the sandbox does not have; PHPUnit then dies in bootstrap and reports zero tests.
Furnace records which modules a sandbox was built for. The same extension reuses it; a different one rebuilds. Clearing it unconditionally instead was correct and cost an extra six minutes per cell.