Run Hob, the Open-Source Local AI Desktop App, in a Throwaway Docker Container

Hob is a free, MIT-licensed desktop app that runs open-weight AI models on your own computer. This guide runs the official v0.2.5 Linux AppImage in a disposable Docker container and opens its window in your browser through noVNC. It uses demo mode, so you don't need a GPU or a model download.

Run Hob, the Open-Source Local AI Desktop App, in a Throwaway Docker Container

Hob from Frigade is "an easier way to use AI on your own computer": a free, open-source desktop alternative to ChatGPT and Claude. On first launch it looks at your hardware, picks an open-weight model that fits (Gemma, or gpt-oss on bigger machines), downloads it and sets everything up. After that it works offline, with no account. It can work on files, run tools, draw interactive charts, render markdown and tables, and search past chats. It is MIT-licensed and built with Electron.

Hob is a desktop app, not a web server. Upstream ships installers for Mac, Windows and Linux, and has no Dockerfile or container image. This guide follows the exact path we recorded and checked:

  • the official v0.2.5 Hob-linux-x86_64.AppImage, verified against its published sha512 and extracted so FUSE isn't needed
  • inside a throwaway debian:bookworm-slim container with a virtual display (Xvfb)
  • shown in your browser through noVNC, with a Chrome DevTools Protocol (CDP) port for automation
  • in Hob's built-in demo mode (HOB_FAKE=1), which uses a stand-in model, so you skip the multi-GB model download and need no GPU

Nothing is installed on your host besides one Docker image, one volume and one container. Teardown is one command.

Hob drawing an interactive

Demo mode, honestly: everything in this guide uses Hob's stand-in model. The download is simulated, replies are scripted, and the setup screen pretends the machine is an "Apple M1 Max · 32 GB". It shows the real app UI working end to end. It says nothing about real model speed or quality. A real model needs several to tens of GB of disk and memory.

Prerequisites

  • Linux x86_64 with Docker Engine and the Compose v2 plugin (docker compose version works). We tested Docker Compose 5.6.1 on a rootful daemon with no buildx plugin (the classic builder). The files below work with either builder.
  • curl and ss (iproute2)
  • About 4 GB free in Docker's data directory. The built image is 2.97 GB, and the AppImage download is about 508 MB.
  • Internet access to github.com (for the AppImage) and the Debian mirrors
  • A browser on the same machine. Both ports bind to 127.0.0.1 only.

Install

1. Set variables

Pick a working directory for the generated files. The version and sha512 are the published values for the v0.2.5 Linux AppImage (from the release's latest-linux.yml).

export DEPLOY=$HOME/hob-demo
export PROJ=hob-demo
export HOB_VERSION=0.2.5
export HOB_SHA512='Pyo7NXiNzfi7Xu/Byy+BBUnjwzrB7AlsSfT6pFew5MkPI0Gs5Iq2clr0+9plOLtybVRF7gP7cH6kI0wopPpkSw=='

2. Preflight

docker info >/dev/null && echo docker_ok
docker compose version
curl -sIL -o /dev/null -w '%{http_code}\n' \
  https://github.com/FrigadeHQ/hob/releases/download/v$HOB_VERSION/Hob-linux-x86_64.AppImage
df -h /var/lib/docker | tail -1

You should see docker_ok, a Compose version, 200 for the release asset, and at least ~4 GB available.

3. Pick free ports

noVNC (the browser view) prefers port 3000. CDP prefers 9222. This takes the first free port from each list:

free_port() { for p in "$@"; do ss -ltn | awk '{print $4}' | grep -qE "[:.]$p\$" || { echo $p; return; }; done; }
HOB_WEB_PORT=$(free_port 3000 3010 3020 3030 3040 3050)
HOB_CDP_PORT=$(free_port 9222 9232 9242 9252)
echo "HOB_WEB_PORT=$HOB_WEB_PORT HOB_CDP_PORT=$HOB_CDP_PORT"

Expected: HOB_WEB_PORT=3000 HOB_CDP_PORT=9222. If either value is empty, every candidate is busy, so add more ports to the list.

4. Generate .env, entrypoint.sh, Dockerfile and compose.yml

mkdir -p $DEPLOY
cat > $DEPLOY/.env <<EOF
HOB_WEB_PORT=$HOB_WEB_PORT
HOB_CDP_PORT=$HOB_CDP_PORT
HOB_VERSION=$HOB_VERSION
HOB_SHA512=$HOB_SHA512
DEPLOY=$DEPLOY
EOF

The entrypoint starts a 1280×720 virtual display, a window manager, a VNC server and noVNC, plus a socat forwarder for CDP (Chromium binds DevTools to the container's loopback). Then it starts Hob:

cat > $DEPLOY/entrypoint.sh <<'EOF'
#!/bin/sh
# Virtual display -> VNC -> noVNC (browser), plus CDP forwarder, then Hob in stand-in-model mode.
set -e
export DISPLAY=:99
rm -f /tmp/.X99-lock
Xvfb :99 -screen 0 1280x720x24 -nolisten tcp &
for i in $(seq 1 50); do [ -e /tmp/.X11-unix/X99 ] && break; sleep 0.1; done
openbox >/dev/null 2>&1 &
x11vnc -display :99 -forever -shared -nopw -localhost -rfbport 5900 -quiet >/tmp/x11vnc.log 2>&1 &
websockify --web /usr/share/novnc 6080 localhost:5900 >/tmp/websockify.log 2>&1 &
socat TCP-LISTEN:9223,fork,reuseaddr TCP:127.0.0.1:9222 &
( xdotool search --sync --class hob windowmove 0 0 windowsize 1280 720 >/dev/null 2>&1 || true ) &
mkdir -p /data/profile
if [ -n "$RECORD" ]; then
  mkdir -p /data/rec
  ffmpeg -loglevel error -y -f x11grab -video_size 1280x720 -framerate 30 -i :99 -c:v libx264 -preset veryfast -pix_fmt yuv420p /data/rec/hob-$(date +%s).mp4 &
fi
exec /opt/hob/AppRun --no-sandbox --disable-gpu --disable-dev-shm-usage \
  --remote-debugging-port=9222 --remote-allow-origins='*'
EOF
chmod +x $DEPLOY/entrypoint.sh

--no-sandbox is needed because Chromium runs as root in the container. That is fine for a throwaway demo container and not fine for daily use.

The Dockerfile downloads the AppImage, fails the build if the sha512 doesn't match, and extracts it with --appimage-extract, so the container needs no FUSE or /dev/fuse:

cat > $DEPLOY/Dockerfile <<'EOF'
FROM debian:bookworm-slim
ARG HOB_VERSION
ARG HOB_SHA512
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y --no-install-recommends \
      ca-certificates curl openssl \
      xvfb x11vnc novnc websockify openbox xdotool socat ffmpeg dbus-x11 \
      libgtk-3-0 libnss3 libgbm1 libasound2 libxss1 libxtst6 libatk-bridge2.0-0 libdrm2 \
      libxkbcommon0 libsecret-1-0 libnotify4 libcups2 libxshmfence1 libuuid1 xdg-utils \
      fonts-dejavu-core fonts-noto-color-emoji \
    && rm -rf /var/lib/apt/lists/*
WORKDIR /opt
RUN curl -fL -o hob.AppImage https://github.com/FrigadeHQ/hob/releases/download/v${HOB_VERSION}/Hob-linux-x86_64.AppImage \
    && test "$(openssl dgst -sha512 -binary hob.AppImage | base64 -w0)" = "$HOB_SHA512" \
    && chmod +x hob.AppImage && ./hob.AppImage --appimage-extract >/dev/null \
    && mv squashfs-root /opt/hob && rm hob.AppImage
COPY entrypoint.sh /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
EOF

The compose file turns on demo mode, keeps Hob's profile in a named volume, and publishes both ports on 127.0.0.1 only:

cat > $DEPLOY/compose.yml <<'EOF'
services:
  hob:
    build:
      context: .
      args:
        HOB_VERSION: ${HOB_VERSION}
        HOB_SHA512: ${HOB_SHA512}
      dockerfile: Dockerfile
    image: shortform-hob:${HOB_VERSION}
    init: true
    shm_size: 1gb
    ports:
      - "127.0.0.1:${HOB_WEB_PORT}:6080"
      - "127.0.0.1:${HOB_CDP_PORT}:9223"
    environment:
      HOB_FAKE: "1"
      HOB_BACKGROUND: "0"
      HOB_SYSTEM_LANGUAGES: en-US
      HOB_USER_DATA: /data/profile
      HOB_FAKE_STEP_MS: "1500"
      RECORD: ${RECORD:-}
    volumes:
      - hob-data:/data
    restart: "no"
volumes:
  hob-data:
EOF

Check that Compose accepts it:

C="docker compose -p $PROJ --env-file $DEPLOY/.env -f $DEPLOY/compose.yml"
$C config >/dev/null && echo compose_ok

Expected: compose_ok.

5. Build and start

$C --progress plain build
$C up -d
sleep 12
$C logs --tail 40 hob

--progress is a global Compose flag, so it goes before build. The build installs the X/VNC packages, downloads the 507 MB AppImage, passes the sha512 test, extracts it and ends with Successfully tagged shortform-hob:0.2.5. up -d creates the network, the hob-data volume and the container.

The sleep 12 gives Electron time to start. In the logs, look for:

DevTools listening on ws://127.0.0.1:9222/devtools/browser/…

Lines like ERROR:dbus/bus.cc:406] Failed to connect to the bus are expected and harmless, because the container has no D-Bus.

Verify

curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:$HOB_WEB_PORT/vnc.html
curl -s http://127.0.0.1:$HOB_CDP_PORT/json/version | head -c 300; echo
curl -s http://127.0.0.1:$HOB_CDP_PORT/json/list | grep -o '"url": *"[^"]*"'
$C exec hob sh -c 'DISPLAY=:99 xdotool search --class hob getwindowname'
$C ps

What we saw:

CheckExpected output
vnc.html200
json/version"Browser": "Chrome/152.0.7977.149" and a User-Agent containing Hob/0.2.5 and Electron/44.7.2
json/list"url": "file:///opt/hob/resources/app.asar/out/renderer/index.html"
xdotool … getwindownamehob (lowercase)
psthe hob-1 container Up, with 127.0.0.1:3000->6080/tcp, 127.0.0.1:9222->9223/tcp

Browser setup

Open the noVNC view. Use your own port if step 3 picked a different one:

http://127.0.0.1:3000/vnc.html?autoconnect=1&resize=scale&reconnect=1&show_dot=1

autoconnect=1 connects right away with no password. You'll see the container's 1280×720 screen with the Hob window in it. Click and type in the canvas like you would on a desktop.

1. Welcome screen. First launch shows "Welcome to Hob" with a hardware-based pick. In demo mode the pretend hardware is "Apple M1 Max · 32 GB", so it recommends Gemma 4 26B, a 19.6 GB download, saving to /data/profile.

Hob's first-launch welcome screen recommending Gemma 4 26B, a 19.6 GB download

2. Other models (optional). Click Change model to see the model list, then press Escape.

The Change model list on Hob's setup screen

3. Download and start. Click Download and start. In demo mode, a simulated "Getting Hob ready" progress bar plays at 1.5 s per step (HOB_FAKE_STEP_MS). Then the chat opens and the status pill reads Ready.

Hob's

Hob's chat screen after setup, with the status pill reading Ready

4. First message. Click one of the suggestion chips. The stand-in model answers right away.

A suggestion chip sent and the assistant reply

5. Chart, thinking, markdown and tables. Demo mode has trigger prompts that exercise the real UI. In a new chat, __think__ streams reasoning, __markdown__ streams formatted text, and __table__ renders a table. The chart at the top of this post came from a scripted __tools__ prompt: it writes sales.csv and calls make_chart, which produces a hoverable "Revenue by region" bar chart. The trigger text appears as-is in your own message bubble.

Streaming

A rendered table in a Hob reply

6. Search. Press Ctrl+K to open the command bar with recent chats and quick actions. Type revenue to find the chart chat.

Hob's Ctrl+K command bar searching for

7. Settings and dark mode. Open Settings. Model is the model picker. Under General, set Theme to Dark, then close Settings.

Hob settings, Model page

Hob in dark theme

The setup screen only appears on a fresh profile. To see it again, wipe the volume and restart. The image is cached, so this takes about 10 seconds:

$C down -v && $C up -d && sleep 12

Automating it with Playwright (optional)

The noVNC page is only a picture of the screen, so it has no DOM selectors. To script Hob, keep the noVNC page open (for example as your recorded page) and drive the real Electron window over CDP:

const browser = await chromium.connectOverCDP('http://127.0.0.1:9222');
const page = browser.contexts()[0].pages()[0];
await page.emulateMedia({ colorScheme: null }); // without this, the dark theme won't apply
await page.getByTestId('setup-download').click();

Hob's upstream data-testids work as-is, the same ones app/e2e/*.spec.ts uses. The ones we used were setup-other-options, setup-download, status, suggestion, new-chat, composer-input, composer-send, chart-card, command-bar, open-settings, settings-nav-model, settings-nav-general and theme. The theme control is a radio group: page.getByTestId('theme').getByRole('radio', { name: 'Dark' }).

Teardown

$C down -v --remove-orphans && docker image rm shortform-hob:0.2.5
rm -rf $DEPLOY

This removes the container, network, volume (Hob's profile and chats) and the 2.97 GB image.

Troubleshooting

SymptomCauseFix
unable to prepare context: unable to evaluate symlinks in Dockerfile path: lstat …/Dockerfile: no such file or directoryThe compose file used dockerfile_inline, which the classic builder ignores when buildx isn't installedUse an on-disk Dockerfile with dockerfile: Dockerfile, as this guide does
--progress is a global compose flag, better use docker compose --progress xx build--progress plain was placed after buildPut it before the subcommand: $C --progress plain build
buildx Docker CLI plugin not found: falling back to the classic builderNo buildx pluginHarmless with this guide's files
Build stops at the test "$(openssl dgst …)" = "$HOB_SHA512" stepThe downloaded AppImage doesn't match the v0.2.5 sha512 (corrupt download or wrong version)Re-run the build. If you change HOB_VERSION, take that release's sha512 from its latest-linux.yml
json/version gives no response right after up -dElectron hasn't opened DevTools yetWait about 12 s, then check $C logs --tail 40 hob for DevTools listening
Failed to connect to the bus in the logsThe container has no D-BusHarmless; ignore it
xdotool prints hob, not HobThe window class is lowercaseCosmetic; nothing to fix
Hob window is 1100 px wide with black bars on both sides in noVNCThe entrypoint's background xdotool … windowsize 1280 720 fails silently before the window mapsCosmetic. For clean screenshots, capture the Hob page itself over CDP (Playwright page.screenshot()), which gives a 1100×720 image
The welcome/setup screen doesn't appearSetup runs once per profile, and the hob-data volume still has it$C down -v && $C up -d
The dark theme toggle does nothing under PlaywrightconnectOverCDP pins prefers-color-scheme: lightCall page.emulateMedia({ colorScheme: null }) after connecting
Ctrl+K dialog text looks ghosted in screenshotsThe open animation was captured mid-fade under software renderingWait about 400 ms after opening before capturing
Message bubbles show __tools__ […] or __think__, and sidebar titles read "Title for: …"Demo-mode trigger prompts and stand-in titlesExpected in HOB_FAKE=1 mode
HOB_WEB_PORT or HOB_CDP_PORT is emptyEvery candidate port is in useAdd more candidates to the free_port lists
Out of disk during buildThe image is 2.97 GB, plus the build cacheFree at least ~4 GB under /var/lib/docker

Notes and limits

  • This is demo mode. For real generations you would drop HOB_FAKE, give the container GPU access and remove --disable-gpu. Hob would then download a real model of several GB. We did not test that path.
  • PDF form filling, scanned-PDF OCR and the Calendar/Mail features are Mac-only upstream, so they aren't shown here.
  • For normal desktop use on Linux you don't need any of this: download the AppImage from the releases page and run it. The container exists so the demo is disposable and leaves nothing on the host.

Assumptions & decisions

  • Source of truth: the commands and file contents are the executed runs/hob-20261029-200452/install.sh, which the Deployment Process Reviewer validated (verdict PASS). They are not the unexecuted install-plan.md, which fails on hosts without buildx (dockerfile_inline) and puts --progress after build.
  • Generalised paths for readers: I replaced the job-specific JOB/RUN/DEPLOY paths and the project name shortform-hob-20261029-200452 with DEPLOY=$HOME/hob-demo and PROJ=hob-demo. The generated file contents are unchanged, including the image tag shortform-hob:${HOB_VERSION} and the optional RECORD/ffmpeg block. I didn't edit those, to keep exactly what was validated.
  • Verification outputs come from deploy-log.md (20:10–20:15) and the reviewer's live re-check (20:22). The container has since been torn down (job status torn-down), so I didn't re-run anything.
  • Screenshots: I used the clean *.hob.png CDP stills from runs/hob-20261029-200452/browser/ instead of the noVNC frames, which show black side bars and the noVNC handle. I skipped 11-command-bar-search because of the animation ghosting and used 12-command-bar. I left out stills that show the __tools__/__markdown__ trigger bubbles prominently (09-markdown). The chart still shows the scripted prompt's output, and the post states that it is scripted.
  • Manual browser steps describe clicking in noVNC, because the reader is a person. The Playwright section summarises what browser-setup.mjs actually did, with only the test IDs it used.
  • Real-model mode is mentioned only as an untested note, with no exact flags, because it was never run.
  • Demo-mode disclosure appears near the top, following the reviewer's instruction not to imply real model speed or an M1 machine.