Skip to content

Docker

Firenook runs in a Linux container as it runs on your machine. Four things matter: an image with Node 22, 24 or 26 and glibc 2.28 or later, the emulators listening on all interfaces, the ports published, and a volume for the data you want to keep.

Use node:24-slim. Firenook’s Linux engine needs glibc 2.28 or later, and the official Node images built on Debian have it:

  • node:24-slim (Debian 12, glibc 2.36) works, and is the image Firenook’s own release checks start the engine in. node:24 is the same system with more tools.
  • node:24-trixie-slim (Debian 13, glibc 2.41) works too.
  • node:24-alpine uses musl, not glibc, and Firenook has no engine for it.

For Node 22 or 26, use the same variant of node:22 or node:26. CI and GitHub Actions shows what Firenook prints on a musl image.

This one is for a project firenook init set up: Firenook in the root package.json and a TypeScript Functions codebase in functions/, each with its lockfile.

Dockerfile
# A glibc image with Node: Debian, not Alpine.
FROM node:24-slim
WORKDIR /app
# Firenook and its engine for this platform (linux-x64 or linux-arm64).
COPY package.json package-lock.json ./
RUN npm ci
# Your Functions' own dependencies.
COPY functions/package.json functions/package-lock.json functions/
RUN npm ci --prefix functions
COPY . .
# Compile the TypeScript codebase into functions/lib.
RUN npm run build --prefix functions
# Every emulator on all interfaces, data kept in .firenook/state.
CMD ["npx", "firenook", "emulators:start", "--host", "0.0.0.0", "--keep-data"]

A JavaScript codebase has no build script: leave out the RUN npm run build line.

.dockerignore
node_modules
functions/node_modules
functions/lib
.firenook

The lockfile lists the engine of every platform, so npm ci installs the Linux engine in the image even when you wrote the lockfile on a Mac. Keep node_modules out of the build context: the engine installed on your machine is not the one the container needs.

By default the emulators listen on the loopback address, 127.0.0.1 (the hub and the console on ::1 as well), which inside a container only the container itself can reach. --host 0.0.0.0 makes every emulator, the hub and the console listen on all interfaces. Firenook warns you as it starts, because the emulators have no authentication:

Firenook: binding 0.0.0.0; the emulators have no authentication, so every service, the data and arbitrary Functions execution are reachable from any device that can reach this host.

Setting the same host on every entry under emulators in firebase.json does the same, as with the official emulator. Firenook listens on one address for the whole suite, so the host values must match.

The hub and the variables Firenook sets still give clients 127.0.0.1 as the address, the one that reaches a port published to your machine. Bound to 0.0.0.0, the console answers whatever host name you reach it by. --allowed-hosts NAME,... limits it to those names and the loopback ones.

Publish each port under the same number, since clients use the ports that firebase.json names. Mount a volume on /app/.firenook to keep the data --keep-data saves in .firenook/state:

compose.yaml
services:
firebase:
build: .
ports:
- "4000:4000" # console
- "4400:4400" # hub
- "4500:4500" # logging
- "5001:5001" # Functions
- "8080:8080" # Firestore
- "8085:8085" # Pub/Sub
- "9099:9099" # Auth
- "9150:9150" # Firestore WebSocket
- "9199:9199" # Storage
- "9299:9299" # Eventarc
- "9499:9499" # Cloud Tasks
volumes:
- firenook-data:/app/.firenook
stop_grace_period: 1m
volumes:
firenook-data:
Terminal window
docker compose up --build

Your app on your machine connects as usual, for example with connectFirestoreEmulator(db, '127.0.0.1', 8080), and the console is at http://127.0.0.1:4000/console. Another container in the same Compose project reaches the emulators by the service name, for example FIRESTORE_EMULATOR_HOST=firebase:8080.

The volume holds each start’s working files in .firenook/runs/ too. Firenook never deletes them; remove old runs when you no longer need them.

docker compose stop and docker stop send SIGTERM. Firenook passes it to the engine, which closes the data and stops cleanly; with --export-on-exit, it exports first. Docker waits 10 seconds by default before it kills the container. A large data set can need longer to export, so the Compose file above allows a minute (docker stop -t 60 does the same). After a kill, the next start checks the data and repairs it if it needs to.

emulators:exec works in the container as it does anywhere:

Terminal window
docker run --rm $(docker build -q .) npx firenook emulators:exec "npm test"

Run only the emulators you need with one service on its own.