-
Notifications
You must be signed in to change notification settings - Fork 9
Expand file tree
/
Copy pathDockerfile.cpu
More file actions
92 lines (79 loc) · 4.23 KB
/
Copy pathDockerfile.cpu
File metadata and controls
92 lines (79 loc) · 4.23 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
# Fulloch — CPU image (slim "tiny + remote" variant).
#
# No CUDA, no flash-attn, no llama-cpp, no Qwen speech. Ships Moonshine ASR +
# Kokoro TTS (both CPU) and the regex-only / remote-OpenAI LLM paths, so a
# low-power / no-GPU host can run the tiny tier or offload reasoning to a remote
# endpoint. Much smaller than the GPU image. The wizard hides GPU-only backends
# (FULLOCH_VARIANT=cpu).
# Stage 0: Build the Obsidian plugin bundle + downloadable zip from source, so
# the dashboard always serves what's in obsidian-plugin/main.ts rather than a
# stale hand-built artifact.
FROM node:20-slim AS obsidian-plugin-builder
WORKDIR /plugin
RUN apt-get update && apt-get install -y zip && rm -rf /var/lib/apt/lists/*
COPY obsidian-plugin/package.json obsidian-plugin/package-lock.json ./
RUN npm ci
COPY obsidian-plugin/main.ts obsidian-plugin/manifest.json obsidian-plugin/esbuild.config.mjs obsidian-plugin/tsconfig.json ./
RUN npm run build && zip fulloch.zip manifest.json main.js
FROM python:3.11-slim
ENV DEBIAN_FRONTEND=noninteractive
WORKDIR /app
ENV HF_HOME=/app/data/models
# HF_HUB_OFFLINE is managed at runtime by app.py (online for the first-run
# wizard download, offline once models are cached) — not pinned here.
# Image variant — the wizard offers only the CPU-runnable backends/tiers.
ENV FULLOCH_VARIANT=cpu
# Runtime system deps:
# - espeak-ng: Kokoro TTS grapheme-to-phoneme
# - libsndfile1: soundfile I/O; ffmpeg: audio decode
# - gcc: a couple of wheels JIT/compile at runtime
# Voice I/O is via the browser satellite WebSocket; no local sounddevice /
# PulseAudio / ALSA needed.
RUN apt-get update && apt-get install -y \
gcc \
ffmpeg \
espeak-ng \
libsndfile1 \
procps \
curl \
&& rm -rf /var/lib/apt/lists/*
# Route ALSA's default device through PulseAudio (same as the GPU image).
RUN printf 'pcm.!default { type pulse }\nctl.!default { type pulse }\n' > /etc/asound.conf
# CPU PyTorch from the CPU wheel index (keeps the CUDA build out), then the
# CPU dependency set.
RUN pip install --no-cache-dir torch==2.8.0 torchaudio==2.8.0 \
--index-url https://download.pytorch.org/whl/cpu
COPY requirements-cpu.txt /tmp/requirements-cpu.txt
RUN pip install --no-cache-dir -r /tmp/requirements-cpu.txt
RUN python -m spacy download en_core_web_sm
# Non-root user
RUN useradd -m -u 1000 -s /bin/bash appuser
USER appuser
# Application code
COPY --chown=appuser:appuser app.py fulloch.png parloch.png ./
COPY --chown=appuser:appuser core/ core/
COPY --chown=appuser:appuser tools/ tools/
COPY --chown=appuser:appuser utils/ utils/
COPY --chown=appuser:appuser audio/ audio/
COPY --chown=appuser:appuser server/ server/
# First-run seeds (copied into the empty ./data volume by core/bootstrap.py).
COPY --chown=appuser:appuser data/config.example.yml /app/seed/config.example.yml
COPY --chown=appuser:appuser data/models/grammars/agent.gbnf /app/seed/grammars/agent.gbnf
COPY --chown=appuser:appuser data/wav/ /app/seed/wav/
# Obsidian plugin zip, built from source in the obsidian-plugin-builder stage
# above — served by GET /api/obsidian/plugin.zip (server/dashboard.py).
COPY --from=obsidian-plugin-builder --chown=appuser:appuser /plugin/fulloch.zip /app/obsidian-plugin/fulloch.zip
# Entrypoint chowns the data dir to appuser on every boot — needed because
# Docker-named volumes start root-owned, and a fresh volume would otherwise
# fail the app's first write to /app/data. Idempotent (no-op when the dir
# is already correctly owned) and safe on read-only mounts.
COPY --chown=root:root --chmod=755 docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
# Liveness probe: the dashboard's GET /ready returns 200 as soon as the
# web server is up. A user who hits the URL right now sees the wizard,
# download progress, or an actionable error — never connection-refused.
# `docker compose ps` flips this to "healthy" the moment the UI is
# reachable. Task 7a of docs/ease-of-use-tasks.md.
HEALTHCHECK --interval=10s --timeout=3s --start-period=15s --retries=3 \
CMD python -c "import ssl,urllib.request,sys; ctx=ssl._create_unverified_context(); sys.exit(0 if urllib.request.urlopen('https://127.0.0.1:8765/ready', context=ctx, timeout=2).status == 200 else 1)"
CMD ["python", "app.py"]