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
Photosreplicated 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-amd64The image is distroless static and runs as uid 65532. The
meshhold-datanamed volume picks that ownership up automatically; a host-path bind-mount needschown 65532:65532first.
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
- Install the APK and open the app.
- Tap Scan invite, point it at the QR from the VPS.
- From the app's Scan tab, scan the
Photosvault's QR (on the VPS: the vault's menu → Show QR / Link). - 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 keygenoutput, exactly. - Open UDP
7777(QUIC) and TCP7778(the fallback for networks that block UDP) in the VPS firewall. - Point the second node at the VPS directly: run
meshhold node shareon the VPS, thenmeshhold node connect '<the invite it printed>'on the second node. (bootstrap_peersinconfig.yamlonly 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).