feat: deploy LifeOS on hf-pdocker-02 (:8095)

Self-hosted LifeOS (github.com/lifeos-app/lifeos, pinned 3009bf6) in local
mode: no Supabase. Multi-stage build - Node builds the Vite bundle, gunicorn
serves it plus the bundled Flask/SQLite API from one process.

LAN-only on purpose: the backend is single-user with no auth, so anyone who
reaches the port has full read/write. Needs Traefik basicAuth before it can
ever be exposed publicly.

Two upstream bugs patched via sed (build fails loudly if they are renamed):
the chat model was hardcoded to the author's private 'sentient-teddy' bridge,
and a hardcoded 30s timeout failed every first message because Ollama needs
~46s to cold-load the model (0.7s warm). Both are env vars now.

SQLite stays on local disk - NFSv3 breaks WAL locking.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-11 11:18:46 -05:00
parent 41ee6452fc
commit 835a8d6644
3 changed files with 111 additions and 0 deletions
+51
View File
@@ -0,0 +1,51 @@
# 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 . .
# 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
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"]