How to setup Firebase Emulator in Docker

How to setup Firebase Emulator in Docker

And also persisting the emulator data

Firebase offers an emulator suite that make developing applications locally easier, but integrating it into a Docker environment can streamline your workflow even further. In this tutorial, we'll explore how to use the Firebase Emulator within Docker. In the end I'll show a ready-to-use Docker image I've prepared, which encapsulates all the steps we'll discuss in this article.

Why Dockerize Firebase Emulator?

Dockerizing the Firebase Emulator offers several benefits:

  • Consistency: Docker ensures that the Firebase Emulator runs the same way, regardless of where it's deployed, by encapsulating dependencies and configurations.

  • Simplicity: By using Docker, setting up the Firebase Emulator becomes a matter of running a container, without the need to manually install dependencies on your local machine or CI/CD environments.

  • Portability: Containers can be easily shared and run across different environments, making collaboration and deployment seamless.

Why another Image?

I know, there are some existing images out there, which we were also using, namely this one: https://hub.docker.com/r/spine3/firebase-emulator. However, we weren't able to make the image persist data, as the container never shuts down gracefully and never reaches the point where data gets exported. Without the underlying Dockerfile we weren't even able to dig deeper, so we thought our time is spent better in making a new setup and open sourcing this. So here we go:

Prerequisites

Before we dive into the setup, ensure you have the following installed:

  • Docker: Install Docker

  • Git (optional): For cloning the repository, though you can also download the code directly.

Step 1: Setting up your Firebase config

First of all we need a firebase.json config file that the emulator picks up on startup. You can get one by installing the firebase CLI and then run firebase init in a folder or you copy the one below.

  • Create a new folder, I'll name mine firebase-docker but the name is up to you

  • Change into that directory and create a new file firebase.json (feel free to copy the one below)

  • (optional) customize the ports if needed

{
  "emulators": {
    "firestore": { "port": "8080", "host": "0.0.0.0" },
    "ui": { "enabled": true, "port": "4000", "host": "0.0.0.0" },
    "auth": { "port": "9099", "host": "0.0.0.0" },
    "functions": { "port": "5001", "host": "0.0.0.0" },
    "database": { "port": "9000", "host": "0.0.0.0" },
    "storage": { "port": "9199", "host": "0.0.0.0" }
  },
  "firestore": {
    "rules": "firestore.rules",
    "indexes": "firestore.indexes.json"
  },
  "storage": {
    "rules": "storage.rules"
  }
}

As you can see the firebase-config references to two .rules-files. Those are needed for those emulators to know how to handle requests to these services. In our case we want every request to be allowed without verification.

  • Create two new files by running touch storage.rules firestore.rules in our directory and add the following contents to them
// firestore.rules

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write: if true;
    }
  }
}
// storage.rules

rules_version = '2';
service firebase.storage {
  match /b/{bucket}/o {
    match /{allPaths=**} {
        allow get;
        allow create: if true;
    }
  }
}

As you can see, both rules allow all reads and writes.

ATTENTION: Do not use these rules in production, as they would allow everybody to read, write and delete data.

Step 2: Setting up your Dockerfile

Next up you'll need a Dockerfile that specifies how to build your Firebase Emulator Docker image. Here's a simple example to get you started:

# We chose node-18 since currently firebase functions work on node-18 only and is in beta in node-20
# https://firebase.google.com/docs/functions/manage-functions#set_nodejs_version
FROM node:18-alpine 

# Install JDK
RUN apk add --no-cache openjdk11

# Install the firebase cli
RUN npm install -g firebase-tools

# Clear the npm cache to keep the image tidy
RUN npm cache clean --force

# Install and setup all the Firebase emulators
RUN firebase setup:emulators:database
RUN firebase setup:emulators:firestore
RUN firebase setup:emulators:storage
RUN firebase setup:emulators:ui

# Copying everything (=the firebase configuration) to the workdir
COPY . /firebase

# Mount your firebase project directory to /firebase when running the container
# This is the folder containing the firebase.json
WORKDIR /firebase

# Expose the emulator ports
# If you use non standard ports change them to the ones in your firebase.json
#       AUTH    STORE   RLTDB   UI  
EXPOSE  9099    9199    8080    4000

# Start the emulator
# we use a shell script to run the emulator to be able to pass in the project from the outside as environment variable
# As you can see, the script exports and imports the emulator data during shutdown and startup, which persists the existing data.
CMD ["sh", "-c", "firebase emulators:start --import=/firebase/data/ --export-on-exit=/firebase/data/ --project=${FB_PROJECT_ID}"]

This Dockerfile does the following:

  1. Starts with a Node.js image since Firebase CLI is a Node.js package.

  2. Sets a working directory inside the container.

  3. Copies your Firebase project files into the container.

  4. Installs the Firebase CLI globally within the image.

  5. Exposes the ports used by various Firebase services.

  6. Defines a command to start the Firebase Emulator. As you can see, the script exports and imports the emulator data during shutdown and startup, which persists the existing data.

Step 3: Building your Docker image (optional)

With your Dockerfile ready, navigate to the directory containing it and run:

docker build -t firebase-emulator .

This command builds your Docker image, tagging it as firebase-emulator.

Step 4: Running your Firebase Emulator Docker container

Option 1: using docker run

ℹ️ For this option, Step 3 needs to be excuted

After building the image, start the Firebase Emulator by running:

docker run -d \
  --name firebase-emulator \
  -v firebase_data:/firebase \
  -e FB_PROJECT_ID=[your_project_id] \
  firebase-emulator

This command runs your Docker container, mapping the necessary ports from the container to your host machine, allowing you to access the Firebase Emulator services locally.

Option 2: using docker-compose

ℹ️ For this option, Step 3 is NOT necessary

You can also run your container on demand with docker-compose. For that create a docker-compose.yml-file: touch docker-compose.yml in the root of your project (next to the Dockerfile) and add these contents:

version: "3.1"

services:
  firebase-emulator:
    container_name: firebase-emulator
    build:
      context: . # <- This instructs docker to build the container from the current directory
      dockerfile: Dockerfile
    restart: always
    ports:
      - 4000:4000 #Emulator UI
      - 9099:9099 #Firebase Auth
      - 9199:9199 #Firebase Cloud Storage
      - 9000:9000 #Firebase Realtime database
    environment:
      - FB_PROJECT_ID=[YOUR_PROJECT_ID] # Enter your Firebase project ID here
    volumes:
      - firebase_data:/firebase

volumes:
  firebase_data:

Then run docker-compose up to start the container. You can add -d to the command to run the container in the background.

Using my Pre-built Docker image

While following the steps above provides a great learning experience, I've prepared a Docker image that encapsulates all these steps, saving you time and effort. You can find the Docker image on DockerHub at evolutecx/firebase-emulator and the source code on GitHub at evolute-cx/firebase-emulator-docker.

Using this pre-built image, you can start the Firebase Emulator with a single command:

docker run -d \
  --name firebase-emulator \
  -v firebase_data:/firebase \
  -e FB_PROJECT_ID=[your_project_id] \
  evolutecx/firebase-emulator:latest

Persisting data

The container is configured in a way that it exports all data to /firebase/data on shutdown and re-imports it from there during startup.

In order to persist data between runs, a named volume is needed.

IMPORTANT: It has to be a named volume, not a bind mount. A bind mount will not work because the host always takes precedence over the container and will overwrite the contents in /firebase which we dont want, because that's where all the config is residing too. bind-mounting to /firebase/data will also not work because firebase will complain about the destination existing already. So a named volume is the way to go (for now)

Source & Contribution

Everything we covered above is already part of our Github-repo here: https://github.com/evolute-cx/firebase-emulator-docker

Feel free to clone the source code and make adjustments to it, we can't wait to have a look at your PR ❤️

Conclusion

Dockerizing the Firebase Emulator streamlines your development workflow, offering a consistent, simple, and portable way to develop and test your Firebase applications. By following this guide, you can set up your Firebase Emulator in Docker from scratch or leverage the pre-built image I've provided to jumpstart your development process.

Integrating Docker and Firebase using this approach can significantly enhance your development lifecycle. I encourage you to explore this setup further and tailor it to your project's needs. Feel free to clone our repo and submit PRs to it.

Happy coding!