Run DisPatch and My AI Tools in Docker: One File, One Command

Posted
October 19, 2026
Updated
October 19, 2026
By
Jacob Lloyd — written with AI assistance, post-project
Read time
15 min read

In plain terms: DisPatch is a chat app you run on your own computer and open from any phone or laptop in the house. Docker is a free program that runs software like this in a sealed box, so you don't install Python or anything else by hand. This page gets you from nothing to a working AI chat with one small text file and one command, and it can include a small AI model that runs on your own processor, so you need no account or API key.

I run my own AI chat app, DisPatch, for my household. Until this month, installing it meant Python, a virtual environment and a service file. Now it ships as a Docker image, and the whole setup is one file, one command, your own server. This page walks through it from an empty folder to a working chat, with a small AI model that runs on your own processor so you can try it without signing up for anything.

Every command on this page is one I ran on 2026-10-19. I don't have Docker Desktop on this machine, so I used Podman 5.8 with Docker Compose 5.6 on Linux. The commands are the same; where Podman needed something extra, I say so in the technical part at the end.

tl;dr

  • What you get: DisPatch, a private chat app for the browser and the phone, plus an optional small AI model, both running in containers on your computer.
  • What you do: install Docker, save one 26-line file, run docker compose up -d, open http://localhost:8765 and answer a six-step setup wizard.
  • What it costs: nothing. About 600 MB for the DisPatch image, 860 MB for the model server image and a 2.5 GB model download.
  • What it never does: send your chats anywhere you didn't connect. With the local model, nothing leaves your computer except the weather and web lookups you ask for.
  • The honest part: DisPatch is the only one of my images published on a registry as of 2026-10-19; the other tools build from their source code with one extra word. The small local model is fine for everyday questions, not for hard reasoning.

What you end up with

DisPatch in a browser. Atlas, the default character, answers a question about keeping basil alive on a windowsill with six numbered tips. Below, asked about the weather in Lisbon, Atlas shows a small Weather lookup notice and then answers with the current temperature, wind and a mild outlook for the week. The model name under each reply is a Qwen3 4B model.
DisPatch running in Docker, talking to a 4-billion-parameter model on the same computer's processor. The weather answer used a live lookup; the small notice above it says so.

Atlas is the character a new DisPatch starts with. It answers questions, and with a model that supports tools it can look things up: web search, a page fetch, weather, Wikipedia, exchange rates, the time and a calculator. The same chat opens on a phone:

The same DisPatch chat on a phone-sized screen: the end of the basil tips, the weather question, the Weather lookup notice and Atlas's answer, with a message box and Bots, Chats and Messages tabs at the bottom.
The same conversation at phone width.

For what DisPatch does beyond this (profiles for every person in the house, a kids' view, backups), see the DisPatch 3.0 article. This page is about getting it running.

Step 1: install Docker

Check it works by opening a terminal (PowerShell on Windows) and typing docker compose version. Any version number means you're ready.

Step 2: make a folder and one file

Make a folder called dispatch and save this in it as docker-compose.yml:

services:
  dispatch:
    image: ghcr.io/laserlloyd/dispatch_chat:main
    ports:
      - "8765:8765"
    volumes:
      - dispatch-data:/data
    restart: unless-stopped
    init: true
    stop_grace_period: 30s

  # Optional: a small AI model that runs on your processor, no account needed.
  # Delete this block if you will use Ollama, LM Studio or a cloud service instead.
  model:
    image: ghcr.io/ggml-org/llama.cpp:server-b11723
    command: ["-hf", "unsloth/Qwen3-4B-Instruct-2507-GGUF:Q4_K_M", "--jinja",
              "--host", "0.0.0.0", "--port", "8080", "--ctx-size", "8192"]
    environment:
      LLAMA_CACHE: /models
    volumes:
      - model-files:/models
    restart: unless-stopped

volumes:
  dispatch-data:
  model-files:

Two services. dispatch is the chat app. model is llama.cpp's server, which downloads Qwen3 4B Instruct (2.5 GB) on its first start and keeps it. Your chats live in a storage volume called dispatch-data, which survives updates and restarts.

"8765:8765" lets other devices on your network open DisPatch, which is what you want for a family chat. If you only want it on this computer for now, write "127.0.0.1:8765:8765" instead.

Step 3: start it

In a terminal, inside the dispatch folder:

docker compose up -d
docker compose ps

The first start downloads the two images and the model, so give it a few minutes. On my machine DisPatch answered 5 seconds after its image arrived, and the model server took about 20 seconds to download and load the model on my connection. docker compose ps should list both services as Up.

Step 4: create your account

Open http://localhost:8765 in a browser. A new DisPatch is locked until it has an owner, and because it runs inside a container it asks for a one-time setup token. Read it from the log:

docker compose logs dispatch | grep "SETUP TOKEN"

If grep isn't available (Windows), run docker compose logs dispatch and look for the line that starts with SETUP TOKEN:. The token changes every time DisPatch starts, and stops working once the owner exists.

The DisPatch setup wizard, step 1 Your account: the name Ada, a password typed twice, an empty optional PIN, and an opened section Setting up from another computer with the setup token pasted in. A Create account button below.
Step 1 of 6. Type a name and a password, open "Setting up from another computer?" and paste the token.

A password needs at least 8 characters. The PIN is optional, for quick unlocking. If you set one, DisPatch writes a recovery code into its data, and you should copy it somewhere safe:

docker compose exec dispatch cat /data/RECOVERY-CODE.txt

Step 5: connect the AI

Step 2 of the wizard looks for a model server on the same computer. Inside Docker it won't find the model service by itself, so type it in:

Wizard step 2, A model. Type OpenAI-compatible, address http://model:8080/v1, API key empty, default model unsloth/Qwen3-4B-Instruct-2507-GGUF:Q4_K_M. Under Test and save, a green line reads 1 models available, 14 ms, OpenAI-compatible server, atlas now uses it.
Type "OpenAI-compatible", address http://model:8080/v1, no key, and the model name. "Test and save" should come back green.
BoxWhat to type
TypeOpenAI-compatible
Addresshttp://model:8080/v1
API keyleave empty
Default modelunsloth/Qwen3-4B-Instruct-2507-GGUF:Q4_K_M

Using a cloud model instead? Pick it in the same box and paste your key. I tested this with an OpenAI-compatible cloud provider: "Test and save" listed its models and Atlas answered from it. The key goes into DisPatch's own data, not into the compose file.

Step 6: finish and say hello

The remaining steps are short. Voice is a separate add-on you can skip. Family adds more people (you can do that later). Network asks who should reach DisPatch: pick the same answer you chose for the port in step 2.

Wizard step 6, Done: ticks for Owner account, A model connection and Who can reach DisPatch; dashes for Voice and Family; open circles for Encrypted backups and Recovery code saved. A Finish button.
The last step is a checklist of what is done and what is optional.

Press Finish. If the page still shows the "create owner account" form after that, reload the page. That's a display bug in the current build (the account was created; the old form just stays on screen). Then pick Atlas on the left, press + for a new chat and ask it something.

To use it from a phone on the same Wi-Fi, open http://<your computer's address>:8765. Install-to-home-screen, notifications and the microphone need HTTPS, which the technical part covers.

That's the whole setup. Everything below is for when you want to know more, keep your data safe, or add my other tools.

How the pieces fit

The DisPatch image runs as an ordinary user (uid 1000), not root. Everything it writes goes to /data, which is the dispatch-data volume: the SQLite database, uploads, avatars, settings and its own six-hourly snapshots. Remove the container and nothing is lost; remove the volume and everything is.

The image tag main follows the main branch of the DisPatch repository. Published tags on 2026-10-19 are main and one sha-<commit> tag per build; there are no version-number tags yet. If you want the image to stay put until you choose to update, use a sha- tag from the package page. Images exist for 64-bit Intel/AMD and ARM; I ran the Intel/AMD one.

Your data: back it up, restore it, update safely

Stop DisPatch first so its database is written out completely, then copy the volume into a file:

docker compose stop dispatch
docker run --rm -v dispatch_dispatch-data:/data:ro -v "$PWD":/backup alpine \
  tar czf /backup/dispatch-backup.tgz -C /data .
docker compose start dispatch

Compose names the volume after the folder, so a folder called dispatch gives dispatch_dispatch-data; docker volume ls shows the real name. My test install with a handful of chats made a 3.7 MB file.

To restore into a fresh volume (I restored mine and started a second DisPatch on it: same owner, same 8 messages):

docker volume create dispatch_dispatch-data
docker run --rm -v dispatch_dispatch-data:/data -v "$PWD":/backup:ro alpine \
  sh -c 'tar xzf /backup/dispatch-backup.tgz -C /data && chown -R 1000:1000 /data'
docker compose up -d

Don't copy chats.db by itself out of a running container: recent messages sit in the chats.db-wal file beside it until DisPatch stops.

Updating is two commands. I ran docker compose down, pull and up -d on a populated install; the owner and every message were still there, and no new setup token was printed.

docker compose pull
docker compose up -d

Take a backup before you update across a big version; DisPatch migrates its database on start, and an older image may not read the migrated one.

Security defaults, and the one setting to check

  • Locked until an owner exists. Until step 1 of the wizard is done, the network sees only a sign-in page, and creating the owner needs the token from the log.
  • Visitors who don't sign in get the Restricted view. This surprised me while testing: from a browser that never signed in, I could open Atlas and get replies. That's DisPatch 3.0's design (a guest is treated like a child's Restricted profile, 200 replies a day by default), but those replies use your AI connection. If DisPatch is on your network with a paid cloud key, change Settings → All settings → People & Profiles → "A device that has not signed in" to Sign-in screen only.
  • Who can reach it is decided by the port line. "8765:8765" is your whole network; "127.0.0.1:8765:8765" is this computer only. The container itself always listens on every address inside its own network.
  • HTTPS is your call. The repository's own docker-compose.yml has an optional Caddy proxy (--profile proxy) for a real hostname with certificates; I did not test it in this round. If you put nginx in front, keep proxy_set_header Host $host;, or the chat loads and then never updates (DisPatch checks that the browser's origin matches the Host header).
  • The model server has no key. Its port isn't published in the file above, so only DisPatch can reach it. Don't add a ports: line to model unless you also give llama.cpp an --api-key.

The full DisPatch Docker guide, including bind mounts, user ids and the agent integration that does not work well in a container, is docs/deploy-docker.md in the repository.

Adding my other tools

Each of my other tools has a deploy/compose/compose.yml in its repository. Their images aren't on a registry yet, so you clone the repository and add --build. Each build took under a minute here, with the base images already downloaded. I built and started each of these from a fresh clone of GitHub on 2026-10-19:

ToolWhat it isImagePortWhat I checked
DisPatchthe chat apppublished, 599 MB8765everything above
MailForgeemail assistant that never sends on its ownbuild, 420 MB8090dashboard opens, no mailbox connected
InfoForge + kiwixoffline Wikipedia search for AI answersbuild, 128 MB, plus kiwix 66 MBnone (kiwix 8090)doctor and search on a 23 MB Wikipedia file
ThemeForgetheme preview pagebuild, 13.8 MB8185page opens
StudioForgeGPU model serverbuild, 568 MB (Vulkan)1234, 8080, 1235served a model on an AMD integrated GPU, see below
CrucibleForgemodel benchmarkbuild, 129 MB8777started and opened; no benchmark run
TransForgesite translatorbuildnonenot tested in this round

Sizes are what Podman reported on disk. The tools' own compose files publish their ports on 127.0.0.1 only, unless you change it.

MailForge

MailForge encrypts your mailbox passwords with a keyring password you choose; it cannot be recovered, so keep it in a password manager. Also point LLM_BASE_URL at your own model server (192.0.2.10 below stands for its address), because the default is port 1234 on the Docker host, and whatever answers there gets MailForge's requests.

git clone https://github.com/LaserLloyd/MailForge.git
cd MailForge
read -rsp 'MailForge keyring passphrase: ' MAILFORGE_KEYRING_PASSWORD; export MAILFORGE_KEYRING_PASSWORD
LLM_BASE_URL=http://192.0.2.10:1234/v1 \
  docker compose -f deploy/compose/compose.yml up -d --build
docker compose -f deploy/compose/compose.yml logs app | grep "MailForge UI"

The log line is a http://127.0.0.1:8090/?token=… address; open it in a browser.

MailForge dashboard in a browser, fresh install: counters for mail received, unread, needs reply, needs action and AI drafts to review all at 0, buttons to compose and open the inbox, empty Needs a reply and Recent inbox panels, and a top bar that says human approval, nothing sends without you, and No mailboxes.
A fresh MailForge in Docker, before any mailbox is added.

InfoForge and offline Wikipedia

Put a .zim file from the Kiwix library in deploy/compose/zims/ and name it in infoforge.config.yaml (copy it from infoforge.config.example.yaml). I used the 23 MB "Chemistry by Wikipedia" mini file to test.

git clone https://github.com/LaserLloyd/InfoForge.git
cd InfoForge/deploy/compose
docker compose up -d kiwix
docker compose --profile cli build infoforge
docker compose --profile cli run --rm infoforge doctor
docker compose --profile cli run --rm infoforge search "noble gas"

Search works with no AI model at all:

[1] Noble Gas  (score 19.0, 4222 chars)
[2] Gas  (score 16.0, 1586 chars)
[3] Noble gas compound  (score 10.5, 2045 chars)

infoforge ask needs a model server in base_url; with the placeholder address, doctor passes every check except "llm primary", which is correct.

ThemeForge

git clone https://github.com/LaserLloyd/ThemeForge.git
cd ThemeForge
docker compose -f deploy/compose/compose.yml up -d --build

Then open http://localhost:8185.

ThemeForge specimen page in the Daylight palette: palette buttons along the top, then switches, checkboxes, radios and tabs shown in their on, off, selected and disabled states, a row of button styles, form fields with an error state, and surface swatches.
The ThemeForge specimen page from its 13.8 MB container.

A GPU model server: StudioForge

StudioForge is my own model server. It runs models on a graphics card, handles several models and several clients, and since version 2.0 wants an owner account and an API key for every client. Its image isn't on a registry yet, so this part builds it.

I expected to say "NVIDIA only" here. The mini PC I tested on has no NVIDIA card, only AMD's integrated Radeon GPU in a Ryzen AI 9 365. I built the Vulkan image anyway, passed the GPU in, and it worked: StudioForge downloaded the matching prebuilt llama.cpp engine from GitHub on first start, tested it, and served Qwen3 4B with every layer on the GPU at 23 to 28 tokens per second. For comparison, the CPU-only model service above did 22 tokens per second with the same model, so on this chip the GPU frees the processor more than it speeds up replies.

StudioForge 2.0.1 control panel, Overview tab: one GPU, an AMD Radeon integrated GPU reported through Vulkan, with 5.68 of 10.48 GiB in use; one VRAM holder, llama-server; and one loaded model, Qwen3-4B-Instruct-2507 Q4_K_M, idle, with the last request at 22.79 tokens per second on engine build b11669.
StudioForge in Docker with an AMD integrated GPU passed through over Vulkan, one model loaded.
git clone https://github.com/LaserLloyd/StudioForge.git
cd StudioForge/deploy/compose
cp env.example .env
# in .env: SF_BACKEND=vulkan, SF_MODELS=/path/to/your/gguf/files,
#          SF_OWNER_PASSWORD (a password of 10 or more characters),
#          SF_VIDEO_GID and SF_RENDER_GID = the numbers from: getent group video render
docker compose -f compose.yml -f compose.vulkan.yml build
docker compose -f compose.yml -f compose.vulkan.yml up -d
docker compose -f compose.yml -f compose.vulkan.yml exec studioforge \
  studioforge keys create --name dispatch --scope inference

The last command prints a key once. The control panel is on http://localhost:8080 (user owner, your password). For an NVIDIA card the file is compose.cuda.yml and SF_BACKEND=cuda; I did not test that path here, and the CUDA image is much bigger.

DisPatch and StudioForge together

DisPatch's repository has deploy/compose/stack.yml, which starts both on one network and pre-fills DisPatch's connection to StudioForge. Out of the box it tries to pull a StudioForge image that isn't public yet, so point it at the one you just built. These are the settings I added to stack.env:

git clone https://github.com/LaserLloyd/DisPatch_Chat.git
cd DisPatch_Chat
cp deploy/compose/stack.env.example deploy/compose/stack.env
# add to deploy/compose/stack.env:
#   DISPATCH_IMAGE=ghcr.io/laserlloyd/dispatch_chat:main
#   STUDIOFORGE_IMAGE=ghcr.io/laserlloyd/studioforge:2.0.1-vulkan
#   STUDIOFORGE_MODELS=/path/to/your/gguf/files
#   STACK_MODEL=<the model id StudioForge lists>
#   SF_OWNER_PASSWORD (the same 10+ character owner password)

The stack file reserves an NVIDIA GPU. For AMD or Intel over Vulkan, I replaced that with a small second file, stack-amd.yml:

services:
  studioforge:
    deploy: !reset {}
    devices: ["/dev/dri:/dev/dri"]
    group_add: ["39", "105"]   # your video and render group numbers
docker compose -f deploy/compose/stack.yml -f stack-amd.yml \
  --env-file deploy/compose/stack.env up -d
docker compose -f deploy/compose/stack.yml -f stack-amd.yml \
  --env-file deploy/compose/stack.env exec studioforge \
  studioforge keys create --name dispatch --scope inference

Then finish DisPatch's wizard as above, open Settings → Connections, press Edit on the StudioForge connection, paste the key and press Test.

DisPatch Settings, Connections tab: one connection named StudioForge at http://studioforge:1234/v1, key saved, used by atlas, with a green result box reading 2 models available, 14 ms, StudioForge 2.0.1.
The pre-filled StudioForge connection after the key was pasted. Atlas then answered through StudioForge on the GPU.

The model id comes from curl -H "Authorization: Bearer <key>" http://localhost:1234/v1/models or the panel's Models tab. If your models are in a Hugging Face download folder, the id is the whole folder path, which is long but works.

Gotchas

  • Reload after Finish. In the build I tested, the "create owner account" form stays on screen after the wizard closes. The account exists; reload the page.
  • The wizard won't find the model by itself. Inside Docker its search looks at the container, not at other services. Type http://model:8080/v1.
  • Small models and lookups. With Qwen2.5 1.5B, Atlas sometimes printed a made-up "Web searching: …" line instead of calling the tool. The 4B model in the file above called the weather lookup correctly every time I tried. Bigger is better here.
  • The recovery code only exists if you set a PIN. With a password alone there is no RECOVERY-CODE.txt, even though the wizard's checklist mentions one.
  • Two tools want port 8090. MailForge and InfoForge's kiwix both default to 8090 on this computer. Running both, set HOST_PORT for MailForge or KIWIX_PORT for kiwix.
  • StudioForge's first start needs GitHub. It fetches the llama.cpp engine through GitHub's API, which allows 60 anonymous requests an hour per address. My first try hit that limit; the slim Vulkan image then tried to build the engine from source, which it can't (there's no git in it), and reported can_serve: false. A restart after the limit reset fixed it. Setting GITHUB_TOKEN in .env raises the limit, according to StudioForge's own log message.
  • docker compose down -v deletes your chats. Plain down does not.
  • Podman users: on Fedora and other SELinux systems add :z to the backup folder mount ("$PWD":/backup:z). Podman with Docker Compose also refused the tools' compose files whose healthcheck block has no test: line ("must define a healthcheck command"); I added the test command from each Dockerfile in a small override file. Rootless kiwix needs KIWIX_USER=0. Docker users can ignore this item.

What I did not test

  • Docker Desktop on Windows or Mac. I used Podman with Docker Compose on Linux, x86-64 only.
  • Ollama or LM Studio running outside Docker on the same computer. On Docker Desktop the address would be http://host.docker.internal:11434/v1 for Ollama, and that server has to accept connections from the container; I haven't confirmed it.
  • NVIDIA and ROCm images of StudioForge, the voice add-on build (VOICE=1), the Caddy HTTPS profile, and TransForge.
  • InfoForge's ask with a model, and a CrucibleForge benchmark run.

DisPatch on GitHub StudioForge MailForge InfoForge

Related: DisPatch 3.0: family profiles and the setup wizard, DisPatch, StudioForge, and how to have an LLM adapt any project to your system.


← More AI & Local LLM