Dockerize: one-command stack with auto Ollama model pull
Some checks failed
Release / semantic-release (push) Successful in 22s
tests / Unit tests (Linux, Python 3.11) (push) Successful in 9m55s
Release / build-linux (push) Failing after 7m36s
Release / build-windows (push) Has been cancelled
Release / build-macos (arm64, macos-latest) (push) Has been cancelled
Release / build-macos (x64, macos-15-intel) (push) Has been cancelled
Release / release-main (push) Has been cancelled
Release / release-develop (push) Has been cancelled

`docker compose up -d --build` now brings up the whole thing automatically —
no host setup needed:

- All-in-one javis image: TigerVNC+XFCE desktop, Chrome, Python brain bridge,
  Node/bun bot, managed by supervisord (verified: all 6 programs RUNNING).
- ollama service + one-shot ollama-init that auto-pulls chat+embed models
  (verified end-to-end; `ollama list` shows pulled models).
- Discord token deferred: without DISCORD_BOT_TOKEN the desktop, bridge,
  Ollama and models all run; only the bot waits (no crash loop).
- Slim container deps (bridge/requirements-bridge.txt) drop the unused
  PyQt6/torch/chatterbox/sounddevice stack. Piper voice + Whisper models
  auto-download into named volumes.
- Configurable host ports (VNC_PORT/NOVNC_PORT/BRIDGE_PORT) to avoid clashing
  with a host VNC already on 5901. Bridge binds 0.0.0.0 in-container.

Verified: image builds; brain imports; bridge /health 200; noVNC 200;
X display :1 @1920x1080; auto-pull completes; supervisorctl status all RUNNING.
This commit is contained in:
javis-bot
2026-06-09 15:27:41 +09:00
parent c4abf63f38
commit 25c77ac794
14 changed files with 448 additions and 4 deletions

30
docker/download-piper.sh Executable file
View File

@@ -0,0 +1,30 @@
#!/usr/bin/env bash
# Download the default Piper voice model if it is not already present.
# Used both at image build time and (as a fallback) at container start.
set -euo pipefail
VOICE="${PIPER_VOICE:-en_GB-alan-medium}"
DEST_DIR="${PIPER_VOICE_DIR:-/opt/piper-voices}"
BASE="https://huggingface.co/rhasspy/piper-voices/resolve/v1.0.0"
# en_GB-alan-medium -> en/en_GB/alan/medium
lang2="${VOICE%%-*}" # en_GB
lang1="${lang2%%_*}" # en
rest="${VOICE#*-}" # alan-medium
name="${rest%%-*}" # alan
quality="${rest#*-}" # medium
path="${lang1}/${lang2}/${name}/${quality}"
mkdir -p "$DEST_DIR"
onnx="$DEST_DIR/${VOICE}.onnx"
json="$DEST_DIR/${VOICE}.onnx.json"
if [ -f "$onnx" ] && [ -f "$json" ]; then
echo "[piper] voice already present: $onnx"
exit 0
fi
echo "[piper] downloading voice $VOICE ..."
wget -q -O "$onnx" "${BASE}/${path}/${VOICE}.onnx"
wget -q -O "$json" "${BASE}/${path}/${VOICE}.onnx.json"
echo "[piper] saved to $onnx"

42
docker/entrypoint.sh Executable file
View File

@@ -0,0 +1,42 @@
#!/usr/bin/env bash
# Container entrypoint: render config from env, set the VNC password, ensure the
# Piper voice exists, then hand off to supervisord (which runs the desktop,
# bridge, and bot).
set -euo pipefail
# --- Defaults (override via .env / compose) ---
: "${VNC_PASSWORD:=javis123}"
: "${VNC_RESOLUTION:=1920x1080}"
: "${OLLAMA_BASE_URL:=http://ollama:11434}"
: "${OLLAMA_CHAT_MODEL:=llama3.1:8b}"
: "${OLLAMA_EMBED_MODEL:=nomic-embed-text}"
: "${WHISPER_MODEL:=small}"
: "${WHISPER_DEVICE:=cpu}"
: "${WHISPER_COMPUTE_TYPE:=int8}"
: "${JARVIS_DB_PATH:=/data/jarvis.db}"
: "${BRIDGE_HOST:=0.0.0.0}"
: "${BRIDGE_PORT:=8765}"
: "${PIPER_VOICE:=en_GB-alan-medium}"
: "${PIPER_VOICE_DIR:=/opt/piper-voices}"
: "${TTS_PIPER_MODEL_PATH:=${PIPER_VOICE_DIR}/${PIPER_VOICE}.onnx}"
export VNC_RESOLUTION OLLAMA_BASE_URL OLLAMA_CHAT_MODEL OLLAMA_EMBED_MODEL \
WHISPER_MODEL WHISPER_DEVICE WHISPER_COMPUTE_TYPE JARVIS_DB_PATH \
PIPER_VOICE PIPER_VOICE_DIR TTS_PIPER_MODEL_PATH BRIDGE_HOST BRIDGE_PORT
mkdir -p /data /app/config "$(dirname "$JARVIS_DB_PATH")"
# --- VNC password file ---
mkdir -p /root/.vnc
echo "$VNC_PASSWORD" | tigervncpasswd -f > /root/.vnc/passwd
chmod 600 /root/.vnc/passwd
# --- Render jarvis brain config from template ---
envsubst < /app/docker/jarvis-config.template.json > /app/config/jarvis.json
export JARVIS_CONFIG_PATH=/app/config/jarvis.json
# --- Ensure the Piper voice exists (best effort) ---
bash /app/docker/download-piper.sh || echo "[entrypoint] piper download failed; TTS may be unavailable"
echo "[entrypoint] display=$DISPLAY ollama=$OLLAMA_BASE_URL whisper=$WHISPER_MODEL/$WHISPER_DEVICE"
exec supervisord -c /app/docker/supervisord.conf

View File

@@ -0,0 +1,18 @@
{
"db_path": "${JARVIS_DB_PATH}",
"sqlite_vss_path": null,
"ollama_base_url": "${OLLAMA_BASE_URL}",
"ollama_embed_model": "${OLLAMA_EMBED_MODEL}",
"ollama_chat_model": "${OLLAMA_CHAT_MODEL}",
"tts_enabled": true,
"tts_engine": "piper",
"tts_piper_model_path": "${TTS_PIPER_MODEL_PATH}",
"whisper_model": "${WHISPER_MODEL}",
"whisper_backend": "faster-whisper",
"whisper_device": "${WHISPER_DEVICE}",
"whisper_compute_type": "${WHISPER_COMPUTE_TYPE}",
"location_enabled": true,
"web_search_enabled": true,
"wikipedia_fallback_enabled": true,
"mcps": {}
}

22
docker/run-bot.sh Executable file
View File

@@ -0,0 +1,22 @@
#!/usr/bin/env bash
# Wait for the brain bridge, then run the Discord bot.
#
# The Discord token is intentionally deferred: if DISCORD_BOT_TOKEN is not set
# yet, the rest of the stack (desktop, bridge, ollama) still runs fully. The bot
# just waits. Add the token to .env and `docker compose up -d` to start it.
set -e
cd /app/bot
if [ -z "${DISCORD_BOT_TOKEN:-}" ]; then
echo "[bot] DISCORD_BOT_TOKEN 미설정 — 봇 대기 중. .env에 토큰을 넣고 'docker compose up -d' 하면 시작됩니다."
echo "[bot] (그동안 VNC 데스크톱 / 브릿지 / Ollama 는 정상 동작합니다.)"
exec sleep infinity
fi
BRIDGE="${BRIDGE_URL:-http://127.0.0.1:8765}"
for i in $(seq 1 60); do
curl -fsS "$BRIDGE/health" >/dev/null 2>&1 && break
sleep 1
done
bun run register || echo "[bot] slash command registration failed (continuing)"
exec bun run start

14
docker/run-chrome.sh Executable file
View File

@@ -0,0 +1,14 @@
#!/usr/bin/env bash
# Wait for the desktop, then launch Chrome on :1 so the VNC screen shows a
# controllable browser (jarvis can also drive it). Runs as root -> --no-sandbox.
set -e
for i in $(seq 1 40); do
xdpyinfo -display :1 >/dev/null 2>&1 && break
sleep 1
done
sleep 3
export DISPLAY=:1
exec google-chrome \
--no-sandbox --no-first-run --disable-dev-shm-usage \
--password-store=basic --start-maximized \
"${CHROME_START_URL:-about:blank}"

12
docker/run-xfce.sh Executable file
View File

@@ -0,0 +1,12 @@
#!/usr/bin/env bash
# Wait for the X server, then start the XFCE session (with a dbus session).
set -e
for i in $(seq 1 30); do
xdpyinfo -display :1 >/dev/null 2>&1 && break
sleep 1
done
export DISPLAY=:1
export XDG_DATA_DIRS=/usr/local/share:/usr/share
export XDG_CONFIG_DIRS=/etc/xdg
# startxfce4 bails when X is already up; call the session directly.
exec dbus-launch --exit-with-session xfce4-session

10
docker/run-xvnc.sh Executable file
View File

@@ -0,0 +1,10 @@
#!/usr/bin/env bash
# Start the TigerVNC X server on display :1.
# NOTE: do NOT pass `-extension RENDER` — it blanks XFCE menus/panels
# (see docs/vnc-xfce-setup.md §3-4).
set -e
: "${VNC_RESOLUTION:=1920x1080}"
exec /usr/bin/Xvnc :1 \
-geometry "$VNC_RESOLUTION" -depth 24 \
-rfbport 5901 -rfbauth /root/.vnc/passwd \
-SecurityTypes VncAuth -localhost no -AlwaysShared

71
docker/supervisord.conf Normal file
View File

@@ -0,0 +1,71 @@
[supervisord]
nodaemon=true
user=root
logfile=/var/log/supervisord.log
pidfile=/run/supervisord.pid
[unix_http_server]
file=/run/supervisor.sock
[supervisorctl]
serverurl=unix:///run/supervisor.sock
[rpcinterface:supervisor]
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
[program:xvnc]
command=/app/docker/run-xvnc.sh
priority=100
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
[program:xfce]
command=/app/docker/run-xfce.sh
priority=200
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
[program:novnc]
command=websockify --web=/usr/share/novnc 6080 localhost:5901
priority=250
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
[program:bridge]
command=/opt/venv/bin/python -m bridge.server
directory=/app
priority=300
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
[program:chrome]
command=/app/docker/run-chrome.sh
priority=350
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
[program:bot]
command=/app/docker/run-bot.sh
directory=/app/bot
priority=400
autorestart=true
startretries=999
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0