This is how to put LocalGhost on a machine you own, from a bare Debian install to a phone that unlocks it. It's the procedure I follow on my own box, taken from the scripts in the server repo, in the order they expect them and with the reason for each step. The first release, wisp 0.0.1, came out on 2 October 2026. Build from its tag for the release, or from main for what I run the day I run it.
Every step is a script you can read before you run it, and the ones that destroy anything ask you to type a word first. Setup takes an afternoon, most of it downloads and compiling. A large photo library then takes the box a few days to describe and index, and the phone shows how far it has got. There's one rule about order (step 2) and one cost you should know about before you start, which is that there is no recovery path yet (section 5).
> 1. WHAT YOU NEED
| Machine | An x86-64 computer running Debian 13, with a terminal on it for the first run, since the enrolment code is drawn there. Mine is a Debian 13 box on a home network, and the Go toolchain the mirror carries is the linux-amd64 build. |
|---|---|
| TPM | TPM 2.0, and a firmware TPM is fine (Intel PTT, AMD fTPM), switched on in the BIOS. ls /dev/tpm* should list /dev/tpmrm0. A machine without one can run the software tier, with the price in section 5. |
| Data disk | A whole disk for the encrypted volume, separate from the one Debian is on. Setup turns the entire disk into one LUKS2 container with no partition table, so everything on it goes. Size it for your photo library plus about 10 GB of models and maps, and more if you want streets (Europe alone is 33 GB to download). |
| GPU | Optional. An NVIDIA card with its driver and the CUDA toolkit installed, which you do yourself. Mine is an RTX 4070 with 12 GB, which holds the 12B model and its vision projector. Without a card everything runs on the CPU, and the same box describes about 24 photos an hour.[6] |
| Phone | Android 15 or newer (the app's minimum SDK is 35), no Google Play services needed. |
| App build | A computer to build the app on, Windows with Android Studio or Debian with the repo's setup script, and a USB cable. I build on Windows and the release builds are made on Debian. |
| Network | Internet during setup, for the signed setup mirror (about 12 GB), the Debian, PostgreSQL and Redis apt repositories, and the Go module proxy. To reach the box away from home you also need a domain whose A record points at your public IP and your router forwarding TCP 443 to the box. On the home network it works without either. |
| Two PINs | A main PIN, and a wipe PIN if you want one. They must be different. The wipe PIN destroys everything on the volume and then shows the same unlock progress a real unlock would.[7] |
> 2. THE ORDER
Seven steps, all from the localghost repository, where the server lives in server/ and the phone app in app/android/.[1]
STEP 1. PREPARE THE MACHINE // BIOS, ROOT
Switch on the firmware TPM in the BIOS and install Debian 13. Then install the four things the scripts expect to find, and if there's an NVIDIA card, its driver and the CUDA toolkit. LocalGhost installs neither of those, and the engine build looks for nvcc under /usr/local/cuda*/bin and builds for the CPU when it doesn't find one. Clone the repo as your own login user, the one with sudo. That user becomes the service user, which builds and owns the code, and the daemons later run as a separate user that can't log in.
# /dev/tpmrm0 should be there ls /dev/tpm* # as root apt install sudo git make nginx # as your login user, the release, wisp 0.0.1 git clone --branch v0.0.1 https://github.com/LocalGhostDao/localghost.git # or the newest code instead git clone https://github.com/LocalGhostDao/localghost.git
STEP 2. BUILD THE APP AND INSTALL IT // YOUR COMPUTER, PHONE
On Windows, install Android Studio (it brings JDK 21) and in its SDK Manager tick Android API 37 and 36, Build-Tools 36.0.0, NDK 28.2.13676358 and CMake 3.22.1, the versions the build pins. Put the repo in a path without spaces, turn on USB debugging on the phone, plug it in and run gradlew.bat installDebug in app\android. On Debian, one script installs the same toolchain.[3]
app/android/tools/debian_setup.sh
source ~/.localghost_android_env
adb devices # the phone should be listed
cd app/android && ./gradlew installDebug
The app carries its own small model for when the box is slow or out of reach, and the engine behind it is the same llama.cpp source the box builds. The build fetches that tarball from the setup mirror and refuses it unless its SHA-256 matches the pin in app/src/main/cpp/CMakeLists.txt. With no internet, put llamaTarball=<path> in local.properties. To check, open MODELS in the app. The top line should say "runtime in this build (llama.cpp v0.5.0-…)", and if it says the build carries no model runtime, the pin is empty and the app works without a model of its own.
That's a debug build, which is fine for your own phone. Release builds are signed and published with their hashes, and app/android/VERIFY.md explains how to rebuild one and get the same bytes.[4]
STEP 3. BUILD THE ENGINE AND FETCH THE MODEL // THE BOX, SUDO
Before the box is provisioned, build llama.cpp's llama-server and fetch the weights. The script builds the pinned llama.cpp release from the mirror, with CUDA when it finds nvcc, into the repo's bin/, where provisioning picks it up along with the daemons. It fetches Gemma 4 12B with its vision projector and the EmbeddingGemma embedder, checks each file against tools/model.pins and stages them on the system disk, and the first unlock moves them onto the encrypted volume.
cd localghost/server # from the mirror, 7.7 GB sudo ./tools/setup_llama.sh # or from files you already have, still checked sudo ./tools/setup_llama.sh --models /media/usb
The CUDA build targets compute capability 8.9, the RTX 40 series, and that flag is written into the script, so a card from another generation means changing CMAKE_CUDA_ARCHITECTURES in setup_llama.sh first. If you run it after provisioning instead, the setup in step 4 reminds you, and the script prints how to put the engine onto a volume that already exists.
STEP 4. RUN THE GUIDED SETUP // THE BOX, SUDO
One command walks the rest of the server setup, and it changes nothing on the data disk until you've typed the word it asks for.[2]
sudo ./tools/setup.sh <your-user>
- Packages. Postgres 18 with pgvector and Redis 8 from their own apt repositories, Go 1.25.4 from the mirror if the machine doesn't have it, checked against the signed manifest, then
cryptsetupandtpm2-tools. It loadsdm_crypt, lets your user reach the TPM through thetssgroup, adds a narrow sudo rule for managing theghost.secdunit, and writes/etc/ghost/ghost.env. On a machine that had no database before, the Postgres and Redis services the packages start on their own are masked, because the box runs private instances inside the encrypted volume, on their own ports. Databases the machine already runs for other things are left alone. - Checks. As your user, in a fresh login so the new group counts, it confirms that the packages, the TPM, the state folder and the sudo rule all work for you.
- Build.
make box, as your user, builds every daemon and tool intobin/. - Disk. It lists the disks and you give it one. Use the
/dev/disk/by-id/name, since names likenvme0n1can swap between boots.[8] It shows the model, size and serial number and asks you to typeYES, plusWIPEITif the disk already holds a LUKS container andIUNDERSTANDif anything on it is mounted. - Seal and name. The seal tier,
tpmfor a real box orsoftwarefor a machine without one, and the domain, left blank for the home network only. - Provision. It creates
ghostd, the user the daemons run as (no password, no shell), runsghost-setupas a dry run that touches nothing, and asks you to typeAPPLY. The apply asks for the main PIN and the wipe PIN on the terminal, formats the disk, makes the box its own certificate authority, writes the nginx site and the systemd unit, startsghost.secdand draws the QR.
STEP 5. SCAN THE QR, THEN UNLOCK // PHONE
Scan it with the app as soon as it appears. Scanning is the enrolment, with no code to type, no confirmation and no network call. The code is 858 bytes split over twelve frames, the phone's certificate and private key, the box's address, its name and the fingerprint of its server certificate, and any eight frames rebuild it, so a clean scan takes 8 to 12 seconds.[6] The box stops showing frames after the first verified connection. Then clear the terminal and its scrollback, because anyone with a recording of those frames holds a working device identity (they'd still need the PIN).
Unlock with the main PIN from the app. The box unseals its key, opens the volume, takes in the weights from step 3 and starts the fleet, and Box Status on the phone fills in.
# the engine is up, on the GPU or the CPU sudo ./tools/ns.sh ./bin/ghost-cli ghost.oracled status # a new QR, for a second phone or a missed scan sudo ./bin/ghost-qr --ca /etc/ghost/ca --host <host>
The app trusts the box by that fingerprint, not by its name. If the box sits behind a router without NAT hairpinning, the domain won't reach the box from inside your own network, so use the LAN address as the host and keep the domain for outside. Enrolment doesn't need the domain at all, and you can leave it blank at setup and add it later.
STEP 6. BRING THE REST IN // THE BOX, UNLOCKED
Everything else the box uses comes from the same mirror with the same checks, in one command, and the rest of the block finishes the job.
# maps, embedder, weights, phone model, engine, speech sudo ./tools/update.sh # streets, one continent at a time sudo GHOST_GEO_ROADS=europe-latest.osm.pbf ./tools/update.sh maps # as your user, the database runtimes onto the volume ./tools/bundle_db_runtime.sh /var/lib/ghost/mnt/slot0 --verify # every daemon, the pipeline, the engine, the map layers sudo ./tools/health.sh # tpm, or software sudo grep GHOST_SEAL_MODE /var/lib/ghost/seal.env
update.sh fetches only what differs from the signed list. The maps step imports the place names and cuts the coastline into tiles in the background, the speech step builds whisper.cpp and fetches its model, and voice notes start transcribing within a minute. The phone step sets the model the app offers under MODELS, and the phone downloads it from the box, never from the internet, and checks it against the hash the box states. Streets are opt-in because they're big. Africa takes about four minutes to cut and Europe takes hours. bundle_db_runtime.sh copies the Postgres and Redis runtimes onto the encrypted volume and tests them there, so the system disk can stop carrying database software. If the model turns out to be on the CPU when you expected the GPU, sudo ./tools/gpu.sh says which layer failed.
Then turn on sync in the app. Photos, videos, the location trail and Health Connect data start flowing, and Box Status shows each pipeline stage as done, total and pace.
STEP 7. DAY TO DAY // THE BOX
# code from git, builds and restarts secd, so the box locks git pull && sudo ./tools/redeploy.sh # new data from the mirror sudo ./tools/update.sh # a hung box resets itself and comes back locked sudo ./tools/watchdog.sh --arm # a shell inside the namespace, standing on the volume sudo ./tools/ns.sh # the box's privacy settings, encrypted swap included sudo ./tools/privacy_check.sh
Releases reach the box through the phone. Once a day on Wi-Fi the app reads the mirror's signed list, and when a newer release is there it says so ("wisp 0.0.1 is out"). DEPLOY, under SETTINGS and SERVER, downloads the release and hands it to the box, which checks the signature and every hash with the same verifier setup used, keeps the previous build and switches to the new one. It goes back by itself if the first unlock onto the new build fails or a critical daemon keeps falling over in its first ten minutes, and ROLL BACK sits beside DEPLOY.[12]
A redeploy from git is for code you've built yourself. It restarts ghost.secd, which locks the box, and you unlock again from the phone. New daemon binaries wait in a staging folder on the system disk and move onto the volume at that unlock. Arm the watchdog on any box you can't walk up to. Mine froze once while I was away and stayed frozen until someone could press the button.[6]
> 3. HOW IT FITS TOGETHER
Only two processes serve LocalGhost from the unencrypted system disk, nginx and ghost.secd, and ghost.secd is the only LocalGhost unit systemd starts. nginx holds port 443 and checks the phone's client certificate against the box's own certificate authority with ssl_verify_client optional, then passes the verdict to secd, which answers 503 to anything without a valid certificate.[9] A stranger scanning the box finds a web server that seems to be down.
phone ─ mTLS ─▶ nginx :443 ─▶ ghost.secd system disk │ PIN ─▶ TPM unseals the key ─▶ LUKS2 opens │ in secd's private namespace ├── postgres, redis on the volume └── ghost.watchd ├── framed, searchd, synthd, cued ├── noted, voiced, tallyd, shadowd └── oracled ── llama-server
When the right PIN arrives, secd unseals the volume key from the TPM, opens the LUKS2 container and mounts it inside its own private mount namespace, so from the host the mount point looks empty whether the box is locked or not.[10] It then starts Postgres and Redis from the volume and ghost.watchd, which starts each daemon from the volume's bin/, restarts any that fall over and keeps their logs. The model runs as a private llama-server child of ghost.oracled on a loopback port, and the embedder as one of ghost.searchd, with the weights read from the volume. Locking runs the same steps in reverse, and everything except secd and nginx stops existing until the next PIN. The daemons and their jobs are on the home page.
On the system disk, /etc/ghost holds the box's settings and its certificate authority, /var/lib/ghost holds the seal, the staging folders and the mount point, and /opt/localghost/bin holds secd and the operator tools. Everything else, the archive, the databases, the models, the logs and the daemons themselves, is on the volume.
> 4. WHAT THE BOX TALKS TO
During setup, the box downloads from three kinds of place. Everything it can get from the setup mirror comes from there, signed and checked against a key pinned in the repo, and if the mirror can't be reached, setup says so and stops.[5] A box with no internet at all can read a copy of the mirror from a USB disk with GHOST_MIRROR=file:///media/usb/mirror. The Go modules for the build come from proxy.golang.org, pinned by go.sum, and the packages from Debian's, PostgreSQL's and Redis's own apt repositories.
After setup, the phone is the only thing that talks to the box, and the box asks the internet only for public things, with nothing of yours in the request. ghost.tallyd reads prices from the exchanges and the ECB itself, every minute. News feeds come from the phone while it's on Wi-Fi and from the box when the phone is away. A coin's page reads the coin's own website, Wikipedia's API is asked only by a box without its own copy of Wikipedia, and updates come from the mirror. The phone runs the web searches and reads the pages, and the box gets the paragraphs. Never a map tile, a location, a photo or a note.[12]
> 5. WHAT IT COSTS
There's no recovery path yet. On the tpm tier the volume key is sealed to this board's TPM, in the boot state it measured, and nothing else holds a copy, so a dead board or a forgotten PIN means the archive on that disk is gone. The two hardware keys described on the About page aren't part of setup yet. Until they are, keep the originals of anything you can't lose somewhere else as well.
The software tier wraps the key under Argon2id of the PIN in /var/lib/ghost/seal.env, so someone with the raw disk and that file can guess PINs offline with nothing to stop them, and a short PIN under Argon2id is still a short PIN.[11] It's for machines without a TPM, and health.sh says which tier a box is on rather than letting you assume.
The box's certificate authority key sits on the system disk, so root on the box, or someone holding that disk, could make a phone certificate, and the PIN would be the only gate left. secd also trusts a certificate header passed to it on its loopback port, so on a box that runs other services, those services can reach it without a certificate. Both are on the security list in the release notes, and until they're fixed, run nothing on the box you don't trust. Releases are signed by the same site key as the mirror, and a separate offline release key is planned. There's no per-device revocation. Phone certificates are valid for ten years and nginx has no revocation list, so shutting out one phone today means a new certificate authority and enrolling every phone again. The QR is a credential while it's on screen, as step 5 says. A first import of a large library takes days on a GPU and longer on a CPU. And this is pre-release software that changes every day on my box first, so read each script before you run it and let the dry runs do their job. What the Box Does Now goes through each part with what it costs, and what broke on the way.
If a step here doesn't match what the script does on your machine, the script is right and this page is late. Email info@localghost.ai or open an issue, and I'll fix whichever one is wrong.
> REFERENCES
server/ (the standard library plus go-tpm and three golang.org/x packages) and the Kotlin app in app/android/, and server/tools/README.md, which lists every tool and the bring-up order this page follows, including the rule about installing the app before the QR. Source for the order and the commands. github.com/LocalGhostDao/localghost and server/tools/README.mdserver/tools/setup.sh, the guided setup, which runs as root, builds as the service user with sudo -u, and asks for a typed word before the disk step and before the apply. With install_db.sh, server_setup_root.sh and server_setup_user.sh beside it, the source for the six stages in step 4. server/tools/setup.shapp/android/BUILDING.md, the app's build guide for Windows and Debian, with the pinned SDK, NDK and CMake versions, the local.properties key for an offline llama.cpp tarball and the release script's variables. Source for step 2. app/android/BUILDING.mdapp/android/VERIFY.md, how a published APK is built, signed with a detached signature by info@localghost.ai and checked by rebuilding it from the same commit with the toolchain recorded in the build environment file. Source for the release builds in step 2. app/android/VERIFY.mdserver/tools/mirror_fetch.sh, which checks the manifest against the key fingerprint pinned in the repo (DCE9 A3D1 4EB4 6197 1DD5 F393 706E 4194 F08A 09A0) and each file against its SHA-256 before giving it its real name. Source for what the box downloads and how. localghost.ai/mirrornvme0n1 are handed out in the order devices are found on each boot, and the stable handles are the IDs under /dev/disk/by-id. Source for the disk advice in step 4. wiki.archlinux.org/title/Persistent_block_device_namingngx_http_ssl_module, the ssl_verify_client directive. With optional, nginx asks for a client certificate, verifies it if one is offered and passes the result on instead of refusing the connection itself, which is what lets secd answer every unauthenticated request with the same 503. Source for section 3. nginx.org/en/docs/http/ngx_http_ssl_module.htmlv0.0.1 of 2 October 2026, and its notes, which describe how the phone takes a release from the mirror and how the box checks it and rolls it back, list what leaves the box, and name the known gaps (the certificate authority key on the system disk, the trusted header on the loopback port, releases signed by the site key alone, no Mist and no decoy volume yet). Source for step 7 and sections 4 and 5. github.com/LocalGhostDao/localghost/releases/tag/v0.0.1