👋

Dockerのコンテナ内でFirebase Emulatorを動かしたい

に公開

背景

自社サービス開発時に認証システムとしてFirebase Authenticationを使用することになりました。ただ、アカウントごとに権限を分けて自社サービスを運用する必要があるため、開発時にはFirebase Emulatorを使用することを決定しました。その際、Dockerコンテナ内でFirebase Emulatorを動かした際に学んだ内容をまとめました。

実装

ディレクトリ構成

記事用に作った構成のためDockerを使うような構成ではありませんが、ご了承ください

.
├── README.md
├── docker
│   ├── emulators
│   └── frontend
├── docker-compose.yml
├── eslint.config.js
├── firebase-debug.log
├── firebase.json
├── index.html
├── package-lock.json
├── package.json
├── public
│   └── vite.svg
├── src
│   ├── App.css
│   ├── App.tsx
│   ├── assets
│   ├── firebase
│   ├── index.css
│   ├── main.tsx
│   └── vite-env.d.ts
├── tsconfig.app.json
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts

Firebaseプロジェクト作成

今回はFirebaseプロジェクトが作成されていることが前提の構築になるためDockerの構築前にプロジェクトを作成していきます
下記から作成をしてください
https://firebase.google.com/?hl=ja

Firebase CLIをインストール

Firebase CLIがインストールされていない場合、以下のコマンドを実行してください。

zsh
npm install -g firebase-tools

ローカル上でログイン

ローカル上でFirebaseにログインしてください

zsh
firebase login

Firebaseプロジェクト初期化

以下のコマンドを実行し、Firebaseプロジェクトの初期化を行います。

zsh
firebase init

基本的にご自身のプロジェクトに合わせて設定を選んで問題ないですが、本記事のメインの一つであるEmulatorsは選んでください
Emulators Setupもご自身のプロジェクトに合わせてください
今回はAuthentication Emulatorのみにしています

zsh
     ######## #### ########  ######## ########     ###     ######  ########
     ##        ##  ##     ## ##       ##     ##  ##   ##  ##       ##
     ######    ##  ########  ######   ########  #########  ######  ######
     ##        ##  ##    ##  ##       ##     ## ##     ##       ## ##
     ##       #### ##     ## ######## ########  ##     ##  ######  ########

You're about to initialize a Firebase project in this directory:

  /Users/roll1226/personalDevelopment/study-firebase-emulator

? Which Firebase features do you want to set up for this directory? Press Space to select features, then Enter to confirm your choices. Emulators: Set up local emulators for Firebase products

=== Project Setup

First, let's associate this project directory with a Firebase project.
You can create multiple project aliases by running firebase use --add, 
but for now we'll just set up a default project.

? Please select an option: Use an existing project
? Select a default Firebase project for this directory: test-firebase-emulators-7de0a (test-firebase-emulators)
i  Using project test-firebase-emulators (test-firebase-emulators)

=== Emulators Setup
? Which Firebase emulators do you want to set up? Press Space to select emulators, then Enter to confirm your choices. Authentication Emulator
? Which port do you want to use for the auth emulator? 9099
? Would you like to enable the Emulator UI? Yes
? Which port do you want to use for the Emulator UI (leave empty to use any available port)? 
? Would you like to download the emulators now? Yes

i  Writing configuration info to firebase.json...
i  Writing project information to .firebaserc...
i  Writing gitignore file to .gitignore...

✔  Firebase initialization complete!

firebase.jsonの設定変更

初期設定が完了したら、firebase.jsonを編集して、Docker内のFirebase Emulatorにローカル環境からアクセス可能にします。

firebase.json
{
  "emulators": {
    "auth": {
-      "port": 9099
+      "port": 9099,
+      "host": "0.0.0.0"
    },
    "ui": {
-      "enabled": true
+      "enabled": true,
+      "host": "0.0.0.0"
    },
    "singleProjectMode": true
  }
}

フロントを整える

プロジェクト作成

簡単に実装するためReact + viteで整えていきます

zsh
npm create vite@latest .
zsh
✔ Current directory is not empty. Please choose how to proceed: › Remove existing files and continue
✔ Select a framework: › React
✔ Select a variant: › TypeScript + SWC

Scaffolding project in /Users/roll1226/personalDevelopment/study-firebase-emulator...

Done. Now run:

  npm install
  npm run dev

Firebaseのライブラリをインストールしていきます

zsh
npm install firebase

ポート設定

開発サーバーのポートを固定するため、vite.config.tsを以下のように編集します。

vite.config.ts
import react from "@vitejs/plugin-react-swc";
import { defineConfig } from "vite";

// https://vite.dev/config/
export default defineConfig({
+  server: {
+    port: 3000,
+    host: "0.0.0.0",
+  },
  plugins: [react()],
});

.env作成

.envを整えていきます
今回はFirebase Emulatorのみ使用するので必要ない可能性はあります
ただ、実運用も考えた場合は記載しとくのが吉です

zsh
touch .env
.env
VITE_FIREBASE_API_KEY="my_api_key"
VITE_FIREBASE_AUTH_DOMAIN="my_auth_domain"
VITE_FIREBASE_PROJECT_ID="my_project_id"
VITE_FIREBASE_STORAGE_BUCKET="my_storage_bucket"
VITE_FIREBASE_MESSAGING_SENDER_ID="my_messaging_sender_id"
VITE_FIREBASE_APP_ID="my_app_id"

Firebaseの初期化コード

Firebaseディレクトリを作成し、初期化コードを記述します。

zsh
mkdir src/firebase && touch src/firebase/index.ts

以下のコードを記載してください。

src/firebase/index.ts
import { initializeApp } from "firebase/app";
import { connectAuthEmulator, getAuth } from "firebase/auth";

const firebaseConfig = {
  apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
  authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN,
  projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID,
  storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET,
  messagingSenderId: import.meta.env.VITE_FIREBASE_MESSAGING_SENDER_ID,
  appId: import.meta.env.VITE_FIREBASE_APP_ID,
};

const app = initializeApp(firebaseConfig);
const auth = getAuth(app);

if (process.env.NODE_ENV === "development") {
  connectAuthEmulator(auth, "http://localhost:9099");
}

export { auth };

tsxを整える

確認がしやすいように表示部分も整えていきます

src/App.tsx
import {
  createUserWithEmailAndPassword,
  signInWithEmailAndPassword,
} from "@firebase/auth";
import { useCallback, useState } from "react";
import "./App.css";
import { auth } from "./firebase";

function App() {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");

  const singUpHandler = useCallback(async () => {
    await createUserWithEmailAndPassword(auth, email, password)
      .then((r) => console.log(r))
      .catch((e) => console.error(e));
  }, [email, password]);

  const signInHandler = useCallback(async () => {
    await signInWithEmailAndPassword(auth, email, password)
      .then((r) => console.log(r))
      .catch((e) => console.error(e));
  }, [email, password]);

  return (
    <>
      <label>
        メールアドレス
        <input type="email" onChange={(e) => setEmail(e.target.value)} />
      </label>
      <br />
      <label>
        パスワード
        <input type="password" onChange={(e) => setPassword(e.target.value)} />
      </label>
      <br />
      <button onClick={singUpHandler}>新規登録</button>
      <button onClick={signInHandler}>ログイン</button>
    </>
  );
}

export default App;

Dockerの設定を整える

初めにdocker-compose.ymlを作成していきます

docker-compose.yml

zsh
touch docker-compose.yml

作成出来たら処理を書いていきます

docker-compose.yml
services:
  frontend:
    build:
      context: .
      dockerfile: docker/frontend/Dockerfile
    ports:
      - "3000:3000"
    environment:
      - VITE_FIREBASE_API_KEY=${VITE_FIREBASE_API_KEY:-null}
      - VITE_FIREBASE_AUTH_DOMAIN=${VITE_FIREBASE_AUTH_DOMAIN:-null}
      - VITE_FIREBASE_PROJECT_ID=${VITE_FIREBASE_PROJECT_ID:-null}
      - VITE_FIREBASE_STORAGE_BUCKET=${VITE_FIREBASE_STORAGE_BUCKET:-null}
      - VITE_FIREBASE_MESSAGING_SENDER_ID=${VITE_FIREBASE_MESSAGING_SENDER_ID:-null}
      - VITE_FIREBASE_APP_ID=${VITE_FIREBASE_APP_ID:-null}
    volumes:
      - ./:/frontend
      - frontend_node_modules:/frontend/node_modules

  emulators:
    build:
      context: .
      dockerfile: docker/emulators/Dockerfile
    ports:
      - "4000:4000"
      - "9099:9099"
    volumes:
      - ./:/emulators
      - ./docker/emulators/bin/:/root/.cache:cached
      - ./docker/emulators/config/:/root/.config:cached

volumes:
  frontend_node_modules:

Dockerfile

次はDockerfileを整えていきます
emulators用とfrontend用の二種類を用意しています

zsh
mkdir -p docker/emulators && touch docker/emulators/Dockerfile
mkdir docker/frontend && touch docker/frontend/Dockerfile

emulators用Dockerfile

docker/emulators/Dockerfile
FROM node:20.10.0-slim

WORKDIR /emulators

RUN npm install -g firebase-tools

COPY . .

# Firebase Emulator UI Port
EXPOSE 4000
# Firebase Emulator Auth Port
EXPOSE 9099

CMD ["npx", "firebase", "emulators:start"]

frontend用Dockerfile

docker/frontend/Dockerfile
FROM node:20.10.0-slim AS builder
WORKDIR /frontend

COPY ./package*.json ./

RUN npm ci
COPY . .

FROM node:20.10.0-slim
EXPOSE 3000
WORKDIR /frontend

COPY --from=builder /frontend /frontend
CMD ["npm", "run", "dev"]

起動準備

ここまで出来たら起動準備をしていきます
初めにDockerのコンテナ内でFirebaseでログインするため下記をターミナルで実行してください

zsh
docker compose run --rm emulators firebase login --no-localhost

実行すると下記内容がターミナルに表示されるはずです
コマンドライン
表示されたら2. Visit the URL below on any device and follow the instructions to get your code:に表示されているURLにアクセスしてください

アクセスするとアカウントを選択する画面に移ります
firebase login時と同じアカウントを選んでください
firebase-auth_1
選択したアカウントで問題なければ「次へ」を押してください
firebase-auth_2
アクセス許可を聞かれるので問題なければ「許可」を押してください
firebase-auth_3
すると画面が移り変わりfirebase login --no-localhostを実行したか、を聞いてくるので「Yes, I Just ran this command」を押してください
firebase-auth_4
次に表示されているsessionIDがターミナルの1. Take note of your session ID:に表示されているsessionID一致しているか確認してきます
一致していれば「Yes, this is my session ID」を押してください
firebase-auth_5
最後にauthorization codeが表示されるのでコピーしてターミナルの3. Paste or enter the authorization code below once you have it:にペースト・Enterを押してください
✔ Success! Logged in as {ご自身のメールアドレス}がターミナル上に表示されれば完了です
firebase-auth_6

起動

下記コマンドを実行してください

zsh
docker compose up --build -d

起動後が下記localhostにアクセスして問題なく表示されたら起動完了です
サインアップ・サインイン処理を行うと実際のプロジェクトではなくFirebase Emulatorにアカウントが作成されるはずです


フロントエンド

Firebase Emulators

余談

Firebase EmulatorFirebase authenticationを使用する際、データを永続化したいという方もいるかと思います
自分もその一人になります
Dockerを使用しないローカルのみでFirebase Emulatorを使用する場合は下記のように実行すると思います

zsh
firebase emulators:start --import data --export-on-exit

ただ、Dockerを使用して同じコマンドで実行しても再起動時に上手くデータを永続化することは出来ませんでした
そこで定期的にデータをexportすることにしました

シェルスクリプト

export用のシェルスクリプトを書いていきます

zsh
mkdir command && touch command/export.sh
command/export.sh
#!/bin/bash

max_retries=10
retry_count=0

while true; do
  if curl --silent --head http://localhost:4000 | grep "200 OK" > /dev/null; then
    echo "Firebase Auth Emulator is running at http://localhost:4000"
    break
  else
    retry_count=$((retry_count + 1))
    echo "Waiting for Firebase Auth Emulator to start at http://localhost:4000... (Attempt $retry_count/$max_retries)"

    if [ "$retry_count" -ge "$max_retries" ]; then
      echo "Failed to detect Firebase Auth Emulator after $max_retries attempts. Exiting."
      exit 1
    fi

    sleep 5
  fi
done

while true; do
  export_dir="./export"

  firebase emulators:export $export_dir --only auth --force

  echo "Firebase Auth Emulator data exported to $export_dir"

  sleep 10
done

Dockerfile・package.json

作成したシェルスクリプトに合わせてDockerfilepackage.jsonを整えていきます

docker/emulators/Dockerfile
FROM node:20.10.0-slim

WORKDIR /emulators

+ RUN apt-get update && apt-get install -y curl \
+   && apt-get clean \
+   && rm -rf /var/lib/apt/lists/*

RUN npm install -g firebase-tools

COPY . .

# Firebase Emulator UI
EXPOSE 4000
# Firebase Emulator Auth
EXPOSE 9099

- CMD ["npx", "firebase", "emulators:start"]
+ CMD ["npm", "run", "emulators"]

package.json
{
  "name": "study-firebase-emulator",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc -b && vite build",
    "lint": "eslint .",
    "preview": "vite preview",
+    "emulators": "npx firebase emulators:start --import ./export --export-on-exit & sh ./commands/export.sh"
  },
  "dependencies": {
    "firebase": "^11.1.0",
    "react": "^18.3.1",
    "react-dom": "^18.3.1"
  },
  "devDependencies": {
    "@eslint/js": "^9.17.0",
    "@types/react": "^18.3.18",
    "@types/react-dom": "^18.3.5",
    "@vitejs/plugin-react-swc": "^3.5.0",
    "eslint": "^9.17.0",
    "eslint-plugin-react-hooks": "^5.0.0",
    "eslint-plugin-react-refresh": "^0.4.16",
    "globals": "^15.14.0",
    "typescript": "~5.6.2",
    "typescript-eslint": "^8.18.2",
    "vite": "^6.0.5"
  }
}

再ビルド

最後に再ビルドをして実行してみてください

zsh
docker compose up --build

下記のようなログが出力されてexportというディレクトリにデータが作らているはずです
これによりDockerを再起動時してもデータを永続化が出来ます

emulators-1  | 
emulators-1  | > study-firebase-emulator@0.0.0 emulators
emulators-1  | > npx firebase emulators:start --import ./export --export-on-exit & sh ./commands/export.sh
emulators-1  | 
emulators-1  | Waiting for Firebase Auth Emulator to start at http://localhost:4000... (Attempt 1/10)
emulators-1  | i  emulators: Starting emulators: auth
emulators-1  | Waiting for Firebase Auth Emulator to start at http://localhost:4000... (Attempt 2/10)
emulators-1  | 
emulators-1  | ┌─────────────────────────────────────────────────────────────┐
emulators-1  | │ ✔  All emulators ready! It is now safe to connect your app. │
emulators-1  | │ i  View Emulator UI at http://127.0.0.1:4000/               │
emulators-1  | └─────────────────────────────────────────────────────────────┘
emulators-1  | 
emulators-1  | ┌────────────────┬──────────────┬────────────────────────────┐
emulators-1  | │ Emulator       │ Host:Port    │ View in Emulator UI        │
emulators-1  | ├────────────────┼──────────────┼────────────────────────────┤
emulators-1  | │ Authentication │ 0.0.0.0:9099 │ http://127.0.0.1:4000/auth │
emulators-1  | └────────────────┴──────────────┴────────────────────────────┘
emulators-1  |   Emulator Hub running at 127.0.0.1:4400
emulators-1  |   Other reserved ports: 4500
emulators-1  | 
emulators-1  | Issues? Report them at https://github.com/firebase/firebase-tools/issues and attach the *-debug.log files.
emulators-1  |  
emulators-1  | Firebase Auth Emulator is running at http://localhost:4000
emulators-1  | i  Found running emulator hub for project test-firebase-emulators-7de0a at http://127.0.0.1:4400
emulators-1  | i  Creating export directory /emulators/export
emulators-1  | i  Exporting data to: /emulators/export
emulators-1  | i  emulators: Received export request. Exporting data to /emulators/export.
emulators-1  | ✔  emulators: Export complete.
emulators-1  | ✔  Export complete
emulators-1  | Firebase Auth Emulator data exported to ./export

参考

https://qiita.com/ppco/items/87682b3a14ceb3702dbb

Discussion