# Docker Container & Web-Hosted ImGui Frontend **Date:** 2026-06-02 **Status:** Draft (pending review) **Reference:** https://imgui-bundle.pages.dev/explorer/ — imgui-bundle web backend via Hello ImGui --- ## Context & Motivation The user wants to deploy Manual Slop on Unraid (a home server OS) and access the GUI via a web browser. The goal is for agents to operate on projects hosted on the home server, with the user monitoring/controlling via web browser. Current state: - `sloppy.py` is a desktop GUI (ImGui via imgui-bundle + Python) - `src/api_hooks.py` provides a FastAPI/Uvicorn headless service on `:8999` for external automation - The app is Windows-oriented (PowerShell subprocesses, `pywin32` for window frame) Target state: - Docker container with the full app - Web browser shows the ImGui GUI in real-time - Agents can interact via the existing Hook API on `:8999` - The user's Unraid server can host multiple project directories imgui-bundle's web backend ([reference](https://imgui-bundle.pages.dev/explorer/)) uses a server-side render with a client-side WebGL display. The Hello ImGui runner pairs a Python render loop with a JavaScript WebGL canvas via WebSocket. --- ## Scope ### In Scope - `Dockerfile` — Container build for the Manual Slop app - `docker-compose.yml` — Multi-container deployment for Unraid - `scripts/docker_build.sh` — Build helper - `scripts/docker_run.sh` — Run helper with env var wiring - `docs/guide_docker_deployment.md` — Unraid setup guide - `tests/test_docker_build.py` — Opt-in Docker build test ### Out of Scope - Migrating `sloppy.py` to use the imgui-bundle web backend (the web backend is an alternative to the desktop backend; switching is a significant refactor and may be deferred) - Multi-user authentication (single-user deployment) - Cloud-specific deployment (AWS, GCP) — Unraid is the target - TLS termination (assumed handled by a reverse proxy like Traefik or Caddy) --- ## Design ### Architecture: V2 — Server-side Python + WebGL client (via WebSocket) ``` ┌─────────────────────────────────────────────┐ │ Docker Container (unraid:manual_slop:latest) │ │ │ │ ┌────────────────────────────────────┐ │ │ │ Python app │ │ │ │ - ImGui renders to framebuffer │ │ │ │ - Hello ImGui web backend: │ │ │ │ - Python: render loop │ │ │ │ - WebSocket: frame deltas │ │ │ │ - HTTP: serves JS client │ │ │ │ - HookServer on :8999 │ │ │ └────────────────────────────────────┘ │ │ │ │ Exposed ports: │ │ - 8080: Web client (HTTP + WS) │ │ - 8999: Hook API │ │ │ │ Volumes: │ │ - /projects: project workspaces │ │ - /config: app state, presets, personas │ └─────────────────────────────────────────────┘ ↑ ↑ │ Browser (Chrome, Firefox) │ Agent (curl, scripts) │ WebSocket for live frames │ HTTP for state ``` ### Dockerfile ```dockerfile FROM python:3.11-slim # System deps RUN apt-get update && apt-get install -y --no-install-recommends \ git curl ca-certificates \ && rm -rf /var/lib/apt/lists/* # Install uv RUN pip install uv # App setup WORKDIR /app COPY pyproject.toml uv.lock ./ RUN uv sync --frozen COPY . . # Volumes RUN mkdir -p /projects /config VOLUME ["/projects", "/config"] # Expose EXPOSE 8080 8999 # Health check HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \ CMD curl -f http://127.0.0.1:8999/status || exit 1 # Entrypoint ENTRYPOINT ["uv", "run", "sloppy.py", "--enable-test-hooks", "--web-host=0.0.0.0", "--web-port=8080"] ``` ### `docker-compose.yml` ```yaml version: '3.8' services: manual_slop: build: . image: manual_slop:latest container_name: manual_slop ports: - "8999:8999" # Hook API (host) - "8080:8080" # Web client (host) volumes: - /mnt/user/projects:/projects:rw # Unraid project share - /mnt/user/appdata/manual_slop:/config:rw # App state environment: - GEMINI_API_KEY=${GEMINI_API_KEY} - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} - DEEPSEEK_API_KEY=${DEEPSEEK_API_KEY} - MINIMAX_API_KEY=${MINIMAX_API_KEY} restart: unless-stopped healthcheck: test: ["CMD", "curl", "-f", "http://127.0.0.1:8999/status"] interval: 30s timeout: 5s retries: 3 start_period: 30s ``` ### Entry Point Changes (`sloppy.py`) The current `sloppy.py` launches the desktop GUI. For web mode, we need: ```python # In sloppy.py import argparse parser = argparse.ArgumentParser() # ... existing args ... parser.add_argument("--web-host", default=None, help="Enable web mode and bind to this host") parser.add_argument("--web-port", type=int, default=8080, help="Web mode port") args = parser.parse_args() if args.web_host is not None: from imgui_bundle import hello_imgui runner_params = hello_imgui.RunnerParams() runner_params.app_window_params.borderless = False runner_params.imgui_window_params.default_imgui_window_type = ... # web backend hello_imgui.run(runner_params) else: # Existing desktop launch ... ``` The imgui-bundle web backend is selected by the Hello ImGui runner. The exact config is per the [imgui-bundle explorer docs](https://imgui-bundle.pages.dev/explorer/). ### `docs/guide_docker_deployment.md` A complete Unraid setup guide: - Prerequisites (Unraid version, Docker template) - Building the image - Configuring volumes and env vars - Accessing the web client (URL, browser requirements) - Agent interaction examples (curl, Python script) - Backup and restore of /config - Updating the image ### `tests/test_docker_build.py` ```python import os import subprocess import time import pytest import requests IMAGE_NAME = "manual_slop:test" CONTAINER_NAME = "manual_slop_test" WEB_PORT = 8080 HOOK_PORT = 8999 @pytest.mark.docker def test_docker_container_starts_and_serves(tmp_path): """Build the Docker image, run the container, verify web client + hook API.""" if os.environ.get("RUN_DOCKER_TEST") != "1": pytest.skip("Set RUN_DOCKER_TEST=1 to enable") if not _docker_available(): pytest.skip("Docker not available") # Build result = subprocess.run( ["docker", "build", "-t", IMAGE_NAME, "."], capture_output=True, text=True, timeout=300, ) assert result.returncode == 0, f"Docker build failed: {result.stderr}" # Run subprocess.run(["docker", "rm", "-f", CONTAINER_NAME], capture_output=True) result = subprocess.run([ "docker", "run", "-d", "--name", CONTAINER_NAME, "-p", f"{WEB_PORT}:8080", "-p", f"{HOOK_PORT}:8999", IMAGE_NAME, ], capture_output=True, text=True, timeout=30) assert result.returncode == 0, f"Docker run failed: {result.stderr}" try: # Wait for hook API start = time.time() ready = False while time.time() - start < 60: try: r = requests.get(f"http://127.0.0.1:{HOOK_PORT}/status", timeout=1) if r.status_code == 200: ready = True break except (requests.ConnectionError, requests.Timeout): pass time.sleep(1) assert ready, "Container did not start hook API within 60s" # Verify web client is served r = requests.get(f"http://127.0.0.1:{WEB_PORT}/", timeout=5) assert r.status_code == 200 assert b" bool: result = subprocess.run(["docker", "version"], capture_output=True) return result.returncode == 0 ``` --- ## File Structure - `Dockerfile` — NEW - `docker-compose.yml` — NEW - `scripts/docker_build.sh` — NEW - `scripts/docker_run.sh` — NEW - `docs/guide_docker_deployment.md` — NEW - `tests/test_docker_build.py` — NEW - `sloppy.py` — MODIFY: add `--web-host` and `--web-port` args --- ## Acceptance Criteria - `docker build -t manual_slop:latest .` succeeds on a clean machine - `docker compose up` starts the container, and `:8999/status` returns 200 within 60s - `curl http://localhost:8080/` returns the web client HTML - An agent can `curl http://localhost:8999/api/gui/mma_status` and get a valid response - The user can navigate to the web UI in a browser and see the ImGui panels - File operations on `/projects` persist across container restarts - Env vars for API keys are not committed to the image (use runtime env) --- ## Risks 1. **imgui-bundle web backend maturity:** The web backend is less battle-tested than the desktop backend. There may be rendering quirks, input latency, or unsupported features. Mitigation: this is experimental; expect to iterate. 2. **Headless rendering in container:** Some ImGui features (e.g., font hinting) may need extra config for headless rendering. Mitigation: test early in development; fall back to Xvfb + noVNC if web backend is too immature. 3. **WebSocket bandwidth:** Streaming frame deltas requires consistent network. On flaky networks, the user experience degrades. Mitigation: implement client-side prediction or reduce frame rate. 4. **Container size:** Python + uv + all deps can produce a 1-2GB image. Mitigation: use multi-stage builds; pin Python deps for reproducibility. 5. **Unraid-specific quirks:** Unraid uses a specific Docker storage driver and may have path mapping edge cases. Mitigation: test on the actual Unraid deployment; document the path mapping clearly.