diff --git a/.dockerignore b/.dockerignore index f8da17c..11c8dbe 100644 --- a/.dockerignore +++ b/.dockerignore @@ -9,10 +9,14 @@ lerna-debug.log* node_modules -# dist +# The local dist/ is a developer's last build, not part of the source. Shipping it +# into the build context only slows the upload and risks a stale artifact being +# picked up if `npm run build` ever fails to overwrite a file. +dist !sdk/dist dist-ssr *.local +.git # Editor directories and files .vscode/* diff --git a/Dockerfile b/Dockerfile index e822330..1529b4d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,13 +9,31 @@ WORKDIR /app # the server. Raise this only if the host gets more RAM. ENV NODE_OPTIONS=--max-old-space-size=1024 +# Vite inlines VITE_* vars into the bundle at build time, so these have to be +# present here — setting them as runtime env vars in Dokploy does nothing, the +# JS is already compiled by then. .env.* is gitignored, so a build from the repo +# has no other source for them: without these the app boots with a null app id, +# every API call resolves against our own origin, nginx answers with index.html +# and the browser shows a blank page. Defaults match this app so a plain +# `docker build` works; override via Dokploy's build args to point elsewhere. +ARG VITE_BASE44_APP_ID=68fc6cf01386035c266e7a5d +ARG VITE_BASE44_BACKEND_URL=https://base44.app +ENV VITE_BASE44_APP_ID=$VITE_BASE44_APP_ID +ENV VITE_BASE44_BACKEND_URL=$VITE_BASE44_BACKEND_URL + # Install deps from the lockfile first so this layer is reused when only src changes COPY package.json package-lock.json ./ RUN npm ci --no-audit --no-fund COPY . . + +# Fail loudly here rather than shipping an image that serves an empty page. +RUN test -n "$VITE_BASE44_APP_ID" || (echo "VITE_BASE44_APP_ID is empty — the build would produce a blank app" && exit 1) RUN npm run build +# The build is only useful if it actually emitted an entry point. +RUN test -f dist/index.html || (echo "vite build produced no dist/index.html" && exit 1) + # ---- Runtime stage ---- FROM nginx:alpine @@ -27,6 +45,14 @@ COPY --from=build /app/dist /usr/share/nginx/html COPY nginx.conf /etc/nginx/nginx.conf +# Catch a broken config at build time instead of at container start, where +# Dokploy would report the deploy as successful and then serve nothing. +RUN nginx -t + EXPOSE 80 3000 +# Gives Dokploy a real signal that the container is serving, not just running. +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD wget -q --spider http://127.0.0.1/health || exit 1 + CMD ["nginx", "-g", "daemon off;"] diff --git a/nginx.conf b/nginx.conf index 29af0b4..1ad25cb 100644 --- a/nginx.conf +++ b/nginx.conf @@ -49,12 +49,29 @@ http { root /usr/share/nginx/html; index index.html; + # Container liveness for Dokploy/Docker. Kept above the SPA fallback so it + # answers with a real status instead of 200-plus-index.html, which would + # make any health check pass even with the app completely broken. + location = /health { + access_log off; + add_header Content-Type text/plain; + return 200 "ok\n"; + } + # Vite writes content-hashed filenames here, so they can be cached forever. # Only add_header is used — combining it with `expires` emits two # conflicting Cache-Control headers. + # + # try_files ending in =404 is deliberate: without it a stale index.html + # asking for an asset from a previous deploy would fall through to the + # SPA rule below and receive index.html with a 200 and a text/html type. + # The browser refuses to execute that as a module and the page renders + # empty with only a MIME error in the console — a real 404 is far easier + # to diagnose and lets a reload recover. location /assets/ { add_header Cache-Control "public, max-age=31536000, immutable"; access_log off; + try_files $uri =404; } # index.html must be revalidated every time or deploys won't take effect @@ -65,6 +82,11 @@ http { location / { # This line forces Nginx to pass routing back to React Router try_files $uri $uri/ /index.html; + + # The fallback is served under the requested path, so the `= /index.html` + # rule above never applies to it. Without this a proxy or browser can + # cache the old HTML and keep loading assets that no longer exist. + add_header Cache-Control "no-cache"; } } } diff --git a/src/App.jsx b/src/App.jsx index f637a4d..b52371d 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -9,6 +9,8 @@ import { BrowserRouter as Router, Route, Routes, useLocation } from 'react-route import { setupIframeMessaging } from './lib/iframe-messaging'; import PageNotFound from './lib/PageNotFound'; import { AuthProvider, useAuth } from '@/lib/AuthContext'; +import ErrorBoundary from '@/lib/ErrorBoundary'; +import { appParams } from '@/lib/app-params'; import UserNotRegisteredError from '@/components/UserNotRegisteredError'; import FeatureReport from './pages/FeatureReport'; import LandingPage from './pages/LandingPage'; @@ -70,6 +72,22 @@ const AuthenticatedApp = () => { navigateToLogin(); return null; } + + // Anything else (bad/missing app id, unreachable backend, network failure) + // used to fall straight through to the app, which then crashed on the empty + // data and left a blank page. Say what happened instead. + return ( +
{authError.message || 'The Base44 backend could not be reached.'}
+
+ App ID: {String(appParams.appId)}
+ Backend: {String(appParams.serverUrl)}
+
+ The application failed to render. The details below are also in the browser console. +
+
+ {String(this.state.error?.stack || this.state.error)}
+
+
+