Scenario

The Personal Cloud

Replace Dropbox / Google Drive / iCloud.

Replace Dropbox / Google Drive / iCloud with three nodes you already own: a VPS for availability, a desktop for the bulk of your files, and a phone for automatic photo upload. Encrypted before it leaves any of them.

What you'll end up with: a vault called Photos replicated across three nodes (RF = 2 reliable + 1 mobile seeder), auto-syncing your phone's camera roll, browsable from any device's web UI.

Prerequisites

  • Any small VPS — 1 vCPU, 1 GB RAM, 25 GB disk is enough for ~20 GB of replicated photos.
  • Your usual desktop / laptop (Linux, macOS, or Windows).
  • An Android phone with the MeshHold APK installed. iOS is not supported yet.
  • A domain pointing at the VPS — optional, but it gives you free Let's Encrypt TLS.

1. Install the first node (VPS)

Start with the VPS — it's the only node guaranteed to be online, so it's our bootstrap peer.

# Ubuntu / Debian
ssh root@vps.example.com
wget https://meshhold.dev/dl/linux/meshhold-amd64.deb
apt-get install -y ./meshhold-amd64.deb

# Set a password for the web UI (daemon stays running)
sudo -u meshhold meshhold set-password

Prefer Docker? Same binary, no apt step, no system user. Works the same on any host with Docker installed:

bash curl -fsSL https://meshhold.dev/dl/docker/meshhold-amd64.tar.gz \ | gunzip | docker load docker volume create meshhold-data docker run -d --name meshhold --restart unless-stopped \ -p 7777:7777/udp -p 7778:7778 -p 8080:8080 \ -v meshhold-data:/var/lib/meshhold \ -e MESHHOLD_PASSWORD=changeme \ meshhold:latest-amd64

The image is distroless static and runs as uid 65532. The meshhold-data named volume picks that ownership up automatically; a host-path bind-mount needs chown 65532:65532 first.

Open https://vps.example.com:8080/ and accept the self-signed cert (or configure api.tls.acme_domain in config.yaml for Let's Encrypt). The Setup Wizard greets you with three choices:

  • Create new network — pick this on the first node; it mints a fresh swarm-key.
  • Join existing — for nodes #2, #3, …
  • Continue offline — for trying it out on a laptop with no peers yet.

2. Create a vault

In the web UI, click Vaults → Add vault → Host a folder. Pick:

  • Name: Photos
  • Folder: the directory the VPS keeps the photos in, e.g. /var/lib/meshhold/photos
  • Replication factor: 2 (we want two reliable copies).

The node generates the vault's key: 32 random bytes. It is the encryption key, and the vault's Show QR / Link carries it — keep a copy of that link somewhere safe. If you bring a key of your own (through the API or config.yaml), it has to be 32 random bytes or more written as lowercase hex, as openssl rand -hex 32 or meshhold vault create prints it. A passphrase will not do: the vault's id is derived from its key and published to the swarm, so anyone in it could guess a phrase offline. A new vault is refused such a key, and meshhold config lint flags one in config.yaml.

RF counts only reliable nodes. Your phone defaults to reliable=false, so it seeds blocks but doesn't count toward the factor. That's the point — phones come and go; reliable nodes stay.

3. Pair your desktop

On the VPS, open Profile → Network → Show QR / Link and copy the network invite (the QR or the link). On the desktop:

# Install the desktop binary (Linux example — Windows uses the MSI).
sudo apt-get install -y ./meshhold-amd64.deb
systemctl start meshhold

# Open https://localhost:8080/, pick "Join existing", paste the invite.

Within ~20 seconds the desktop appears in the VPS's Network page. On the VPS, open the Photos vault's menu → Show QR / Link and copy the link; on the desktop, open Vaults → Add vault → Join with key and paste it.

4. Pair your phone

  1. Install the APK and open the app.
  2. Tap Scan invite, point it at the QR from the VPS.
  3. From the app's Scan tab, scan the Photos vault's QR (on the VPS: the vault's menu → Show QR / Link).
  4. Toggle Camera-roll auto-upload. New photos appear on the other nodes in seconds.

5. Set replication and walk away

The defaults are already sane, but you can tune them on the VPS. The key file holds the vault's key — the vault_key its Show QR / Link carries; it stays out of config.yaml itself, which deployment tools copy around:

vaults:
  - name: "Photos"
    key_file: "/etc/meshhold/photos.key"   # the vault's key: 64 lowercase hex characters
    storage_path: "/var/lib/meshhold/photos"
    full_sync: true               # keep a full copy on the VPS
    replication_factor: 2
    type: "storage"

Restart the daemon. The VPS will pull and decrypt every block; the desktop already has a local copy of files it created; the phone keeps the encrypted ciphertext but can decrypt anything you scroll to.

6. Look at the photographs

Open Vaults → Photos and switch the view to the gallery — the third button next to the table and grid icons. It is a view of the vault, not a separate app: nothing to install, no second index, no import step.

The gallery reads each picture's own EXIF on the node that holds it, so it groups by the day the shutter fired rather than by the file's modification time — which for a photograph is usually the day it was copied off the phone, and would file a whole holiday under "today". Pictures that carry no date collect in an Undated section that says whether they are genuinely undated or simply not read yet.

It is recursive from wherever you are, so you never walk into 2019/08/, and the folder structure is still available as a grouping if you organised your pictures deliberately. Tile size, grouping and sort are per-device.

Press and hold a picture — or Ctrl-click on a desktop — to start selecting; then drag across tiles, Shift-click for a run, or tick a whole day from its heading. The bar that appears can download the selection as a single zip, move or copy it into another folder, or delete it. Deleting keeps earlier versions, and the confirmation says so.

Two things it will not do. A HEIC from an iPhone or a raw file from a camera shows as an icon: there is no decoder for either in a build that ships without cgo, though the date and the place are still read. And there is no grouping by place name and no map, because both would mean sending the coordinates of your own photographs to somebody else's service — the coordinates are shown in a picture's details panel and stay on your machines.

On a television the same gallery is driveable with the remote, with larger tiles by default.

7. Optional: turn on the media player

The music / video library and player are off by default — a fresh node is infrastructure, not a media server. To browse a vault as a media library and stream from it, turn the feature on for that node in Profile → Settings → Media library. The Player tab then appears in the navigation. It's a per-node switch: leave it off on headless servers that only store and replicate, turn it on where you actually watch and listen.

Troubleshooting

Nodes don't see each other

  • Check the swarm-key matches on both — meshhold keygen output, exactly.
  • Open UDP 7777 (QUIC) and TCP 7778 (the fallback for networks that block UDP) in the VPS firewall.
  • Point the second node at the VPS directly: run meshhold node share on the VPS, then meshhold node connect '<the invite it printed>' on the second node. (bootstrap_peers in config.yaml only seeds a brand-new node; once a node has joined a network it no longer reads it.)

Phone keeps going offline

In the background (unless it is charging on Wi-Fi, or in a call) a phone does not stay connected to every node: it keeps one quiet connection, to its home node, and the rest of the mesh reaches it — and sees it present — through that node. Choose the home on the phone: open Network, pick the VPS (or the desktop), and choose Make home node from its menu. With none chosen, or the home not answering, the phone takes the first always-on node that answers.

The phone's power row (Profile → Network → Status) says where it stands: Standby via … names the node holding it; Deep means it holds no connection — no network, or no node answered — and it tries again after 1, 2, 5 and 15 minutes, then every 30, and at once when the network comes back or you open the app.

If it vanishes altogether, it is battery optimisation. In Android settings, mark MeshHold as unrestricted (and add it to any OEM "protected apps" list) so the background daemon's foreground service isn't killed — that service is what keeps chat and call notifications arriving while the screen is off.

RF stuck at 1

Open the vault's Health panel. If only one reliable node is online, RF can't climb. Bring up the second reliable node, or temporarily mark a third as reliable: true in its config.

Next up

Add a chat vault for your household (see The Secure Comms Hub), or expose Photos over S3 to back up your Immich instance (see The S3-compatible Backend).