CI and containers
GitHub Actions
Section titled “GitHub Actions”fireemu exec returns your test command’s exit status, so a CI job needs no separate start and stop steps:
jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 22 - run: npm ci - run: npx fireemu exec -- npm testFireemu needs no Java and no download at startup: the npm package installs a native binary for the runner’s platform.
To also check what the development defaults let through, such as unsigned test tokens, add a job that runs with a second configuration file that sets productionExact, for example fireemu.production-exact.toml:
# Configuration schema version: 1.schemaVersion = 1# Compatibility profile: "strict" or "emulator".profile = "strict"# Use production behavior for development defaults: true or false; requires strict.productionExact = true# Path to Firebase project configuration, relative to this file; a non-empty string.firebaseJson = "firebase.json"{ "schemaVersion": 1, "profile": "strict", "productionExact": true, "firebaseJson": "firebase.json"}npx fireemu exec --config fireemu.production-exact.toml -- npm testWith the JSON form, name the file fireemu.production-exact.json.
Container jobs
Section titled “Container jobs”In a GitHub Actions container: job, or any container that runs as root with a bind-mounted checkout, the checkout is owned by a host user that the container’s root does not trust, so a discovered fireemu.toml or firebase.json is skipped with a warning. Name the files explicitly, or chown the checkout:
npx fireemu exec --config fireemu.toml --firebase-json firebase.json -- npm testSee Configuration files.
Serve the emulators from a container
Section titled “Serve the emulators from a container”By default every listener is on loopback, which a published container port cannot reach. To serve the emulators to the host or to other containers, opt in with network.allowNonLoopbackHosts in fireemu.toml, and give the product emulators a host in firebase.json:
# Configuration schema version: 1.schemaVersion = 1# Path to Firebase project configuration, relative to this file; a non-empty string.firebaseJson = "firebase.json"
[network]# Allow product emulators to bind non-loopback hosts: true or false.allowNonLoopbackHosts = true{ "schemaVersion": 1, "firebaseJson": "firebase.json", "network": { "allowNonLoopbackHosts": true }}{ "emulators": { "firestore": { "host": "0.0.0.0", "port": 8080 }, "auth": { "host": "0.0.0.0", "port": 9099 } }}- Only Firestore, Authentication, Storage, Functions, Eventarc, Cloud Tasks, Pub/Sub and the Realtime Database may listen off loopback. The Hub, the UI and the Logging emulator stay on loopback. Authentication, whose port also serves the control API, listens on loopback or on every interface (
0.0.0.0or::), not on one other address. - The emulators serve without a credential, so startup prints a
warning:line for each listener off loopback. Any client that reaches a published port can do what a local client can on the product APIs, including the Admin SDK’sBearer ownercredential and the Auth emulator’s own routes. For the Realtime Database this includesdatabase.defaultAccess = "locked", which admits that well-knownownercredential, so it is no access control off loopback. Under theemulatorprofile any bearer token is a Realtime Database administrator for data requests, and its/.settings/rules.json, like the Firestore and Storage rules routes, answers loopback peers only. Publish the ports only to a network you trust. - Fireemu’s own controls (the control API, App Check debug-token management, and the routes that load Firestore, Storage and Realtime Database Security Rules or clear Firestore data) answer loopback peers only and return 403 to any other peer (
@firebase/rules-unit-testingcalls the rules and clear routes throughloadFirestoreRules,loadStorageRulesandclearFirestore). Run code that uses them inside the container, or in a container that shares its network namespace, such as a Podman pod or--network container:<name>. - Because a forwarder inside that namespace (
kubectl port-forward, a sidecar proxy) connects from loopback, every control API request also needsAuthorization: Bearer <control token>once the option is on, from a process as from a page; only/health/is exempt. The routes that load Security Rules or clear Firestore data are not held to the token, so@firebase/rules-unit-testingkeeps working; a client that reaches the emulator through such a forwarder can call them, as it could on the official emulator.fireemu execgives its command the token inFIREEMU_CONTROL_TOKEN, andfireemu/testingand the UI send it. - Browser pages must still come from a loopback origin (
http://localhost:...), so a page served from another host cannot use the exposed ports. - A listener on
0.0.0.0or::is advertised as127.0.0.1or::1in the environment and the Hub, as the official CLI does. - With the option on, the emulators also answer a single-label
Hostsuch as a Compose service name (fireemu:8080). Other names must be listed innetwork.allowedHosts; see below.
Host names the emulators answer
Section titled “Host names the emulators answer”A web page can re-point a name it controls at 127.0.0.1 after it has loaded (DNS rebinding). The browser then treats requests to the emulators as same-origin and sends no Origin, so the page could read local data that the emulators serve without a credential. To prevent that, the product emulators (Firestore, Authentication and the control API, Storage, Functions, Eventarc, Cloud Tasks, Pub/Sub, the Realtime Database and Hosting) answer 403 to an HTTP/1 request whose Host header names any other host than:
localhostand names under.localhost;- any IP address, so
10.0.2.2from the Android emulator, a LAN address or a bridge address works; - names under
.internal, the reserved suffix of the names container runtimes give a container for its host:host.docker.internalandgateway.docker.internal(Docker),host.containers.internal(Podman),host.lima.internal(Lima and Colima),host.orb.internal(OrbStack); - names under
.firebaseio.comand.firebasedatabase.app, by which the Realtime Database selects a namespace; - names under
.cloudworkstations.dev, the preview and port URLs of Firebase Studio and Cloud Workstations; - names under
.app.github.dev, the forwarded-port URLs of GitHub Codespaces; - names under the special-use suffixes
.test,.localand.home.arpa, which public DNS does not answer:/etc/hostsaliases such asmyapp.test, mDNS names such asmy-mac.local, Kubernetes service names under the defaultsvc.cluster.local, and home-network names; - with
network.allowNonLoopbackHosts, single-label names such as a Compose service name; - the names in
network.allowedHosts.
The special-use and .internal names are safe only while local name resolution can be trusted. On an untrusted network a malicious resolver (for example one handed out by DHCP on a public Wi-Fi, a risk RFC 8375 section 6.1 notes for .home.arpa) or an mDNS peer on the LAN (for .local) can answer them and rebind a page to the emulators. network.allowedHosts only adds names; no setting removes these defaults.
A request without Host (HTTP/1.0) and HTTP/2 requests (gRPC clients) are not checked, because no browser sends them. The comparison ignores case, the port and one trailing dot. The Hub, the UI and the Logging emulator answer loopback names only. The official emulator does not check Host; production has no counterpart, so the check is the same in both profiles.
Add the other names your clients use, for example a tunnel, a name in /etc/hosts outside the special-use suffixes, or a Kubernetes service under a custom cluster domain. An entry is a name, a name with a leading dot for that domain and all its subdomains, or "*" to answer every Host (which turns the defence off):
[network]# Allow product emulators to bind non-loopback hosts: true or false.allowNonLoopbackHosts = true# Additional accepted host names: exact names, dot-prefixed domains, or "*".allowedHosts = ["emulator.ci.example", ".corp.example"]{ "network": { "allowNonLoopbackHosts": true, "allowedHosts": ["emulator.ci.example", ".corp.example"] }}An entry has no scheme, port or path. Listing a name only lets requests naming it through; the browser-origin checks are unchanged, so a page must still come from a loopback origin to use the privileged routes. Fireemu prints a warning: line at start for "*" and for an entry that covers a whole top-level domain such as ".com", because any web page can register a name there.
A refused request gets a 403 whose message names the setting, and the daemon logs one warning: refused Host "<name>" line per refused name (at most 32 names), so a client whose SDK hides the response body still leaves a trace.
The entries for common environments:
| Environment | Entry |
|---|---|
| A Kubernetes service under a custom cluster domain | ".svc.<cluster domain>" (or the exact service name) |
| ngrok | the exact tunnel name, for example "abc123.ngrok-free.app" |
| Cloudflare Tunnel | the exact public hostname of the tunnel |
A name in /etc/hosts outside .test, .local and .home.arpa |
that name |
List exact tunnel names rather than the tunnel provider’s whole domain: anyone can open a tunnel under it. Firebase Studio’s .cloudworkstations.dev, Codespaces’ .app.github.dev, mDNS .local, Kubernetes svc.cluster.local and .test names need no entry. The policy is read when the daemon starts; restart it after changing network.allowedHosts.