I’ve been struggling to deploy my Expo web app on Coolify for some time now. The app works perfectly on Safari and some mobile browsers, but it fails to load on Chrome in production. Here’s a quick summary:
current production url: https://dev.api.v2.quiz.aleos.digital/
The Problem
Local Environment: Works flawlessly on all browsers, including Chrome.
Production (Coolify): Fails to load on Chrome, but no errors appear in the console.
Browsers:
Safari: Works perfectly.
Chrome: Doesn’t load.
Mobile Browsers: Mixed results, mostly works.
What I’ve Tried
Output Methods:
Tried static, server, and single bundle outputs.
Configured both Express server and serve npm package.
Works perfectly on local but fails in production.
Server Configurations:
Using Traefik for proxy with HTTPS redirection, gzip enabled, and permissive CORS headers.
Ensured all assets are available with correct Cache-Control, CSP, and Content-Type headers.
Debugging Attempts:
No errors in Chrome’s console in production.
Network requests for assets return 200 or 304.
Works fine in Chrome’s incognito mode.
Express Server:
Added detailed logging to ensure all requests are served correctly.
Removed restrictive security headers to isolate potential issues.
expo config:
"expo": {
"name": "QuizzApp",
"slug": "QuizzApp",
"platforms": ["ios", "android", "web"],
"web": {
"bundler": "metro",
"output": "static",
"favicon": "./assets/images/favicon.png",
"csp": {
"directives": {
"default-src": ["'self'", "https:", "wss:", "data:", "blob:"],
"script-src": ["'self'", "'unsafe-inline'", "'unsafe-eval'"],
"style-src": ["'self'", "'unsafe-inline'"],
"img-src": ["'self'", "data:", "https:"],
"font-src": ["'self'", "data:", "https:"],
"connect-src": ["'self'", "https:", "wss:"],
"frame-src": ["'self'", "https:"],
"media-src": ["'self'", "https:"],
"object-src": ["'none'"]
}
}
}
}
Express server code:
const express = require("express");
const path = require("path");
const fs = require('fs');
const app = express();
const PORT = process.env.PORT || 3000;
// Logging middleware for all requests
app.use((req, res, next) => {
const timestamp = new Date().toISOString();
console.log('n=== New Request ===');
console.log(`[${timestamp}] ${req.method} ${req.url}`);
console.log('Headers:', JSON.stringify(req.headers, null, 2));
console.log('Query:', JSON.stringify(req.query, null, 2));
console.log('Body:', JSON.stringify(req.body, null, 2));
// Log response
const oldWrite = res.write;
const oldEnd = res.end;
const chunks = [];
res.write = function (chunk) {
chunks.push(chunk);
return oldWrite.apply(res, arguments);
};
res.end = function (chunk) {
if (chunk) chunks.push(chunk);
console.log('n=== Response ===');
console.log('Status:', res.statusCode);
console.log('Headers:', JSON.stringify(res.getHeaders(), null, 2));
return oldEnd.apply(res, arguments);
};
next();
});
// Check if dist directory exists
app.use((req, res, next) => {
const distPath = path.join(__dirname, 'dist');
fs.access(distPath, fs.constants.F_OK, (err) => {
if (err) {
console.error(`n=== Error: dist directory not found ===`);
console.error(`Tried to access: ${distPath}`);
console.error(`Error details:`, err);
} else {
console.log(`n=== Dist directory found at ${distPath} ===`);
}
});
next();
});
// Add permissive CORS and disable security/caching headers
app.use((req, res, next) => {
// Add permissive CORS headers
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET,HEAD,PUT,PATCH,POST,DELETE');
res.setHeader('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
// Remove restrictive caching and security headers
// (No Cache-Control, Pragma, Expires, no-sniff, etc.)
next();
});
// Serve static files from the dist directory
app.use(express.static(path.join(__dirname, 'dist'), {
setHeaders: (res, filePath, stat) => {
console.log('n=== Serving Static File ===');
console.log('File path:', filePath);
console.log('File stats:', stat);
// Do not set X-Content-Type-Options or any restrictive headers here
// Just let Express serve files with default headers
}
}));
// Handle SPA routing
app.get('*', (req, res) => {
const indexPath = path.join(__dirname, 'dist/index.html');
console.log('n=== Serving index.html ===');
console.log('Request URL:', req.url);
console.log('User Agent:', req.headers['user-agent']);
console.log('Attempting to serve:', indexPath);
fs.access(indexPath, fs.constants.F_OK, (err) => {
if (err) {
console.error('Error: index.html not found');
console.error('Error details:', err);
res.status(404).send('index.html not found');
} else {
console.log('index.html found, sending file');
fs.readFile(indexPath, 'utf8', (err, data) => {
if (err) {
console.error('Error reading index.html:', err);
res.status(500).send('Error reading index.html');
} else {
console.log('index.html content length:', data.length);
res.sendFile(indexPath, (err) => {
if (err) {
console.error('Error sending index.html:', err);
} else {
console.log('index.html sent successfully');
}
});
}
});
}
});
});
// Error handling middleware
app.use((err, req, res, next) => {
console.error('n=== Error Occurred ===');
console.error('Error:', err);
console.error('Stack:', err.stack);
res.status(500).send('Something went wrong!');
});
app.listen(PORT, () => {
console.log('n=== Server Started ===');
console.log(`Server running on port ${PORT}`);
console.log(`Current directory: ${__dirname}`);
console.log(`Dist path: ${path.join(__dirname, 'dist')}`);
const distPath = path.join(__dirname, 'dist');
fs.readdir(distPath, { withFileTypes: true }, (err, files) => {
if (err) {
console.error('Error reading dist directory:', err);
} else {
console.log('nDist directory contents:');
files.forEach(file => {
console.log(`- ${file.name} (${file.isDirectory() ? 'directory' : 'file'})`);
});
}
});
});
layout_.tsx
import 'react-native-get-random-values';
import { v4 as uuidv4 } from 'uuid';
import React, { useCallback, useEffect, useState } from 'react';
import { Alert, AppState, StatusBar, Text } from 'react-native';
import * as SplashScreen from 'expo-splash-screen';
import { AppProvider } from '@root/app/contexts/AppContext';
import { setCustomText } from 'react-native-global-props';
import { PostHogProvider } from './utils/posthog-config';
import { PurchasesProvider } from './contexts/purchases';
import { DetailedStatsProvider } from './contexts/DetailedStatsContext';
import { AuthProvider } from './contexts/AuthProvider';
import { useAuth } from './contexts/AuthProvider';
import { fontsLoaded, initializeFonts } from './utils/loadFonts';
import { ThemeProvider, DefaultTheme } from '@react-navigation/native';
import { Slot, Stack } from 'expo-router';
SplashScreen.preventAutoHideAsync();
initializeFonts();
export default function RootComponent() {
return (
<AuthProvider>
<AuthAwareProviders />
</AuthProvider>
);
}
const AuthAwareProviders = () => {
const { loading, isAuthenticated } = useAuth();
if (loading) {
return null;
}
return (
<PostHogProvider>
{isAuthenticated ? (
<AppProvider>
<PurchasesProvider>
<DetailedStatsProvider>
<AppContent />
</DetailedStatsProvider>
</PurchasesProvider>
</AppProvider>
) : (
<AppContent />
)}
</PostHogProvider>
);
};
const AppContent = () => {
const [isFontsLoaded, setFontsLoaded] = useState(fontsLoaded);
const initializeApp = useCallback(async () => {
try {
if (isFontsLoaded) {
await SplashScreen.hideAsync();
}
} catch (error) {
console.error('Initialization error:', error);
}
}, [isFontsLoaded]);
useEffect(() => {
if (isFontsLoaded) {
initializeApp();
}
}, [isFontsLoaded, initializeApp]);
if (!isFontsLoaded) {
return null;
}
// Set custom text props
const customTextProps = {
style: {
fontFamily: 'FontMedium',
fontSize: 14,
lineHeight: 14,
},
};
setCustomText(customTextProps);
// Set Text default props
try {
// @ts-ignore
Text.defaultProps = Text.defaultProps || {};
// @ts-ignore
Text.defaultProps.allowFontScaling = false;
} catch (error) {
console.error("Error setting default Text props", error);
}
return (
<>
<StatusBar
barStyle="dark-content"
translucent={true}
backgroundColor="transparent"
/>
{/* Simply render the children, Expo Router handles the navigation */}
<RootLayoutNav />
</>
);
};
function RootLayoutNav() {
// const { isAuthenticated, loading } = useAuth();
// if (loading) {
// return null; // Or a loading spinner
// }
return (
<ThemeProvider value={DefaultTheme}>
<Stack screenOptions={{ headerShown: false }}>
<Slot />
</Stack>
</ThemeProvider>
);
}
coolify traefik:
traefik.enable=true
traefik.http.middlewares.gzip.compress=true
traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https
traefik.http.routers.http-0-u8wo48ww804owcggk0sc04kc.entryPoints=http
traefik.http.routers.http-0-u8wo48ww804owcggk0sc04kc.middlewares=redirect-to-https
traefik.http.routers.http-0-u8wo48ww804owcggk0sc04kc.rule=Host(`dev.api.v2.quiz.aleos.digital`) && PathPrefix(`/`)
traefik.http.routers.http-0-u8wo48ww804owcggk0sc04kc.service=http-0-u8wo48ww804owcggk0sc04kc
traefik.http.routers.https-0-u8wo48ww804owcggk0sc04kc.entryPoints=https
traefik.http.routers.https-0-u8wo48ww804owcggk0sc04kc.middlewares=gzip
traefik.http.routers.https-0-u8wo48ww804owcggk0sc04kc.rule=Host(`dev.api.v2.quiz.aleos.digital`) && PathPrefix(`/`)
traefik.http.routers.https-0-u8wo48ww804owcggk0sc04kc.service=https-0-u8wo48ww804owcggk0sc04kc
traefik.http.routers.https-0-u8wo48ww804owcggk0sc04kc.tls.certresolver=letsencrypt
traefik.http.routers.https-0-u8wo48ww804owcggk0sc04kc.tls=true
traefik.http.services.http-0-u8wo48ww804owcggk0sc04kc.loadbalancer.server.port=3000
traefik.http.services.https-0-u8wo48ww804owcggk0sc04kc.loadbalancer.server.port=3000
caddy_0.encode=zstd gzip
caddy_0.handle_path.0_reverse_proxy={{upstreams 3000}}
caddy_0.handle_path=/*
caddy_0.header=-Server
caddy_0.try_files={path} /index.html /index.php
caddy_0=https://dev.api.v2.quiz.aleos.digital
caddy_ingress_network=coolify
traefik.http.middlewares.headers.headers.customResponseHeaders.Content-Security-Policy=default-src * 'unsafe-inline' 'unsafe-eval'; connect-src * 'unsafe-inline'; img-src * data: blob: 'unsafe-inline'; style-src * 'unsafe-inline';
traefik.http.middlewares.headers.headers.customResponseHeaders.X-Frame-Options=SAMEORIGIN
traefik.http.middlewares.headers.headers.customResponseHeaders.X-Content-Type-Options=nosniff
traefik.http.routers.your-service.middlewares=headers
1