Files
hobbs-iac/compose/hf-pdocker-02/lifeos/Dockerfile
T
jeremy f48c7a40cd fix: make LifeOS actually render in local mode
The container served a blank page. Four upstream assumptions broke a
root-served, Supabase-less deployment; all are patched at build time with
grep assertions so a silent upstream rename fails the build instead of
shipping a blank page again.

- React Router was pinned to basename '/app' (upstream is hosted under an
  /app path). At '/' it matched no route and rendered nothing -- silently,
  with only a console warning. This was the blank page.
- supabase.ts calls createClient() at module scope, which throws
  'supabaseUrl is required' on an empty string and killed the bundle before
  React mounted. Dummy values fix it; nothing reads Supabase in local mode.
- initAuth() only booted the local user for Tauri/Electron, so a browser
  build fell through to a Supabase login screen the backend cannot serve
  (it has no /api/auth/login or /api/auth/signup -- it is single-user).
- The local API adapter has no realtime layer, so supabase.channel() was
  undefined and every subscribing component tripped the ErrorBoundary
  ('Something went wrong'). Added no-op stubs; realtime is meaningless on a
  single-user instance.

Verified in a browser: dashboard and routed pages render, 0 fatal errors.

Known gap: the Flask backend implements only a subset of the cloud schema,
so quests, messages, partnerships, grocery_lists, recurring_transactions,
junction_traditions and habit_suggestions 404. Non-fatal; those features
degrade. Core (tasks/habits/goals/health/finance/journal/XP/AI chat) works.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 11:32:14 -05:00

83 lines
4.1 KiB
Docker

# LifeOS — https://github.com/lifeos-app/lifeos
#
# Two stages: Node builds the Vite/React bundle, Python serves that bundle plus
# the Flask API from a single process. Build context is the upstream source tree
# (cloned to /opt/lifeos on the node); this file is copied in alongside it.
# ─── Stage 1: build the frontend ──────────────────────────────────────
FROM node:22-bookworm-slim AS frontend
WORKDIR /build
COPY package.json package-lock.json ./
# --ignore-scripts skips the electron-builder postinstall, which pulls a desktop
# toolchain we never use and fails without an X stack.
RUN npm ci --ignore-scripts
COPY . .
# Upstream is hosted under an /app base path and hardcodes it in two places.
# We serve at the root, so both must be rewritten or the app renders a blank page:
# - App.tsx pins React Router to basename "/app"; at "/" it matches no route and
# renders NOTHING (silent -- only a console warning, no error).
# - index.html points the PWA manifest and touch icons at /app/...
RUN sed -i 's|basename: "/app"|basename: "/"|' src/App.tsx \
&& sed -i 's|"/app/|"/|g' index.html \
&& grep -q 'basename: "/"' src/App.tsx \
&& ! grep -q '"/app/' index.html
# initAuth() only boots the single local user for Tauri/Electron, so a browser
# build in local-api mode falls through to a Supabase login screen instead --
# one the backend cannot serve (it has no /api/auth/login or /api/auth/signup
# routes, being single-user by design). Treat local-api like the desktop envs.
RUN sed -i "s@if (detectedEnv === 'tauri' || detectedEnv === 'electron') {@if (detectedEnv === 'tauri' || detectedEnv === 'electron' || detectedEnv === 'local-api') {@" src/stores/useUserStore.ts \
&& grep -q "detectedEnv === 'local-api') {" src/stores/useUserStore.ts
# The local API adapter has no realtime layer, so supabase.channel() is undefined
# and every subscribing component crashes the app. Append no-op stubs.
COPY local-api-realtime-shim.ts /tmp/shim.ts
RUN cat /tmp/shim.ts >> src/lib/local-api.ts && rm /tmp/shim.ts
# Routes the data layer (src/lib/data-access.ts) to the local Flask API instead
# of Supabase Cloud. VITE_API_BASE_URL is deliberately left unset: local-api.ts
# then falls back to a relative /api, so the bundle works on any host and port.
ENV VITE_USE_LOCAL_API=true
# Dummy Supabase values. Nothing reads Supabase in local mode, but src/lib/supabase.ts
# calls createClient() at module scope, and createClient throws "supabaseUrl is
# required" on an empty string -- which kills the bundle before React mounts, leaving
# a blank page. Its own comment claims it only warns; it does not. A syntactically
# valid dummy URL constructs fine and is never called.
ENV VITE_SUPABASE_URL=https://noop.invalid
ENV VITE_SUPABASE_ANON_KEY=noop
ENV NODE_OPTIONS=--max-old-space-size=3072
RUN npm run build
# ─── Stage 2: Flask API + built frontend ──────────────────────────────
FROM python:3.12-slim
WORKDIR /app
COPY backend/requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt gunicorn
COPY backend/ /app/
COPY serve_container.py /app/
COPY --from=frontend /build/dist /app/dist
# Upstream hardcodes the author's own model name in the chat proxy, and a 30s
# timeout that a cold Ollama model load (~46s for a 9.6GB model) blows through.
# Make both env vars. Fails the build if upstream renames these out from under us.
RUN sed -i 's/"model": "sentient-teddy"/"model": os.environ.get("AI_MODEL", "sentient-teddy")/' app.py \
&& sed -i 's/timeout=30$/timeout=int(os.environ.get("AI_TIMEOUT", "120"))/' app.py \
&& grep -q 'os.environ.get("AI_MODEL"' app.py \
&& grep -q 'os.environ.get("AI_TIMEOUT"' app.py
# app.py hardcodes its SQLite path to ~/.lifeos/data.db, so HOME is what pins
# the database into the mounted volume.
ENV HOME=/data
RUN mkdir -p /data/.lifeos
EXPOSE 8080
CMD ["gunicorn", "--bind", "0.0.0.0:8080", "--workers", "2", "--threads", "4", \
"--timeout", "60", "--access-logfile", "-", "serve_container:app"]