10 KiB
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.pyis a desktop GUI (ImGui via imgui-bundle + Python)src/api_hooks.pyprovides a FastAPI/Uvicorn headless service on:8999for external automation- The app is Windows-oriented (PowerShell subprocesses,
pywin32for 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) 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 appdocker-compose.yml— Multi-container deployment for Unraidscripts/docker_build.sh— Build helperscripts/docker_run.sh— Run helper with env var wiringdocs/guide_docker_deployment.md— Unraid setup guidetests/test_docker_build.py— Opt-in Docker build test
Out of Scope
- Migrating
sloppy.pyto 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
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
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:
# 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.
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
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"<html" in r.content.lower() or b"<!doctype" in r.content.lower()
finally:
subprocess.run(["docker", "rm", "-f", CONTAINER_NAME], capture_output=True)
def _docker_available() -> bool:
result = subprocess.run(["docker", "version"], capture_output=True)
return result.returncode == 0
File Structure
Dockerfile— NEWdocker-compose.yml— NEWscripts/docker_build.sh— NEWscripts/docker_run.sh— NEWdocs/guide_docker_deployment.md— NEWtests/test_docker_build.py— NEWsloppy.py— MODIFY: add--web-hostand--web-portargs
Acceptance Criteria
docker build -t manual_slop:latest .succeeds on a clean machinedocker compose upstarts the container, and:8999/statusreturns 200 within 60scurl http://localhost:8080/returns the web client HTML- An agent can
curl http://localhost:8999/api/mma_statusand get a valid response - The user can navigate to the web UI in a browser and see the ImGui panels
- File operations on
/projectspersist across container restarts - Env vars for API keys are not committed to the image (use runtime env)
Risks
- 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.
- 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.
- WebSocket bandwidth: Streaming frame deltas requires consistent network. On flaky networks, the user experience degrades. Mitigation: implement client-side prediction or reduce frame rate.
- Container size: Python + uv + all deps can produce a 1-2GB image. Mitigation: use multi-stage builds; pin Python deps for reproducibility.
- 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.