Centralising Beads Issue Tracking with Dolt on Unraid
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:
| Database | Project |
|---|---|
beads_admin | ~/admin general tasks |
beads_ductile | ductile integration gateway |
beads_ductile-plugins | plugin development |
beads_agenticloop | AgenticLoop project |
beads_unraid_admin | Unraid server admin |
beads | misc |
Mac M1 — two projects using embedded Dolt (no server, just a local database directory):
| Path | Database |
|---|---|
~/.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.jsonthat recordsdolt_mode(embeddedorserver), 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 createsroot@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 dumpis 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 execto rundolt sqlagainst 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 sqlThis exits cleanly every time.
docker exec dolt dolt sqldoes 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 setupdocker.img ENOSPC— explained the space pressureuid/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
| Project | Machine | Old mode | DB name |
|---|---|---|---|
~/.claude | Mac | embedded | claude |
fram-harness | Mac | embedded | fh |
ductile-plugins | Mac | server (ThinkPad) | beads_ductile-plugins |
unraid_admin | Mac | server (ThinkPad) | beads_unraid_admin |
admin | ThinkPad | server (local) | beads_admin |
ductile | ThinkPad | server (local) | beads_ductile |
ductile-plugins | ThinkPad | server (local) | beads_ductile-plugins |
AgenticLoop | ThinkPad | server (local) | beads_agenticloop |
unraid_admin | ThinkPad | server (local) | beads_unraid_admin |
All now on 192.168.20.4:3306.