Setup Guide · v2.1.1

Build your personal cloud
in about an hour.

Every step, every command, every gotcha. Pick your operating system below — every command block updates to match.

Your choice is remembered across the page.
~60Minutes
9Steps
$130One-time cost
1
~ 10 min · Shopping

Gather the parts

Everything ships from Amazon or Adafruit. Total ≈ $130 for a 1 TB build. The Pi 5 is the only non-substitutable part; everything else has good alternatives.

Raspberry Pi 5
$60–80
4 GB or 8 GB model
NVMe SSD
$25–100
M.2 2230 or 2242
NVMe HAT
~ $15
Pimoroni or Pineboards
27 W USB-C PSU
~ $12
Must be 27 W, not 15 W
MicroSD card
~ $8
32 GB+ Class 10
Why an NVMe SSD instead of just using a bigger MicroSD?

MicroSD cards have a limited number of write cycles and are dramatically slower (~50 MB/s vs NVMe's ~500 MB/s). A Pi running a database on an SD card will wear the card out in months. The NVMe sits on a HAT that plugs into the Pi 5's PCIe port — it's the same storage shape laptops use, so a 512 GB drive is under $50 and a 2 TB drive is under $150. The MicroSD on Ugh! Storage only holds the operating system; none of your files ever touch it.

Does the PSU wattage really matter?

Yes — don't reuse a Pi 4 charger. The Pi 5 expects a 5V/5A (27 W) supply. With only 15 W, the Pi still boots but PCIe (the NVMe) gets de-prioritized and may fail to enumerate, or run at reduced speed. The official Raspberry Pi 27 W USB-C PSU is the known-good option.

2
~ 10 min · Hands-on

Assemble the hardware

Work on a non-metal surface. Ground yourself by briefly touching something metal (radiator, computer case) to discharge any static. Check off each step as you go — it saves across refreshes:

Ribbon cable orientation matters
The PCIe ribbon has a blue/silver side and a gold-contact side. Gold contacts face down on the Pi end and up on the HAT end (check your HAT's manual — orientation varies by brand).
3
~ 10 min · All platforms

Flash the operating system

We use Raspberry Pi Imager, the official flashing tool from the Raspberry Pi Foundation. It runs on all three OSes and handles cloud-init for us — meaning username, password, SSH key, and WiFi all get baked into the SD card before first boot.

  1. Download Raspberry Pi Imager for macOS: imager_latest.dmg
  2. Open the DMG, drag Imager into Applications, then launch it. First run will ask for your Mac password (to write to removable media).
  1. Install via apt, dnf, or Snap:
    # Debian / Ubuntu
    sudo apt update && sudo apt install -y rpi-imager
    
    # Fedora
    sudo dnf install -y rpi-imager
    
    # Any distro via Snap
    sudo snap install rpi-imager
  2. Launch it: rpi-imager in a terminal, or find it in your applications menu.
  1. Download for Windows: imager_latest.exe
  2. Run the installer. Accept the UAC prompt (it needs raw disk access to write the SD card).
  3. Launch from Start menu. If Windows Defender SmartScreen warns you, click More info → Run anyway — the binary is signed by Raspberry Pi Ltd.

Now configure the flash

In Raspberry Pi Imager:

  1. Choose Device: Raspberry Pi 5
  2. Choose OS: Raspberry Pi OS (other) → Raspberry Pi OS Lite (64-bit). Lite means no desktop — faster boot, less disk use, and we SSH in anyway.
  3. Choose Storage: your MicroSD card. Triple-check the size matches your card — Imager will overwrite whatever is selected.
  4. Click Next. When asked “Would you like to apply OS customisation settings?” click Edit Settings.

OS customisation settings (critical)

Write these down
Every value below becomes part of your Pi's identity. Save the username + password into your password manager now, before you close Imager — there's no way to recover them later without reflashing.
  • Hostname: ughstorage
  • Username: pick one you'll remember (the default guide uses ughstorage). Avoid pi or admin — common targets for bots.
  • Password: generate a strong one with your password manager. This is your SSH login forever.
  • Configure wireless LAN: your home WiFi SSID + password. Select your country (required for WiFi compliance).
  • Set locale: your timezone + keyboard layout.
  • In the Services tab: enable SSH, password authentication. (You can add a public key too — paste the contents of ~/.ssh/id_ed25519.pub — but password is fine for first boot.)

Click Save → Yes → Yes when it warns you about data loss. The flash + verify takes ~5 minutes. When Imager says "write successful", safely eject, put the card in the Pi, and plug in the 27 W PSU. Wait ~2 minutes for first boot (cloud-init expands the filesystem and applies your settings).

4
~ 5 min · First login

SSH into the Pi

The Pi announces itself on the local network via mDNS as ughstorage.local. All three operating systems can resolve this without extra setup. Open your terminal:

Open Terminal.app (⌘+Space, type "Terminal"). macOS ships with OpenSSH built-in; no install needed.

# Replace "ughstorage" with whatever username you set in Imager
ssh ughstorage@ughstorage.local

Accept the fingerprint prompt (type yes + Enter), then enter the password you set. Prompt will change to ughstorage@ughstorage:~ $ — you're in.

Open your terminal (Ctrl+Alt+T on most distros). OpenSSH is standard on every major Linux. If missing: sudo apt install openssh-client or your package manager's equivalent.

# Replace "ughstorage" with whatever username you set in Imager
ssh ughstorage@ughstorage.local

Accept the fingerprint (type yes), enter the password. You should land at ughstorage@ughstorage:~ $.

Windows 10+ includes OpenSSH. Press Win + XTerminal (or PowerShell). If the ssh command is missing, install it via:

Add-WindowsCapability -Online -Name OpenSSH.Client~~~~0.0.1.0
ssh ughstorage@ughstorage.local

Accept the fingerprint (type yes), enter the password. If mDNS fails (rare on older Windows setups), see troubleshooting below.

Add your SSH key for passwordless login (highly recommended)

This lets you SSH in without typing a password every time, and later lets you run rsync for backups:

# Generate an Ed25519 key (modern, small, strong)
# Press Enter for defaults; optional passphrase
ssh-keygen -t ed25519 -C "$(whoami)@$(hostname)"

# Install it on the Pi
ssh-copy-id ughstorage@ughstorage.local
ssh-keygen -t ed25519 -C "$(whoami)@$(hostname)"
ssh-copy-id ughstorage@ughstorage.local
# Generate the key
ssh-keygen -t ed25519 -C "$env:USERNAME@$env:COMPUTERNAME"

# Install it on the Pi (Windows has no ssh-copy-id — this works instead)
type $env:USERPROFILE\.ssh\id_ed25519.pub | ssh ughstorage@ughstorage.local "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"
Why ughstorage.local instead of an IP address?

.local is mDNS (multicast DNS), a zero-config name-resolution protocol. macOS, most Linux distros (via avahi-daemon, which Raspberry Pi OS runs out of the box), and Windows 10+ all speak it. Your router doesn't need to do anything. The upshot: even if your router assigns the Pi a different IP tomorrow, ughstorage.local still works.

What if ughstorage.local doesn't resolve?

Two possible fallbacks:

  1. Find the Pi's IP via your router. Log into your router's admin page (usually 192.168.1.1), look for "connected clients" or "DHCP leases", and find a device named ughstorage. Then use that IP: ssh ughstorage@192.168.1.xxx.
  2. Scan the LAN: nmap -sn 192.168.1.0/24 (install nmap first on Windows: choco install nmap).

mDNS often fails on corporate or guest WiFi networks where client isolation is enabled. Use the wired Ethernet port on the Pi in that case.

5
~ 8 min · On the Pi

Prepare the NVMe SSD

Everything from here is on the Pi (you're SSH'd in). We'll partition the NVMe drive, format it with ext4 (Linux's standard filesystem), and mount it at /mnt/nvme — the location every Ugh! Storage path expects.

Confirm the NVMe is detected

lsblk

You should see a line starting with nvme0n1. If you don't, stop here — the HAT or cable isn't seated properly. Power off (sudo poweroff), reseat the ribbon cable, power back on, try again.

Partition, format, mount

# 1. Partition: one partition spanning the whole drive
sudo parted /dev/nvme0n1 --script mklabel gpt
sudo parted /dev/nvme0n1 --script mkpart primary ext4 0% 100%

# 2. Format as ext4 (Linux's journaling filesystem)
sudo mkfs.ext4 -F -L ughstorage /dev/nvme0n1p1

# 3. Mount it + make your user the owner
sudo mkdir -p /mnt/nvme
sudo mount /dev/nvme0n1p1 /mnt/nvme
sudo chown -R $USER:$USER /mnt/nvme

# 4. Remount automatically on every boot
echo "LABEL=ughstorage /mnt/nvme ext4 defaults,noatime,nofail 0 2" \
  | sudo tee -a /etc/fstab

# 5. Verify — should show the full drive capacity
df -h /mnt/nvme
What does each command do?

parted … mklabel gpt writes a GPT partition table (modern, supports drives >2 TB). mkpart creates one ext4 partition spanning the whole drive.

mkfs.ext4 -L ughstorage formats the partition and labels it — we mount by label in fstab so a different USB drive plugged in doesn't confuse the boot order.

chown -R $USER:$USER gives your Linux user (the one you SSH'd in as) ownership of the mount, so you don't need sudo for every write.

noatime in fstab is a performance win: Linux normally updates a file's access time on every read, which is an extra write for every read. You don't need that.

nofail means "if this drive is missing at boot, don't hang — just skip it." Prevents an unrecoverable boot loop if the NVMe ever comes loose.

This erases the drive
Double-check with lsblk that /dev/nvme0n1 really is your new NVMe and not something else. Brand-new drives never have data you care about, but belt-and-suspenders.
6
~ 8 min · On the Pi

Install Ugh! Storage

Clone the public setup repo and run the setup script. The script is idempotent — safe to re-run if anything fails partway.

# Install git if it's missing (the minimal Pi OS image skips it)
sudo apt update && sudo apt install -y git

# Clone the PUBLIC setup repo's stable branch — pre-signed, verified releases
cd ~
git clone --branch stable https://github.com/hneogy/Ugh-Storage--Setup-Files.git server-src
cd server-src/server

# Make the shell scripts executable
chmod +x setup.sh ble_setup_service.sh

# Run the main installer
./setup.sh

The installer runs six phases and prints [N/6] markers so you can see progress:

  1. System dependencies — Python 3.11, pip, ffmpeg, sqlite, curl
  2. Cloudflared — the tunnel binary (outbound-only, explained in Architecture)
  3. Python virtualenv — creates /home/USER/server/venv and installs pinned deps from requirements.txt
  4. Storage directories/mnt/nvme/storage, /mnt/nvme/thumbnails, /mnt/nvme/hls
  5. .env file — writes your device's Supabase config (read from the repo's .env.example)
  6. Systemd service — installs + enables ughstorage.service, auto-starts on boot, auto-restarts on failure

When it finishes, verify the service is up:

sudo systemctl status ughstorage --no-pager
curl -fs http://127.0.0.1:8000/health && echo "  ✓ server is live"

Expect Active: active (running) and a JSON response ending with "ok".

I'd rather use the private dev repo

If you have write access to the private hneogy/ughstorage repo, swap the clone URL:

git clone git@github.com:hneogy/ughstorage.git server-src

You'll need SSH key auth to GitHub. The public stable branch is always the same code but signed and tag-locked to released versions.

7
~ 3 min · On the Pi

Install the Bluetooth setup service

This second service advertises your Pi over Bluetooth so the iOS app can find it during pairing. It's only used during initial setup — once the Pi is paired, the BLE service mostly idles.

sudo bash ble_setup_service.sh

This configures:

  • BlueZ — the Linux Bluetooth stack (already installed; we just enable it)
  • NetworkManager — lets the iOS app push WiFi credentials over Bluetooth
  • ughstorage-ble.service — a systemd unit that runs ble_setup.py as a GATT server
Your SSH may drop briefly
The NetworkManager switch restarts networking. If SSH disconnects, wait 30 seconds and reconnect with the same command: ssh ughstorage@ughstorage.local.
8
~ 2 min · Anti-abuse gate

Get a provisioning token

A provisioning token is a short code (like UGH-R3P2-K9LM-TC71-4) that proves you bought a device or got invited to the beta. The iOS app asks for this during pairing so random strangers can't register 10,000 Pis against our infrastructure.

How to get one

  • Pre-built device owners: printed on a sticker on the bottom of the enclosure — scan the QR code with your phone camera, or type it manually.
  • DIY builders in the beta: request one at ughstorage.com/request-token. You'll receive a token via email, usually within a few hours.
Keep it safe
A token is tied to one device. Once you pair with it, the token is "claimed" and won't work for anyone else — but the same user can re-use it to re-pair after a reset.
What does the token prevent?

Without a gate, anyone could spin up 10,000 Raspberry Pis (or cloud VMs pretending to be Pis) and register each one as a device, consuming Cloudflare tunnel quota and Supabase quota that the project foots the bill for. Tokens cap that at "whoever we've handed one to."

Technically: the Supabase register-device edge function verifies the token against the provisioning_tokens table, checks it isn't revoked/expired/already claimed by someone else, and only then provisions a Cloudflare tunnel.

9
~ 4 min · On your iPhone

Pair with the iOS app

Open the Ugh! Storage app (App Store). Create an account (email+password, Sign in with Apple, or Sign in with GitHub), then tap Add Device. A 4-step pairing wizard walks you through:

  1. Enter your provisioning token — paste it or scan the QR code. The app validates the format + checksum before contacting the network.
  2. Find your Pi — the app scans Bluetooth. Your Pi appears as UghStorage-Setup. Tap to connect (takes a few seconds).
  3. Choose WiFi — the Pi reports back every network it can see. Pick yours and enter the password. The Pi connects and reports back when it's online.
  4. Register & done — the app sends your auth token + provisioning token to the Pi, which calls Supabase's register-device function. A Cloudflare tunnel is provisioned, the Pi starts phoning home every 5 minutes, and the app switches from BLE to HTTPS. Your personal cloud is live at cloud.ughstorage.com.
You're done
Open the Files tab and start uploading. Device status on Settings will show the green Online dot within ~30 seconds of the first heartbeat landing.
Common issues

Troubleshooting

I forgot the SSH password I set in Imager

Pull the MicroSD, put it in your Mac/PC/Linux box. Open the bootfs partition, append init=/bin/sh to the end of cmdline.txt (all on one line), eject, boot the Pi with HDMI+keyboard. You'll land at a root shell. Run mount -o remount,rw / then passwd USERNAME to reset. Power off, restore cmdline.txt, boot normally.

The app says "Connection Lost" after setup worked

Three things to check on the Pi:

sudo systemctl status ughstorage     # FastAPI running?
sudo systemctl status cloudflared    # Tunnel alive?
sudo journalctl -u ughstorage -n 50  # Any errors?

If the tunnel is down: sudo systemctl restart cloudflared. If the server is down: sudo systemctl restart ughstorage.

NVMe not showing up in lsblk

Power off the Pi completely. Reseat the PCIe ribbon cable — it's a press-fit (no latch), so it's easy to miss. Verify the HAT's orientation matches its documentation (there's usually an arrow or text printed on the board). Check that the NVMe screw is tight enough that the drive lies flat. Boot again.

Bluetooth pairing fails in the app

On the Pi:

sudo systemctl restart ughstorage-ble
sudo systemctl status ughstorage-ble

On the iPhone: force-quit the app, toggle Bluetooth off-then-on, try again. Stay within ~3 meters of the Pi; BLE range indoors is much shorter than marketing specs suggest.

Where are my files stored on disk?

/mnt/nvme/storage (originals), /mnt/nvme/thumbnails (auto-generated thumbs), /mnt/nvme/hls (pre-transcoded HLS for video streaming). SQLite metadata is at /mnt/nvme/ughstorage.db. Nothing leaves the NVMe — ever.

All 9 steps complete

Your cloud is yours now.

Files go straight to your NVMe. No company between you and your data — ever.