Centralising Beads Issue Tracking with Dolt on Unraid

homelabunraiddoltdockerdevtoolsbeads

I’ve been using Beads as my issue tracker — a CLI-first, AI-native tool that stores issues in a Dolt database. Dolt is essentially Git for data: it gives you version control, branching, and merging semantics on top of a MySQL-compatible SQL server.

The problem? I had drifted into a split-brain situation. My Mac M1 was running two embedded Dolt databases locally. My ThinkPad was running a standalone dolt sql-server on port 3307. Neither machine could see the other’s issues. If I wanted to pick up work on a different machine, I was flying blind.

The fix was obvious: centralise everything on a single Dolt SQL server, running as a Docker container on my Unraid NAS. Every client just connects to one place.

This post walks through the migration.


The Existing Mess

Before touching anything, I audited what I actually had.

ThinkPad (192.168.86.45) — a dolt sql-server running as a systemd user service on port 3307. Working directory ~/admin/.beads/dolt/, containing six databases:

DatabaseProject
beads_admin~/admin general tasks
beads_ductileductile integration gateway
beads_ductile-pluginsplugin development
beads_agenticloopAgenticLoop project
beads_unraid_adminUnraid server admin
beadsmisc

Mac M1 — two projects using embedded Dolt (no server, just a local database directory):

PathDatabase
~/.claude/.beads/embeddeddolt/claude/claude — 29 issues
/Volumes/Projects/fram-harness/.beads/embeddeddolt/fh/fh — 19 issues

Plus two more Mac projects already in server mode, but pointing at 127.0.0.1:3307 — which only works when SSH-tunnelled to the ThinkPad, which I never actually set up.

How Beads stores connection info. Each project has a .beads/metadata.json that records dolt_mode (embedded or server), host, port, user, and database name. This is what we’ll be editing.


The Target Architecture

A single dolthub/dolt-sql-server Docker container on Unraid, at 192.168.20.4:3306. All clients — Mac, ThinkPad, and anything else on the LAN — connect to it. No more local instances, no more split brain.

Mac M1 ──────────────────┐
ThinkPad ─────────────── Unraid NAS (192.168.20.4:3306)
                          dolthub/dolt-sql-server (Docker)

Step 1: Deploy the Container

On Unraid, create /mnt/user/appdata/dolt/ and write docker-compose.yml:

services:
  dolt:
    image: dolthub/dolt-sql-server:latest
    container_name: dolt
    restart: unless-stopped
    ports:
      - "3306:3306"
    volumes:
      - /mnt/user/appdata/dolt/data:/var/lib/dolt
    environment:
      - DOLT_ROOT_HOST=%
      - DOLT_ROOT_PASSWORD=<your-password>

Then docker compose up -d.

Critical gotcha: DOLT_ROOT_HOST=%. Without this environment variable, the Dolt container creates root@localhost — meaning connections from anywhere outside the container are refused, even with the correct password and the correct port published. The port binding works. The password is accepted. But MySQL’s host-based access control is a separate layer that silently rejects you.

% is MySQL wildcard syntax for “any host”. Set it and forget it.


Step 2: Export the Mac Embedded Databases

The embedded databases are just Dolt repos on disk. The cleanest export path is dolt dump run directly inside each database directory — no server required:

cd ~/.claude/.beads/embeddeddolt/claude
dolt dump -f -fn /tmp/dolt-mac-export/claude.sql

cd /Volumes/Projects/fram-harness/.beads/embeddeddolt/fh
dolt dump -f -fn /tmp/dolt-mac-export/fh.sql

Each dump starts with CREATE DATABASE IF NOT EXISTS \claude`; USE `claude`;` — which means the import will create the database and populate it in one shot.

Why not mysqldump? No MySQL client tools on Mac by default. dolt dump is built into the Dolt binary and works perfectly well for this purpose.


Step 3: Transfer and Import

sshpass -p '<password>' scp -o PubkeyAuthentication=no \
    /tmp/dolt-mac-export/*.sql [email protected]:/tmp/dolt-import/

Import using TCP (not docker exec — see the warning below):

dolt --host=192.168.20.4 --port=3306 --user=root --password=<pw> --no-tls sql < /tmp/dolt-import/claude.sql
dolt --host=192.168.20.4 --port=3306 --user=root --password=<pw> --no-tls sql < /tmp/dolt-import/fh.sql

Verify counts match the originals:

echo "USE claude; SELECT COUNT(*) FROM issues;" \
  | dolt --host=192.168.20.4 --port=3306 --user=root --password=<pw> --no-tls sql
# → 29 ✓

Step 4: Reconfigure the Mac Clients

Update each .beads/metadata.json to switch from embedded to server mode:

{
  "database": "dolt",
  "backend": "dolt",
  "dolt_mode": "server",
  "dolt_server_host": "192.168.20.4",
  "dolt_server_user": "root",
  "dolt_database": "claude",
  "project_id": "..."
}

Two more Beads config details to know:

Port file instead of metadata: As of Beads 1.0, dolt_server_port in metadata.json is deprecated. The port now lives in .beads/dolt-server.port:

echo "3306" > ~/.claude/.beads/dolt-server.port

Password via environment variable: The password is not stored in metadata.json. It’s read from BEADS_DOLT_PASSWORD:

# Add to ~/.zshrc
export BEADS_DOLT_PASSWORD=<your-password>

After that:

bd list --status=open
# Returns all 29 claude issues, fetched from Unraid ✓

Highs and Lows

No migration survives contact with reality unchanged.

The High: It Mostly Worked

The core plan was sound. Dolt exposes a MySQL-compatible wire protocol, so any MySQL client can talk to it. The Docker container came up first time. The DOLT_ROOT_HOST=% gotcha was already documented in my ZK notes from a previous deployment — zero debugging time lost there.

The Low: docker exec dolt dolt sql Is a Trap

Early in the migration I used docker exec dolt dolt sql -q "SHOW DATABASES;" to verify the imports. It looked fine. It was not fine.

docker exec dolt dolt sql starts a Dolt process inside the container that opens file handles directly against the volume on the Unraid array. It doesn’t exit cleanly. Each verification command left a process stuck in D-state (uninterruptible I/O wait), holding open file descriptors to /mnt/user/appdata/dolt/data/.

After a handful of these piled up, the shfs FUSE filesystem became unresponsive. ls /mnt/user would hang indefinitely. docker kill timed out. The GUI stop array request timed out. reboot was itself blocked by the shutdown script detecting active PIDs on the array devices.

Recovery required: killing the killable processes manually, stopping Docker via rc.docker stop, lazy-unmounting the array with umount -l, and only then getting a clean reboot.

Never use docker exec to run dolt sql against a volume-mounted Dolt container. Use the MySQL protocol over TCP instead:

echo "SHOW DATABASES;" | dolt --host=<ip> --port=3306 --user=root --password=<pw> --no-tls sql

This exits cleanly every time. docker exec dolt dolt sql does not.

The Low: Wrong Migration Method

The first attempt used raw dolt dump SQL files. It worked for small databases. After some research, the correct Beads-native migration path is bd backup sync + bd backup restore. This preserves full Dolt commit history and is what the tool is designed for. The SQL dump approach works but loses history.

The Low: docker.img at 85%

Before the migration, docker.img had accumulated 25GB of reclaimable images and 5.6GB of build cache — 85% of the 50GB allocation. Under heavy I/O this contributed to write pressure. A docker system prune -f && docker image prune -a -f freed 20GB and brought it to 50%.

Worth running before any data-heavy Docker operation on Unraid.

The High: The ZK Vault Paid Off

Three separate ZK notes from previous incidents were directly relevant:

  • DOLT_ROOT_HOST=% — saved the container setup
  • docker.img ENOSPC — explained the space pressure
  • uid/gid mismatch → SQLite errors — context for why volume mounts need care

The knowledge base built from past incidents made this migration faster than it would otherwise have been. That’s the point of it.


Appendix: Full Inventory

ProjectMachineOld modeDB name
~/.claudeMacembeddedclaude
fram-harnessMacembeddedfh
ductile-pluginsMacserver (ThinkPad)beads_ductile-plugins
unraid_adminMacserver (ThinkPad)beads_unraid_admin
adminThinkPadserver (local)beads_admin
ductileThinkPadserver (local)beads_ductile
ductile-pluginsThinkPadserver (local)beads_ductile-plugins
AgenticLoopThinkPadserver (local)beads_agenticloop
unraid_adminThinkPadserver (local)beads_unraid_admin

All now on 192.168.20.4:3306.