- Notifications
You must be signed in to change notification settings - Fork 979
Description
Operating System
Windows 11 (Docker host) running Running via Docker Compose (bridge network) and with alpine containers
Environment (if applicable)
Next.js 15.5.4 Node.js v22
Firebase SDK Version
12.4.0
Firebase SDK Product(s)
Auth
Project Tooling
Next.js with TypeScript
Docker & Docker Compose
Firebase Emulator running on Docker
Server-side rendering using initializeServerApp
Detailed Problem Description
I have a Next.js system setup with Firebase, and I want to run it locally using the Firebase Emulator, but fully inside Docker.
The Next.js app contains both:
- Frontend (executed on client machines)
- Backend (APIs and server components executed on the server)
Running locally works fine if I use localhost
for all emulator hosts.
However, when everything runs inside Docker Compose (Next.js app + emulator), it fails on the server side when using initializeServerApp
with a auth id token. In this case I need to use localhost in the client-side configurations and the container name in the server-side configurations.
Maybe when using emulator both client and server emulator host needs to be the same?? I could not find anything like that in the documentation.
Works When
Running Next.js locally and only the emulator in Docker:
- Both server and client connect to the emulator using
localhost
- Everything works (user sign-up, login, authenticated server requests, etc.)
Fails When
Running both Next.js and the emulator in Docker Compose:
- Client can create an account and log in successfully
- Server receives the request and tries to initialize the server app
- Fails with the following error:
FirebaseServerApp could not login user with provided authIdToken: Error [FirebaseError]: Firebase: Error (auth/network-request-failed). at p (.next/server/chunks/2514.js:1:23892) at l (.next/server/chunks/2514.js:1:23452) at A (.next/server/chunks/2514.js:1:29337) at async ai.initializeCurrentUserFromIdToken (.next/server/chunks/2514.js:1:50019) at async (.next/server/chunks/2514.js:1:49521) { code: 'auth/network-request-failed', customData: [Object] }
The only difference is the emulator host configuration:
- Client-side must use
localhost
- Server-side must use the Docker container hostname (e.g.
firebase-emulator:9099
)
Steps and code to reproduce issue
Steps and Code to Reproduce Issue
1. Client Firebase Configuration
'use client'; import { initializeApp } from 'firebase/app'; import { getAuth } from 'firebase/auth'; import { getFirestore } from 'firebase/firestore'; import { getStorage } from 'firebase/storage'; import { firebaseConfig } from '@/services/firebase/config'; import { enableAuthEmulatorIfEnabled, enableFirestoreEmulatorIfEnabled, enableStorageEmulatorIfEnabled } from './emulator'; export const firebaseApp = firebaseConfig ? initializeApp(firebaseConfig) : initializeApp(); export const auth = getAuth(firebaseApp); enableAuthEmulatorIfEnabled(auth); export const db = getFirestore(firebaseApp); enableFirestoreEmulatorIfEnabled(db); export const storage = getStorage(firebaseApp); enableStorageEmulatorIfEnabled(storage);
firebaseConfig
is only undefined if I run this application on Firebase App Hosting, in the test neither of the cases will have firebaseConfig undefined (running locally or running on docker). I do not use App Hosting emulator for now.
2. Firebase Admin Configuration
import 'server-only'; import admin from 'firebase-admin'; function getFirebaseAdminConfig(): admin.AppOptions | undefined { if ( typeof process === 'undefined' || process.env.FIREBASE_WEBAPP_CONFIG ) { return undefined; } let options: admin.AppOptions = { storageBucket: process.env.FIREBASE_ADMIN_STORAGE_BUCKET, projectId: process.env.FIREBASE_ADMIN_PROJECT_ID, } if (process.env.FIREBASE_ADMIN_PRIVATE_KEY) { options.credential = admin.credential.cert({ privateKey: process.env.FIREBASE_ADMIN_PRIVATE_KEY, clientEmail: process.env.FIREBASE_ADMIN_CLIENT_EMAIL, projectId: process.env.FIREBASE_ADMIN_PROJECT_ID, }); } return options; } const firebaseAdminConfig = getFirebaseAdminConfig(); let adminApp: admin.app.App; try { adminApp = admin.app(); } catch (_error) { adminApp = firebaseAdminConfig ? admin.initializeApp(firebaseAdminConfig) : admin.initializeApp(); } export const adminAuth = admin.auth(adminApp); export const adminStorage = admin.storage(adminApp); export const adminFirestore = admin.firestore(adminApp);
Connects automatically with the emulator when using specific environment variables, but in this example the admin client is not used because user can't even login to perform actions in the application that requires the admin client.
3. Server Firebase Initialization
import 'server-only'; import { FirebaseServerApp, FirebaseServerAppSettings, initializeServerApp } from 'firebase/app'; import { cookies } from 'next/headers'; import { firebaseConfig } from '@/services/firebase/config'; import { logger } from '../../lib/logger/default-logger'; import { enableAuthEmulatorIfEnabled } from './emulator'; export async function getAuthenticatedAppForUser() { const authIdToken = (await cookies()).get('__session')?.value; var firebaseServerApp: FirebaseServerApp; try { var settings: FirebaseServerAppSettings = authIdToken ? { authIdToken } : {}; firebaseServerApp = firebaseConfig == undefined ? initializeServerApp(settings) : initializeServerApp(firebaseConfig, settings); } catch (error) { logger.warn('Firebase server app initialization failed:', error); throw error; } const auth = getAuth(firebaseServerApp); enableAuthEmulatorIfEnabled(auth); await auth.authStateReady(); return { auth, firebaseServerApp, currentUser: auth.currentUser }; }
Because of #8347 (comment) I tried to replace the enableAuthEmulatorIfEnabled call with a direct call to the connectAuthEmulator with hardcoded host to check if maybe my enableAuthEmulatorIfEnabled function was taking more time than necessary to run, but since the authentication is a tick after initializing auth, I don't think this can be an issue. The enableAuthEmulatorIfEnabled works fine when using localhost for both server and client hosts.
4. Emulator Enabler Function
export function enableAuthEmulatorIfEnabled(auth: Auth) { if (!process.env.NEXT_PUBLIC_FIREBASE_AUTH_EMULATOR_HOST && !process.env.FIREBASE_AUTH_EMULATOR_HOST) { return; } if (process.env.NEXT_PUBLIC_FIREBASE_AUTH_EMULATOR_HOST === 'disabled' || process.env.FIREBASE_AUTH_EMULATOR_HOST === 'disabled') { return; } const emulatorHost = process.env.FIREBASE_AUTH_EMULATOR_HOST || process.env.NEXT_PUBLIC_FIREBASE_AUTH_EMULATOR_HOST; if (!emulatorHost) return; if (!auth.emulatorConfig) { const [host, port] = emulatorHost.split(':'); connectAuthEmulator(auth, `http://${host}:${port}`, { disableWarnings: true }); } }
Initially I didn’t have the
auth.emulatorConfig
check, but added it because of firebase-admin-node#2647.
5. Docker Compose Setup
firebase-emulator: image: spine3/firebase-emulator:latest working_dir: /firebase environment: - ENABLE_UI=true - GCP_PROJECT=demo-build-manage-scale ports: - 9000:9000 - 8080:8080 - 4000:4000 - 9099:9099 - 8085:8085 - 5001:5001 - 9199:9199 volumes: - firebase_data:/firebase/baseline-data - ../../firebase.json:/firebase/firebase.json - ../gcp/storage/storage.rules:/firebase/infra/gcp/storage/storage.rules nextjs: build: context: ../../ dockerfile: infra/docker/nextjs/Dockerfile args: DOT_ENV_FILE: .env.docker restart: always ports: - "3000:3000" depends_on: firebase-emulator: condition: service_healthy env_file: - required: false path: ../../.env.docker networks: - my-network networks: my-network: driver: bridge volumes: firebase_data:
6. Environment Variables
Local (works):
NEXT_PUBLIC_FIREBASE_AUTH_EMULATOR_HOST=localhost:9099 NEXT_PUBLIC_FIREBASE_STORAGE_EMULATOR_HOST=localhost:9199 NEXT_PUBLIC_FIRESTORE_EMULATOR_HOST=localhost:8080 NEXT_PUBLIC_FIREBASE_PROJECT_ID=demo-build-manage-scale NEXT_PUBLIC_FIREBASE_API_KEY=demo-build-manage-scale FIREBASE_AUTH_EMULATOR_HOST=localhost:9099 FIREBASE_STORAGE_EMULATOR_HOST=localhost:9199 FIRESTORE_EMULATOR_HOST=localhost:8080 FIREBASE_ADMIN_PROJECT_ID=demo-build-manage-scale GCLOUD_PROJECT=demo-build-manage-scale
Docker Compose (fails) even tho server can perform requests on the emulator without server app:
NEXT_PUBLIC_FIREBASE_AUTH_EMULATOR_HOST=localhost:9099 NEXT_PUBLIC_FIREBASE_STORAGE_EMULATOR_HOST=localhost:9199 NEXT_PUBLIC_FIRESTORE_EMULATOR_HOST=localhost:8080 NEXT_PUBLIC_FIREBASE_PROJECT_ID=demo-build-manage-scale NEXT_PUBLIC_FIREBASE_API_KEY=demo-build-manage-scale FIREBASE_AUTH_EMULATOR_HOST=firebase-emulator:9099 FIREBASE_STORAGE_EMULATOR_HOST=firebase-emulator:9199 FIRESTORE_EMULATOR_HOST=firebase-emulator:8080 FIREBASE_ADMIN_PROJECT_ID=demo-build-manage-scale GCLOUD_PROJECT=demo-build-manage-scale
Additional Notes
- When running locally, everything works correctly (both client and server connect to the emulator using
localhost
). - When both run in Docker Compose, the client can authenticate, but the server fails on
initializeServerApp
withauth/network-request-failed
.