Reference

Running in Docker

Two images — which one to run, what is in each, and why neither contains any model weights.

MeshHold ships two container images. They run the same daemon, from the same binary; what differs is which of other people's programs are installed beside it.

meshhold:<version>-<arch> meshhold:<version>-tools-<arch>
Base distroless static Debian slim
Size ~14 MB compressed roughly thirty times that
Shell none yes
ffmpeg, LibreOffice no yes
llama.cpp, whisper.cpp, sherpa-onnx, stable-diffusion.cpp no yes
Model weights no no

The last row is the one people are surprised by, so it is worth saying plainly: neither image contains any model weights. They are gigabytes, they are chosen per node, and the node downloads the ones you ask for into a vault — where they replicate to your other trusted machines, so a second node does not fetch the same eight gigabytes again.

Which one

Take the plain image unless you know you want the other. It is a storage, sync, chat and VPN node, which is most of what MeshHold is, and it is a distroless image with no shell in it — a smaller thing to keep patched and a smaller thing to break into.

Take the -tools image when you want that node to do work on the files it holds: convert documents, transcode video, transcribe recordings, read them aloud, index them for search, or answer questions about them. Every program on the external tools page is already in it, at a version the build pins.

The two are interchangeable at any time. Nothing in a node's data depends on which image wrote it, so swapping the image and restarting is a supported move in both directions — and it is the quickest way to find out whether a job type you are missing is missing for want of a program.

Loading and running

The release publishes each image as a gzipped OCI tarball rather than to a registry:

gunzip -c meshhold-<version>-amd64.tar.gz | docker load
docker run -d --name meshhold \
  -p 7777:7777 -p 8080:8080 \
  -v meshhold-data:/var/lib/meshhold \
  -v /srv/meshhold/config.yaml:/etc/meshhold/config.yaml:ro \
  -e MESHHOLD_PASSWORD='choose-something' \
  meshhold:<version>-amd64

7777/tcp is libp2p — the port peers reach this node on, and the one worth forwarding. 8080/tcp is the Web UI and REST API, and is not something to expose to the internet without thinking about it first.

/var/lib/meshhold is the volume: the metadata store and the encrypted blocks. The config file is deliberately not in it — it is a bind mount, or absent entirely, in which case the daemon comes up in limited mode and the Web UI walks you through setup.

MESHHOLD_PASSWORD is read once. If the store has no password hash yet, the daemon writes an Argon2id hash of it on first boot and ignores the variable from then on, so leaving it in a compose file does not keep resetting anything.

The -tools image runs identically — same ports, same volume, same entrypoint — under uid 65532, with HOME inside the volume so that anything a tool downloads for itself survives a restart.

Checking what the node can do

docker exec meshhold meshhold tools --api http://127.0.0.1:8080

On the plain image this lists everything as missing, which is correct and expected. On the -tools image everything should read yes; anything that does not is worth reporting, because that image exists precisely so nobody has to install these by hand.

The same list is in the Web UI under Settings → Tools, with the model store beneath it.

Building the images yourself

build_docker.bat

It expects the Linux binaries to already exist under distr/linux/<arch>/ from build_linux.bat, and deliberately does not build them itself: one Go binary, several packagers, no chance of the .deb, the .msi and the image drifting apart in toolchain version or build flags.

The tools image is slow to build — it installs LibreOffice and fetches four model runtimes — so it is off by default and amd64-only when on:

set SKIP_TOOLS=0
set TOOLS_ARCHES=amd64 arm64

The runtime archives are cached across builds by a BuildKit cache mount, so the second build is much faster than the first.