Skip to content

Running modes: default, swapped engine, real Fabric

The emulator runs in a handful of shapes. This page is the map: what each one starts, how to confirm it actually works, and when you would want it. Every mode has a make target, so the commands are identical on Linux, macOS and Windows.

If you only want to get going, the quickstart walks mode 1 end to end. Come back here when you need to change something.

I want to work on…CommandServicesIdleGive the runtime
control plane, OneLake, RBAC, git, CI/CDmake up-lite3~65 MB2 GB
…plus Spark and the T-SQL warehousedocker compose up6~1 GB8 GB
…plus catalog, lineage, glossary, Airflowmake up12~3 GB13 GB
…plus KQL (Eventhouse)add --profile rti13+4 GB+4 GB
…plus Eventstream (Kafka, Lakehouse/Reflex dest)add --profile eventstream and -f docker-compose.eventstream.yml+1+400 MB+400 MB
…plus a shell in the Flow viewadd --profile terminal14+5 MB+5 MB
Spark features Sail lacks (RDD, JVM UDFs)make up-jvm6~1 GB10 GB
full DAX against msmdsrv (optional oracle)a machine you own — 52+Windows guest8–16 GB; not make up; not macos-latest / ubuntu-latest
the real Fabric serviceFABRIC_TARGET=real0

Idle is not the number that matters, and it is nowhere near the other one. A freshly booted lite stack is 65 MB; the same three containers writing Delta are a few hundred. Sail costs 36 MB to start and ~1.9 GB to run PySpark through. So the column to size against is the last one — and the reason it is so much larger is the work, not the container count. Full measurements below.

ModeSparkT-SQLExtras
1. Default — make upSailSQL ServerOpenMetadata, Airflow
2. Plain compose — docker compose upSailSQL Server
3. Lite — make up-lite❌ 501❌ 501
4. JVM overlay — make up-jvmJVM SparkSQL Server
5. Real Fabric — FABRIC_TARGET=realthe real service

Whatever you start, make status is the verdict. make up only means containers were created; status probes the endpoints and reports stack OK or names what is broken — including the failure no health check catches, a container that is up but attached to no network.

The everyday mode, and the one the parity map grades against.

Terminal window
make doctor # toolchain, docker context, memory, ports — run this first
make up
make status

You get the family (entra-emulator, fabric-emulator, azure-keyvault-emulator) plus real compute: Sail as the Spark engine behind the Livy surface, and a SQL Server sidecar behind the T-SQL/TDS warehouse. Livy sessions, notebook cells, and warehouse queries all do real work — nothing to attach.

make up also brings up the governance profile (OpenMetadata and its Postgres/OpenSearch) and Airflow, because the quickstart advertises them. That is twelve containers. To skip them:

Terminal window
make up PROFILE= # same stack, no OpenMetadata

Worth knowing that this is not the same as make up:

Terminal window
docker compose up # 6 services — no governance / airflow profiles
make up # 12 services — adds OpenMetadata + Airflow

Compose auto-loads docker-compose.override.yml, which is what attaches Sail and SQL Server. That is why engines are opt-out, not opt-in — a distinction the parity map’s 🟠 mark now turns on.

The contract-only pair: control plane, OneLake, identity. No compute sidecars, so the Spark and T-SQL surfaces answer an honest 501 instead of pretending.

Terminal window
make up-lite

Reach for it when you are testing the control plane, CI/CD, git integration or RBAC and do not want to wait for engines you will not use. Naming -f explicitly is what makes Compose skip the auto-override:

Terminal window
docker compose -f docker-compose.yml up -d # what the target runs

Swaps the default Sail engine for JVM Spark 3.5.5 (Java 11, Delta 3.2) — the same engine real Fabric Runtime 1.3 uses, so it is the higher-fidelity option.

Terminal window
make up-jvm

Note this swaps rather than adds: the sail service is gone and the statement agent becomes a classic in-process Spark session.

Reach for it when your test touches the RDD/SparkContext API, a checkpointed streaming query (foreachBatch on an engine stream, or resume from checkpointLocation), Java/Scala UDFs, spark.jars, or a CDF-enabled table you need Spark itself to author. Durable streaming sinks (delta/parquet/memory), OSS format("kafka") on Sail, and the Fabric Eventstream notebook API land one announced micro-batch on the default Livy path. The ❌ rows that stay red are in the engine matrix, which measures both engines with the same probes rather than asserting.

What it costs, measured on the same 19 probes:

Sail (default)JVM overlay
Image size943 MB2.1 GB
Run output125 log lines78,040 log lines
Startupsecondsminutes

It is not the default because most tests never touch those capabilities, and the common notebook path — Delta write/append, both time-travel forms, createDataFrame, SQL, readStream — passes on both.

Profiles — extra services, only when asked

Section titled “Profiles — extra services, only when asked”

A profile adds services rather than swapping them, and nothing is pulled unless you name it. --profile is repeatable, so combine them freely. governance and airflow are already in make up’s default PROFILE; overriding PROFILE replaces that default rather than adding to it, so name them again if you still want them.

ProfileAddsGives youCosts
governanceOpenMetadata + Postgres + OpenSearchcatalog, glossary, lineage over the state your pipelines wrote (22)~2.8 GB
airflowapache/airflow scheduler + webserverApacheAirflowJob items run on genuine Airflow (14)~1.1 GB
rtikustainerMicrosoft’s own KQL engine behind Eventhouse (25)4 GB (its own mem_limit)
eventstreamkafka (apache/kafka KRaft)Fabric Eventstream notebook API, Custom HTTP produce, Lakehouse Delta dest, Reflex job dest (51)~400 MB
terminalttyda shell in the Flow view, beside the graph (31)negligible
Terminal window
make up PROFILE="--profile rti" # KQL only — drops governance and airflow
make up PROFILE="--profile governance --profile airflow --profile rti"

--profile rti needs amd64 with AVX2. Microsoft documents ARM as unsupported and Rosetta does not supply AVX2 — on Apple silicon it needs an x86-64 VM with a QEMU CPU type that provides it.

There is no --profile dax. Analysis Services is a Windows process. macOS uses UTM on the host you own; Linux uses make dax-linux (Docker Engine + /dev/kvm on the metal); Windows runs Desktop on the machine. Those are not GitHub macos-latest / ubuntu-latest — those runners are already VMs, and nested Windows is not a CI job. The CI oracle is e2e/pbix-desktop on windows-latest. OrbStack and Rancher Desktop on a Mac cannot pass KVM into dockur/windows. The guest then runs pwsh e2e/msmdsrv/start.ps1 and the emulator sets FABRIC_DAX_URL. See 52-msmdsrv-hosts.md.

The profile starts Kafka; a second file tells the emulator where it is. Both, or Spark resolution 501s. Works on Sail (the default docker compose up engine) without the JVM overlay:

Terminal window
docker compose --profile eventstream \
-f docker-compose.yml -f docker-compose.override.yml \
-f docker-compose.eventstream.yml \
up -d

Add -f docker-compose.spark-jvm.yml when you want the native OSS Kafka source instead of the Sail LocalRelation wrap.

See 51-eventstream-kafka.md.

The profile starts ttyd; a second file tells the emulator where it is. Both, or the pane never appears:

Terminal window
docker compose --profile terminal \
-f docker-compose.yml -f docker-compose.override.yml -f docker-compose.terminal.yml \
up -d

Then read the token the emulator printed at startup (docker compose logs fabric-emulator | grep terminal), open https://localhost:9443/#flow, click Terminal and paste it. The token is deliberately not served by any endpoint — the portal is unauthenticated, so an endpoint handing it out would be the same as having none.

Naming any -f disables the auto-loaded override. That is why all three files are listed above. Leave docker-compose.override.yml out and the stack still starts — without Sail, the Spark agent or SQL Server, so Livy and the warehouse answer 501 while everything looks healthy.

Nothing cheap catches that: docker compose ps shows only what you asked for, and plain make status reports containers and endpoints without asserting which services should exist, so it prints stack OK. make status-spark opens a real Livy session and is the check that fails.

Every engine and every profile — what CI’s heaviest legs approximate, and the most a laptop will be asked for:

Terminal window
docker compose --profile governance --profile rti --profile terminal \
-f docker-compose.yml -f docker-compose.override.yml -f docker-compose.terminal.yml \
up -d

Thirteen services, and the most a laptop will be asked for.

Measured with docker stats --no-stream on a 16 GB machine, idle and then under an active medallion. Two columns because the difference is the whole point: the emulator is nothing at rest and grows only while it is doing work.

ContainerIdleUnder a running medallion
fabric-emulator5 MiB0.9 – 1.3 GiB
entra-emulator8 MiB38 MiB
keyvault-emulator4 MiB7 MiB
sail (Spark Connect)50 MiB1.8 GiB
spark-agent88 MiB95 MiB
sqlserver (warehouse)880 MiB fresh, 380 MiB once trimmed1.6 GiB
om-opensearch1.0 GiB1.5 GiB
openmetadata940 MiB940 MiB
om-postgresql8 MiB100 MiB
airflow (2.10.5)1.09 GiB1.1 GiB+
ttyda few MiBa few MiB
kustainer (RTI)not measured — amd64/AVX2 only

Which gives, per mode:

ModeServicesIdleUnder load
make up-lite3 — control plane, OneLake, portal~20 MiB~1 GiB
docker compose up6 — adds Sail, spark-agent, SQL Server~530 MiB~4.5 GiB
make up (the default)10 — adds the catalog and Airflow~3.6 GiB~7.5 GiB

make up is the heavy one on purpose: PROFILE ?= --profile governance --profile airflow attaches every real runtime, because each backs a first-class Fabric item type and a capability that answers “not configured” looks broken rather than optional. Drop what you do not need:

Terminal window
make up PROFILE="--profile governance" # no Airflow (-1.1 GiB)
make up-jupyter # + JupyterLab on :8888 (docs/44)
make up PROFILE= # no catalog, no Airflow (-3 GiB)
make up-lite # contract only

CPU is not the constraint, and that is worth knowing before you size a VM. Peak during a light run — a Delta write via delta-rs plus two Copy activities — was sqlserver 18%, sail 12%, fabric-emulator 1.2%. Those are fractions of a single core. Four cores is ample for control-plane and pipeline work; six to eight if you drive PySpark or warehouse queries in anger. One caveat for Apple silicon: sqlserver is the only amd64 container in the stack, so it runs under Rosetta and costs more CPU there than these figures suggest.

Three things worth taking from that:

  • The emulator itself is not the cost. At rest it is ~5 MiB against a 22 MB image — a single static Go binary with the portal embedded. What costs memory is the real engines it attaches: a JVM-free Sail, a real SQL Server, a real OpenSearch. That is the trade the project makes on purpose, and 14-real-compute.md is the argument for it.
  • It is not free under load. 5 MiB idle becomes ~1 GiB while a medallion runs, because Delta writes and Copy activities pass through the emulator’s own storage layer. Size the box for the work, not for the idle screenshot.
  • Sail is why the Spark tier is affordable at all. 50 MiB idle where a JVM Spark would be 1–2 GiB, and a 943 MB image against 2.1 GB (the table above).

01-quickstart.md asks for 8 GB of runtime memory, which covers everything here with room for the containers under test beside it. A control-plane-only loop needs a fraction of that.

To re-derive any of this:

Terminal window
docker stats --no-stream --format '{{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}'

5. Real Fabric — one environment variable

Section titled “5. Real Fabric — one environment variable”

The same Python code runs against the real service with no edits. This is a client-side switch, not a compose mode, so nothing above applies:

Terminal window
pip install ./python/fabric-target
export FABRIC_TARGET=emulator # local — the default, zero config
python my_pipeline.py
az login # real: your own identity…
export FABRIC_TARGET=real
export FABRIC_WORKSPACE=my-workspace-name # …scoped to one workspace, always
python my_pipeline.py # same code
from fabric_target import target
t = target()
ws = t.workspace("analytics") # names, not GUIDs — they differ per target
s = t.session() # authed, TLS-aware, 429-honouring
s.post(f"/workspaces/{ws.id}/items", json={"displayName": "nb", "type": "Notebook"})

Two guardrails are deliberate. Real mode refuses to start without a credential source (az login or AZURE_*) and never falls back to the seeded dev values — so a misconfigured run fails instead of silently testing the emulator. And it is always scoped to one workspace, so a destructive test cannot wander across a tenant.

Non-Python tools get the same switch as environment variables:

Terminal window
eval "$(python -m fabric_target env real)"

Design and phasing: 21-real-fabric-toggle.md. The conformance suite runs the same tests against both targets — the emulator leg on every push, the real leg behind a secret-gated workflow.

Compose reuses containers by project name, so switch cleanly rather than layering one mode on another:

Terminal window
make down # stop, keep the data volumes
make clean # stop AND delete the volumes — full reset
make up-jvm # then start the mode you want

If something looks wrong after a switch, make status first: a stale container from a previous mode is the usual cause, and it names that rather than leaving you to infer it from a health column.