Auth Emulator fails with `initializeServerApp` when running fully inside Docker
Closed this issue · 3 comments
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);
firebaseConfigis 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.emulatorConfigcheck, 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
initializeServerAppwithauth/network-request-failed.
Possibly Related Issues
I couldn't figure out how to label this issue, so I've labeled it for a human to triage. Hang tight.
Hi @lucasoares, the auth/network-request-failed tells me there is a networking issue. I am not very familiar with Docker, but I believe this may be a startup race condition.
In your docker-compose.yml, your nextjs service has depends_on: firebase-emulator: { condition: service_healthy }. However, your firebase-emulator service definition doesn't include a healthcheck block. Without a healthcheck, I think Docker's service_healthy condition only waits for the container to start, not for the Auth emulator service inside it (on port 9099) to be fully booted and ready to accept HTTP requests.
https://docs.docker.com/compose/how-tos/startup-order/#control-startup
Can you try adding a healthcheck to your firebase-emulator service? The health check could test that the Auth emulator is running and accepting HTTP requests.
Yeah, that one's on me.
The Firebase emulator wasn't on the same network either, my bad!
After a few hours, we start losing our sense of reality
Thanks haha