diff --git a/.gitea/workflows/build-hermes.yml b/.gitea/workflows/build-hermes.yml new file mode 100644 index 0000000..8684f16 --- /dev/null +++ b/.gitea/workflows/build-hermes.yml @@ -0,0 +1,31 @@ +name: Build Hermes agent +on: + pull_request: + branches: [ master ] + paths: + - 'ai/hermes/**' + - 'ai/compose.yml' + push: + branches: [ master ] + paths: + - 'ai/hermes/**' + - 'ai/compose.yml' + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + run: | + git clone -b "${{ github.head_ref || github.ref_name }}" \ + https://gitea:${{ secrets.GITHUB_TOKEN }}@code.lazyworkhorse.net/gortium/compose.git . + git log --oneline -3 + + - name: Build hermes image + run: | + cd ai + docker compose build hermes 2>&1 + + - name: Verify image + run: | + docker run --rm ai-hermes /opt/hermes/.venv/bin/python --version 2>&1 diff --git a/.gitea/workflows/build-ollama.yml b/.gitea/workflows/build-ollama.yml new file mode 100644 index 0000000..69b622a --- /dev/null +++ b/.gitea/workflows/build-ollama.yml @@ -0,0 +1,31 @@ +name: Build ollama (gfx906) +on: + pull_request: + branches: [ master ] + paths: + - 'ai/ollama/**' + - 'ai/compose.yml' + push: + branches: [ master ] + paths: + - 'ai/ollama/**' + - 'ai/compose.yml' + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + run: | + git clone -b "${{ github.head_ref || github.ref_name }}" \ + https://gitea:${{ secrets.GITHUB_TOKEN }}@code.lazyworkhorse.net/gortium/compose.git . + git log --oneline -3 + + - name: Build ollama image + run: | + cd ai + docker compose build ollama --no-cache 2>&1 + + - name: Verify version + run: | + docker run --rm ollama/ollama:rocm-gfx906 ollama --version 2>&1 diff --git a/ai/compose.yml b/ai/compose.yml index e96993f..1f4d58c 100755 --- a/ai/compose.yml +++ b/ai/compose.yml @@ -1,39 +1,40 @@ version: "3.8" services: - webui: - image: ghcr.io/open-webui/open-webui:main - volumes: - - /mnt/HoardingCow_docker_data/Ollama/open-webui:/app/backend/data - restart: always - environment: - - OLLAMA_API_BASE_URL=http://ollama:11434/api - networks: - - ai_net - - ai_backend - labels: - - "traefik.enable=true" + # webui: + # image: ghcr.io/open-webui/open-webui:main + # volumes: + # - /mnt/HoardingCow_docker_data/Ollama/open-webui:/app/backend/data + # restart: always + # environment: + # - OLLAMA_API_BASE_URL=http://ollama:11434/api + # networks: + # - ai_net + # - ai_backend + # labels: + # - "traefik.enable=true" - # Router for HTTP + redirection to HTTPS - - "traefik.http.routers.webui-http.rule=Host(`ai.lazyworkhorse.net`)" - - "traefik.http.routers.webui-http.entrypoints=web" - - "traefik.http.routers.webui-http.middlewares=redirect-to-https" + # # Router for HTTP + redirection to HTTPS + # - "traefik.http.routers.webui-http.rule=Host(`ai.lazyworkhorse.net`)" + # - "traefik.http.routers.webui-http.entrypoints=web" + # - "traefik.http.routers.webui-http.middlewares=redirect-to-https" - # Router for HTTPS with TLS - - "traefik.http.routers.webui-https.rule=Host(`ai.lazyworkhorse.net`)" - - "traefik.http.routers.webui-https.entrypoints=websecure" - - "traefik.http.routers.webui-https.tls=true" - - "traefik.http.routers.webui-https.tls.certresolver=njalla" + # # Router for HTTPS with TLS + # - "traefik.http.routers.webui-https.rule=Host(`ai.lazyworkhorse.net`)" + # - "traefik.http.routers.webui-https.entrypoints=websecure" + # - "traefik.http.routers.webui-https.tls=true" + # - "traefik.http.routers.webui-https.tls.certresolver=njalla" hermes: - image: nousresearch/hermes-agent:latest + build: + context: ./hermes + ssh: + - default container_name: hermes + entrypoint: ["/bin/bash", "-c", + "bash /opt/data/hermes-tools/install.sh && . /opt/hermes/.venv/bin/activate && uv pip install openai 'mautrix[encryption]' -q && exec /usr/bin/tini -g -- /opt/hermes/docker/entrypoint.sh \"$@\"", + "hermes-entrypoint"] restart: always - # Gateway run enables the internal API server on port 8642 - # Install openai and mautrix[encryption] for Matrix bridge with E2EE support on startup - # Uses uv (modern Python package manager) with --system flag for venv installation - entrypoint: > - sh -c "/opt/hermes/.venv/bin/uv pip install openai mautrix[encryption] --system -q && /opt/hermes/.venv/bin/hermes gateway run" environment: - OLLAMA_HOST=http://ollama:11434 - API_SERVER_ENABLED=true @@ -42,8 +43,20 @@ services: - API_SERVER_KEY=hermes_local_key - GATEWAY_ALLOW_ALL_USERS=true - OPENROUTER_API_KEY=${OPENROUTER_API_KEY} + # ROCm for GPU-accelerated faster-whisper STT + - HSA_OVERRIDE_GFX_VERSION=9.0.6 + - HCC_AMDGPU_TARGET=gfx906 + - HIP_VISIBLE_DEVICES=0,1 + - ROCR_VISIBLE_DEVICES=0,1 + - HSA_ENABLE_SDMA=0 + - TZ=America/Montreal volumes: - /mnt/HoardingCow_docker_data/Hermes/data:/opt/data + # Syncthing-shared org files — read-only view of user's agenda + - /mnt/HoardingCow_docker_data/Syncthing/telos-ro:/opt/data/telos-ro:ro + # Syncthing-shared inbox — write tasks here, they sync to user's laptop + - /mnt/HoardingCow_docker_data/Syncthing/telos-rw:/opt/data/telos-rw:rw + # Persist Python venv across container recreation (Matrix bridge deps, etc.) - /mnt/HoardingCow_docker_data/Hermes/venv:/opt/hermes/.venv devices: - /dev/kfd:/dev/kfd @@ -54,10 +67,41 @@ services: networks: - ai_backend + syncthing: + image: syncthing/syncthing:latest + container_name: syncthing + hostname: syncthing + restart: always + ports: + - "8384:8384" + - "22000:22000" + - "21027:21027/udp" + environment: + - TZ=America/Montreal + volumes: + - /mnt/HoardingCow_docker_data/Syncthing/config:/var/syncthing/config + - /mnt/HoardingCow_docker_data/Syncthing/telos-ro:/telos-ro + - /mnt/HoardingCow_docker_data/Syncthing/telos-rw:/telos-rw + networks: + - ai_backend + - ai_net + labels: + - "traefik.enable=true" + - "traefik.http.routers.syncthing-http.rule=Host(`syncthing.lazyworkhorse.net`)" + - "traefik.http.routers.syncthing-http.entrypoints=web" + - "traefik.http.routers.syncthing-http.middlewares=redirect-to-https" + - "traefik.http.routers.syncthing-https.rule=Host(`syncthing.lazyworkhorse.net`)" + - "traefik.http.routers.syncthing-https.entrypoints=websecure" + - "traefik.http.routers.syncthing-https.tls=true" + - "traefik.http.routers.syncthing-https.tls.certresolver=njalla" + - "traefik.http.services.syncthing.loadbalancer.server.port=8384" + ollama: - image: ollama/ollama:latest + build: + context: ./ollama + dockerfile: Dockerfile + image: ollama/ollama:rocm-gfx906 container_name: ollama - privileged: true tty: true restart: always ports: @@ -75,7 +119,7 @@ services: - HSA_ENABLE_SDMA=0 - OLLAMA_HOST=0.0.0.0 - OLLAMA_DEBUG=1 - - OLLAMA_FLASH_ATTENTION=0 + - OLLAMA_FLASH_ATTENTION=1 - OLLAMA_NUM_PARALLEL=2 devices: # Map the render nodes and KFD for ROCm to work inside the container diff --git a/ai/hermes/Dockerfile b/ai/hermes/Dockerfile new file mode 100644 index 0000000..a6edcfc --- /dev/null +++ b/ai/hermes/Dockerfile @@ -0,0 +1,93 @@ +# syntax=docker/dockerfile:1 +# Hermes Agent -- custom fork build +# Builds on top of official image + overlays our forked source from Gitea. +# Requires Docker BuildKit. Pass SSH agent for git clone: +# docker compose build hermes +# Or manually: +# DOCKER_BUILDKIT=1 docker build --ssh default -t hermes-agent:custom . + +# ---------- Base: official Hermes image (system deps, npm, uv, Playwright) ---------- +FROM nousresearch/hermes-agent:latest + +# ---------- Overlay our forked source ---------- +# Uses SSH agent forwarding from the build host (no key baked into image). +# --exclude node_modules/.venv keeps the base image's pre-built layers intact. +# Only the Python source, web UI source, and config change. +RUN --mount=type=ssh \ + mkdir -p /root/.ssh && \ + ssh-keyscan -p 2222 code.lazyworkhorse.net >> /root/.ssh/known_hosts 2>/dev/null && \ + cd /tmp && \ + GIT_SSH_COMMAND='ssh -p 2222 -o StrictHostKeyChecking=no' \ + git clone --depth 1 --branch main \ + git@code.lazyworkhorse.net:gortium/hermes-agent.git fork && \ + rsync -a --delete fork/ /opt/hermes/ \ + --exclude node_modules \ + --exclude .venv \ + --exclude .git && \ + rm -rf /tmp/fork /root/.ssh/ + +# ---------- Rebuild web UI ---------- +# Source files changed; node_modules (from base image) reused. +RUN cd /opt/hermes && npm run build + +# ---------- Reinstall Python package (editable) ---------- +# Picks up source changes from our fork. +RUN . /opt/hermes/.venv/bin/activate && \ + uv pip install --no-cache-dir --no-deps -e /opt/hermes + +# ---------- Extra system deps ---------- +USER root +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + libportaudio2 ca-certificates poppler-utils imagemagick \ + texlive-latex-base texlive-latex-extra texlive-fonts-recommended \ + texlive-xetex texlive-science \ + qemu-user-static binfmt-support emacs-nox && \ + rm -rf /var/lib/apt/lists/* + +# ---------- UV ---------- +COPY --chmod=0755 --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/ + +# ---------- Piper TTS ---------- +RUN . /opt/hermes/.venv/bin/activate && \ + uv pip install --no-cache-dir piper-tts sounddevice numpy && \ + mkdir -p /opt/hermes/.venv/share/piper/voices + +RUN /opt/hermes/.venv/bin/python3 /dev/stdin << 'PYEOF' +import urllib.request +base = '/opt/hermes/.venv/share/piper/voices' +url = 'https://huggingface.co/rhasspy/piper-voices/resolve/main/en/en_US/ryan/high/en_US-ryan-high.onnx' +urllib.request.urlretrieve(url, base + '/en_US-ryan-high.onnx') +urllib.request.urlretrieve(url + '.json', base + '/en_US-ryan-high.onnx.json') +PYEOF + +# ---------- Install Himalaya email CLI ---------- +RUN /opt/hermes/.venv/bin/python3 /dev/stdin << 'PYEOF' +import urllib.request, tarfile, os, shutil +url = 'https://github.com/pimalaya/himalaya/releases/download/v1.2.0/himalaya.x86_64-linux.tgz' +tgz = '/tmp/himalaya.tgz' +urllib.request.urlretrieve(url, tgz) +with tarfile.open(tgz) as t: + t.extractall('/tmp') +shutil.move('/tmp/himalaya', '/usr/local/bin/himalaya') +os.chmod('/usr/local/bin/himalaya', 0o755) +os.remove(tgz) +print('himalaya v1.2.0 installed') +PYEOF + +# ---------- Install himalaya-ro wrapper ---------- +COPY --chmod=0755 himalaya-ro.sh /usr/local/bin/himalaya-ro + + +# ---------- Runtime ---------- +USER hermes +ENV HERMES_HOME=/opt/data +ENV PATH="/opt/data/.local/bin:${PATH}" +# Point browser tool to Playwright's Chromium (already in base image) +ENV CHROME_EXECUTABLE=/opt/hermes/.playwright/chromium/chrome-linux/chrome + +# Ensure tools directory and toolsets.py are writable by the hermes runtime user +# so custom tools can be injected from the persistent volume at startup. +RUN chown -R hermes:hermes /opt/hermes/tools /opt/hermes/toolsets.py + +VOLUME [ "/opt/data" ] \ No newline at end of file diff --git a/ai/hermes/himalaya-ro.sh b/ai/hermes/himalaya-ro.sh new file mode 100644 index 0000000..212f1ae --- /dev/null +++ b/ai/hermes/himalaya-ro.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash +# ───────────────────────────────────────────────────────────── +# himalaya-ro — Read-only wrapper for himalaya +# +# Blocks destructive commands and logs audit trail. +# Pass-through for read-only commands (list, read, search). +# +# Usage: himalaya-ro [options] [args...] +# +# Install: place in PATH before the real himalaya, or use +# `ln -sf himalaya-ro /usr/local/bin/himalaya` +# ───────────────────────────────────────────────────────────── +set -o pipefail + +# ── Configuration ─────────────────────────────────────────── +HIMALAYA_BIN="${HIMALAYA_BIN:-/usr/local/bin/himalaya}" +AUDIT_LOG="${HIMALAYA_AUDIT_LOG:-/var/log/himalaya-audit.log}" + +# ── Destructive commands we block ────────────────────────── +BLOCKED_CMDS=( + "message move" + "message delete" + "message copy" + "flag add" + "flag remove" + "folder create" + "folder delete" + "folder rename" + "template send" + "account configure" + "account delete" +) + +# ── Determine the subcommand being invoked ───────────────── +# Strip leading options (--account, --output, etc.) to find the verb +ARGS=() +SKIP_NEXT=false +for arg in "$@"; do + if $SKIP_NEXT; then + SKIP_NEXT=false + continue + fi + if [[ "$arg" == --* ]]; then + case "$arg" in + --account|--output|--page|--page-size|--folder|--color|--format) + SKIP_NEXT=true ;; + esac + continue + fi + ARGS+=("$arg") +done + +# Build subcommand string and check against blocklist +CMD_STR="" +for ((i=0; i<${#ARGS[@]}; i++)); do + if [ -z "$CMD_STR" ]; then + CMD_STR="${ARGS[$i]}" + else + CMD_STR="$CMD_STR ${ARGS[$i]}" + fi + for blocked in "${BLOCKED_CMDS[@]}"; do + if [[ "$CMD_STR" == "$blocked" ]]; then + TS=$(date '+%Y-%m-%d %H:%M:%S') + echo "[AUDIT] $TS BLOCKED: himalaya $*" >> "$AUDIT_LOG" + echo "ERROR: Command 'himalaya $CMD_STR ...' is blocked by read-only policy." >&2 + echo " Audit log: $AUDIT_LOG" >&2 + exit 100 + fi + done +done + +# ── Allow pass-through ───────────────────────────────────── +exec "$HIMALAYA_BIN" "$@" diff --git a/ai/hermes/patch_tts_tool.py b/ai/hermes/patch_tts_tool.py new file mode 100644 index 0000000..0aa056b --- /dev/null +++ b/ai/hermes/patch_tts_tool.py @@ -0,0 +1,181 @@ +#!/usr/bin/env python3 +"""Patch Hermes TTS tool: add Piper TTS provider, remove Edge TTS as default. + +Patches ALL copies of tts_tool.py found (venv site-packages + /opt/hermes/tools/). + +Searches multiple paths for tts_tool.py so it works both at build time +(in the image venv) and at runtime (on the mounted data volume). + +Idempotent: if already patched, does nothing. +""" + +import sys +import os + +# --------------------------------------------------------------------------- +# Search for all copies of tts_tool.py +# --------------------------------------------------------------------------- +CANDIDATE_PATHS = [ + "/opt/hermes/.venv/lib/python3.13/site-packages/tools/tts_tool.py", + "/opt/hermes/tools/tts_tool.py", +] + +found_paths = [] + +for p in CANDIDATE_PATHS: + if os.path.exists(p): + found_paths.append(p) + print(f"Found tts_tool.py at: {p}") + +# Also try to find via Python import +import subprocess +try: + result = subprocess.run( + [sys.executable, "-c", "import tools.tts_tool; print(tools.tts_tool.__file__)"], + capture_output=True, text=True, timeout=5 + ) + if result.returncode == 0: + p = result.stdout.strip() + if os.path.exists(p) and p not in found_paths: + found_paths.append(p) + print(f"Found tts_tool.py via import at: {p}") +except Exception: + pass + +if not found_paths: + print("WARNING: tts_tool.py not found anywhere. Patching deferred to runtime.") + print(f"Searched: {CANDIDATE_PATHS}") + sys.exit(0) + +# --------------------------------------------------------------------------- +# Old else block: the Edge TTS default fallback to replace +# --------------------------------------------------------------------------- +old_else = ''' else: + # Default: Edge TTS (free), with NeuTTS as local fallback + edge_available = True + try: + _import_edge_tts() + except ImportError: + edge_available = False + + if edge_available: + logger.info("Generating speech with Edge TTS...") + try: + import concurrent.futures + with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool: + pool.submit( + lambda: asyncio.run(_generate_edge_tts(text, file_str, tts_config)) + ).result(timeout=60) + except RuntimeError: + asyncio.run(_generate_edge_tts(text, file_str, tts_config)) + elif _check_neutts_available(): + logger.info("Edge TTS not available, falling back to NeuTTS (local)...") + provider = "neutts" + _generate_neutts(text, file_str, tts_config) + else: + return json.dumps({ + "success": False, + "error": "No TTS provider available. Install edge-tts (pip install edge-tts) " + "or set up NeuTTS for local synthesis." + }, ensure_ascii=False)''' + +# --------------------------------------------------------------------------- +# New block: elif provider == "piper" + else: fallback with Piper only +# --------------------------------------------------------------------------- +new_block = ''' elif provider == "piper": + # Piper TTS (local, CPU, no cloud, no Microsoft) + piper_binary = "/opt/hermes/.venv/bin/piper" + piper_config = tts_config.get("piper", {}) + voice = piper_config.get("voice", "en_US-lessac-medium") + model_dir = piper_config.get("model_dir", "/opt/hermes/.venv/share/piper/voices") + model_path = os.path.join(model_dir, f"{voice}.onnx") + if not os.path.exists(model_path): + return json.dumps({ + "success": False, + "error": "Piper TTS voice model not found. " + "Install Piper TTS and download a voice model." + }, ensure_ascii=False) + logger.info("Generating speech with Piper TTS (local, CPU)...") + import subprocess as _sp + cmd = [piper_binary, "--model", model_path, "--output-raw"] + try: + proc = _sp.Popen(cmd, stdin=_sp.PIPE, stdout=_sp.PIPE, stderr=_sp.PIPE) + raw_audio, stderr = proc.communicate(input=text.encode(), timeout=60) + if proc.returncode != 0: + raise RuntimeError(f"Piper TTS failed: {stderr.decode()[:200]}") + ffmpeg_cmd = ["ffmpeg", "-f", "s16le", "-ar", "22050", "-ac", "1", "-i", "-", "-y", file_str] + _sp.run(ffmpeg_cmd, input=raw_audio, capture_output=True, timeout=30) + except Exception as e: + return json.dumps({ + "success": False, + "error": f"Piper TTS failed: {e}" + }, ensure_ascii=False) + + else: + # Default: Piper TTS (local, CPU, no cloud, no Microsoft) + piper_binary = "/opt/hermes/.venv/bin/piper" + piper_config = tts_config.get("piper", {}) + voice = piper_config.get("voice", "en_US-lessac-medium") + model_dir = piper_config.get("model_dir", "/opt/hermes/.venv/share/piper/voices") + model_path = os.path.join(model_dir, f"{voice}.onnx") + if os.path.exists(model_path) and os.path.exists(piper_binary): + logger.info("Generating speech with Piper TTS (local, CPU)...") + import subprocess as _sp + cmd = [piper_binary, "--model", model_path, "--output-raw"] + try: + proc = _sp.Popen(cmd, stdin=_sp.PIPE, stdout=_sp.PIPE, stderr=_sp.PIPE) + raw_audio, stderr = proc.communicate(input=text.encode(), timeout=60) + if proc.returncode != 0: + raise RuntimeError(stderr.decode()[:200]) + ffmpeg_cmd = ["ffmpeg", "-f", "s16le", "-ar", "22050", "-ac", "1", "-i", "-", "-y", file_str] + _sp.run(ffmpeg_cmd, input=raw_audio, capture_output=True, timeout=30) + except Exception: + pass + else: + return json.dumps({ + "success": False, + "error": "Piper TTS not available. Install piper-tts and download a voice model." + }, ensure_ascii=False)''' + +# --------------------------------------------------------------------------- +# Apply the patch to all copies found +# --------------------------------------------------------------------------- +patched_any = False + +for tts_path in found_paths: + with open(tts_path) as f: + code = f.read() + + if 'provider == "piper"' in code: + print(f"ALREADY PATCHED: {tts_path}") + continue + + if old_else in code: + code = code.replace(old_else, new_block, 1) + with open(tts_path, 'w') as f: + f.write(code) + print(f"PATCHED: {tts_path}") + patched_any = True + else: + print(f"SKIP {tts_path}: Edge fallback pattern not found") + import re + for m in re.finditer(r' else:\n # Default:', code): + start = max(0, m.start() - 100) + end = min(len(code), m.end() + 300) + print(f" Found 'else:/# Default:' at position {m.start()}:") + print(f" {code[start:end]}") + print(" ---") + # Don't exit with error — if one copy isn't patchable, try the others + +if not patched_any: + all_patched = all( + 'provider == "piper"' in open(p).read() + for p in found_paths + ) + if all_patched: + print("All copies already patched.") + sys.exit(0) + print("WARNING: Could not patch any copy of tts_tool.py") + sys.exit(1) + +print("tts_tool.py patched successfully across all copies.") diff --git a/ai/ollama/Dockerfile b/ai/ollama/Dockerfile new file mode 100644 index 0000000..438e607 --- /dev/null +++ b/ai/ollama/Dockerfile @@ -0,0 +1,106 @@ +# ollama-gfx906/Dockerfile +# +# Custom ollama image with ROCm 6.1 + gfx906 (MI50) support. +# The official ollama/rocm image ships ROCm 7.2 which dropped gfx906. +# This uses v0.23.2's native CMake build system with AMDGPU_TARGETS including gfx906. +# +# Build: docker build -t ollama/ollama:rocm-gfx906 ai/ollama + +FROM rocm/dev-ubuntu-22.04:6.1.2-complete AS builder + +# Build dependencies (CMake, Ninja, Go) +ARG CMAKEVERSION=3.31.2 +ARG NINJAVERSION=1.12.1 +ARG GOLANG_VERSION=1.22.0 + +RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y \ + curl git ccache build-essential pkg-config unzip \ + && rm -rf /var/lib/apt/lists/* + +# Install CMake from official binaries +RUN curl -fsSL https://github.com/Kitware/CMake/releases/download/v${CMAKEVERSION}/cmake-${CMAKEVERSION}-linux-x86_64.tar.gz \ + | tar xz -C /usr/local --strip-components 1 + +# Install Ninja +RUN curl -fsSL -o /tmp/ninja.zip \ + https://github.com/ninja-build/ninja/releases/download/v${NINJAVERSION}/ninja-linux.zip \ + && unzip /tmp/ninja.zip -d /usr/local/bin && rm /tmp/ninja.zip + +# Install Go +RUN curl -fsSL https://go.dev/dl/go${GOLANG_VERSION}.linux-amd64.tar.gz \ + | tar xz -C /usr/local +ENV PATH=/usr/local/go/bin:$PATH + +ARG OLLAMA_VERSION=v0.23.2 +RUN git clone --depth 1 --branch ${OLLAMA_VERSION} https://github.com/ollama/ollama.git /build +WORKDIR /build + +# ROCm paths +ENV HIP_PATH=/opt/rocm +ENV ROCM_PATH=/opt/rocm +ENV CMAKE_GENERATOR=Ninja +ENV LDFLAGS=-s + +# Step 1: Build CPU backends with GCC (no ROCm preset) +# Pre-set CMAKE_HIP_COMPILER="" to prevent check_language(HIP) from +# finding a HIP compiler (it searches /opt/rocm even without PATH). +# Remove /opt/rocm from PATH to prevent find_program from finding hipcc. +RUN mkdir -p build-cpu && \ + PATH=/usr/local/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \ + cmake -B build-cpu -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_HIP_COMPILER="" \ + -DCMAKE_INSTALL_PREFIX=/build/dist && \ + cmake --build build-cpu --target ggml-cpu -- -l $(nproc) && \ + cmake --install build-cpu --component CPU --strip && \ + echo "=== CPU install ===" && \ + (find /build/dist/lib/ollama -type f -o -type l 2>&1 | head -20 || echo "empty") + +# Step 2: Build HIP backend with ROCm preset + gfx906 target only +# The ROCm 6 preset enables HIP language detection (enable_language(HIP)) +# which ensures GPU kernels are properly compiled for gfx906. +# OLLAMA_RUNNER_DIR=rocm from the preset, so HIP goes to lib/ollama/rocm/ +# Need CMAKE_PREFIX_PATH so find_package(hip) finds hip-config.cmake +# at /opt/rocm/lib/cmake/hip/hip-config.cmake. +RUN mkdir -p build-hip && \ + cmake -B build-hip \ + --preset 'ROCm 6' \ + -DAMDGPU_TARGETS="gfx906:xnack-" \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_PREFIX_PATH="/opt/rocm" && \ + cmake --build build-hip --target ggml-hip -- -l $(nproc) && \ + cmake --install build-hip --component HIP --strip && \ + echo "=== HIP install ===" && \ + find /build/dist/lib/ollama -type f -o -type l | head -20 + +# Step 3: Build Go binary (GCC for CGo linking) +ENV CGO_ENABLED=1 +RUN go build -trimpath -ldflags="-X=github.com/ollama/ollama/version.Version=${OLLAMA_VERSION}" -o /build/dist/ollama . + +# ---------- Runtime image ---------- +FROM ubuntu:24.04 + +RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y \ + ca-certificates curl libstdc++6 libgomp1 libvulkan1 libopenblas0 \ + && rm -rf /var/lib/apt/lists/* + +# Copy ROCm 6.1 runtime libraries +# These are needed at runtime by ggml-hip via LD_LIBRARY_PATH +COPY --from=builder /opt/rocm/lib/ /opt/rocm/lib/ +COPY --from=builder /opt/rocm/share/ /opt/rocm/share/ + +# Copy ollama binary + all backends (CPU + HIP) +# CPU install: /build/dist/lib/ollama/libggml-*.so +# HIP install: /build/dist/lib/ollama/rocm/libggml-hip.so +COPY --from=builder /build/dist/ollama /usr/bin/ollama +COPY --from=builder /build/dist/lib/ollama/ /usr/lib/ollama/ + +RUN ldconfig + +ENV LD_LIBRARY_PATH=/opt/rocm/lib:/usr/lib/ollama/rocm:/usr/lib/ollama +ENV HSA_OVERRIDE_GFX_VERSION=9.0.6 +ENV HCC_AMDGPU_TARGET=gfx906 +ENV HSA_ENABLE_SDMA=0 + +EXPOSE 11434 +ENTRYPOINT ["/bin/ollama"] +CMD ["serve"] diff --git a/coms/compose.yml b/coms/compose.yml index 1036a7a..34897c0 100644 --- a/coms/compose.yml +++ b/coms/compose.yml @@ -1,15 +1,15 @@ version: "3.9" services: - nomadnet: - image: ghcr.io/markqvist/nomadnet:master - container_name: nomadnet - restart: always - volumes: - - /mnt/HoardingCow_docker_data/Nomadnet:/root/.nomadnetwork - - /mnt/HoardingCow_docker_data/Reticulum:/root/.reticulum - # Reticulum transport must be reachable directly (NOT through Traefik) - ports: - - "4242:4242" + # nomadnet: + # image: ghcr.io/markqvist/nomadnet:master + # container_name: nomadnet + # restart: always + # volumes: + # - /mnt/HoardingCow_docker_data/Nomadnet:/root/.nomadnetwork + # - /mnt/HoardingCow_docker_data/Reticulum:/root/.reticulum + # # Reticulum transport must be reachable directly (NOT through Traefik) + # ports: + # - "4242:4242" synapse: image: ghcr.io/element-hq/synapse:latest diff --git a/network/compose.yml b/network/compose.yml index 7642118..5a35eff 100644 --- a/network/compose.yml +++ b/network/compose.yml @@ -13,17 +13,20 @@ services: - "--certificatesresolvers.njalla.acme.storage=/letsencrypt/acme.json" - "--certificatesresolvers.njalla.acme.httpchallenge.entrypoint=web" - - "--log.level=DEBUG" + - "--log.level=INFO" + - "--log.filepath=/var/log/traefik/traefik.log" + - "--accesslog.filepath=/var/log/traefik/access.log" - "--providers.docker=true" - "--providers.docker.exposedByDefault=false" ports: - "80:80" - "443:443" environment: - - NJALLA_TOKEN=${NJALLA_TOKEN} + - NJALLA_TOKEN=*** volumes: - /var/run/docker.sock:/var/run/docker.sock:ro - /mnt/HoardingCow_docker_data/Traefik:/letsencrypt + - /var/log/traefik:/var/log/traefik restart: unless-stopped networks: - traefik_backend diff --git a/versioncontrol/compose.yml b/versioncontrol/compose.yml index 7e7b54e..01007ff 100644 --- a/versioncontrol/compose.yml +++ b/versioncontrol/compose.yml @@ -7,8 +7,11 @@ services: - USER_UID=1000 - USER_GID=1000 - GITEA__server__ROOT_URL=https://code.lazyworkhorse.net + - GITEA__actions__ENABLED=true - SSH_PORT=2222 - SSH_LISTEN_PORT=2222 + # Enable Gitea Actions (act_runner required on host) + - GITEA__actions__ENABLED=true volumes: - /mnt/HoardingCow_docker_data/Gitea:/data networks: @@ -40,6 +43,22 @@ services: # Internal Routing - "traefik.http.services.gitea.loadbalancer.server.port=3000" + act_runner: + image: gitea/act_runner:latest + container_name: act_runner + environment: + - GITEA_INSTANCE_URL=https://code.lazyworkhorse.net + - GITEA_RUNNER_REGISTRATION_TOKEN=${GITEA_RUNNER_TOKEN} + - GITEA_RUNNER_NAME=ai-host-runner + - GITEA_RUNNER_LABELS=ubuntu-latest:docker://catthehacker/ubuntu:full-22.04,nixos-builder:docker://nixos/nix + volumes: + - /var/run/docker.sock:/var/run/docker.sock + networks: + - vc_net + restart: always + depends_on: + - gitea + networks: vc_net: external: true diff --git a/vpn/Dockerfile b/vpn/Dockerfile new file mode 100644 index 0000000..f0c29c9 --- /dev/null +++ b/vpn/Dockerfile @@ -0,0 +1,9 @@ +# Custom wg-easy with iptables-nft (nftables-backed iptables) +# Fixes crash-loop when host kernel lacks legacy iptable_nat module. +FROM ghcr.io/wg-easy/wg-easy:latest + +# The upstream image registers only iptables-legacy with update-alternatives. +# iptables-nft binary exists but isn't registered as an alternative key. +# Override the alternatives-managed symlinks directly. +RUN ln -sf /usr/sbin/iptables-nft /usr/sbin/iptables && \ + ln -sf /usr/sbin/ip6tables-nft /usr/sbin/ip6tables diff --git a/vpn/compose.yml b/vpn/compose.yml new file mode 100644 index 0000000..cd14f27 --- /dev/null +++ b/vpn/compose.yml @@ -0,0 +1,38 @@ +version: "3.8" + +services: + wireguard: + build: + context: . + dockerfile: Dockerfile + image: wg-easy-iptables-nft:latest + container_name: wireguard + cap_add: + - NET_ADMIN + - SYS_MODULE + environment: + - WG_HOST=vpn.lazyworkhorse.net + - PASSWORD=${WG_PASSWORD} + - WG_PORT=51820 + - WG_DEFAULT_ADDRESS=10.8.0.x + - WG_DEFAULT_DNS=1.1.1.1,8.8.8.8 + - WG_ALLOWED_IPS=0.0.0.0/0, ::/0 + - WG_PERSISTENT_KEEPALIVE=25 + - UI_TRAFFIC_STATS=true + - UI_CHART_TYPE=0 + ports: + - "51820:51820/udp" + - "51821:51821/tcp" + volumes: + - /mnt/HoardingCow_docker_data/WireGuard:/etc/wireguard:rw + sysctls: + - net.ipv4.conf.all.src_valid_mark=1 + - net.ipv4.ip_forward=1 + restart: unless-stopped + networks: + - vpn_net + +networks: + vpn_net: + external: true + name: vpn_net