Local-First Is an Operating Choice, Not a Lifestyle Brand

Keeping a large runtime local only works when the machine, storage, and recovery behavior are operated as one system.

A MacBook with lines of code on its screen on a busy desk

Keeping a large runtime local only works when the machine, storage, and recovery behavior are operated as one system.

The migration began with an unglamorous constraint: the runtime had outgrown the storage arrangement under it. Moving its data to an external device looked like a file operation. It wasn't. It was a service transition involving databases, container workloads, indexers, caches, and the processes that quietly reopen files after everyone thinks they've stopped.

That distinction is why I take “local-first” as an operating choice. It says where responsibility lands. There's no storage team on the other side of a ticket, and no managed service quietly repairing a sloppy cutover. I chose local execution, so quiescence, media behavior, mount timing, restart order, and rollback landed with me too.

Start with writers, not bytes

I started by listing process classes. Long-running application services were obvious. Database processes were obvious too, although “stopped” had to mean cleanly shut down, not merely absent from a dashboard. Less obvious writers included container runtimes updating metadata, background indexers, downloaders, log shippers, and model services maintaining local state.

The question for each class was simple: can this process write into the tree during the copy? If the answer was yes or “maybe,” it had to stop. Read traffic wasn't a useful exemption. A service that looks read-only at the application layer may still update a journal, access database, lock file, or cache. Computers are fussy about distinctions humans find convenient.

I recorded the shutdown order and checked the process table afterward. This wasn't ceremony. It separated “I issued stop commands” from “nothing is writing.” I've learned not to confuse those two. The copy could begin only in the second state.

Local systems encourage casual familiarity. The machine is close, the data is visible, and the temptation is to treat maintenance like rearranging a desk. A live data tree does not become safe because I can hear the computer running beside me.

Copy the shape faithfully

The runtime data wasn't a neat collection of ordinary files. It included sparse files whose apparent size and allocated size were different. A naive copy can expand those holes, consume far more space than expected, and still leave a directory that looks complete at a glance.

So the transfer had to preserve the filesystem shape, not merely filenames and timestamps. I used a sparse-aware copy and treated its completion as the start of verification. The destination was then checked against the quiesced source with checksums. I didn't retain or publish the values; what mattered was that the comparison ran against a stable source and that mismatches would block the next step.

This is where the economics of local-first become honest. External storage may be inexpensive and physically simple, but correctness still costs attention. The savings do not repeal the need for a migration plan. They move that work into my hands.

The original tree stayed intact. I didn't repurpose it, clean it up, or make space “while I was already in there.” That last impulse is how one maintenance job auditions for a sequel. Until the new arrangement survived an operational exercise, the source was the rollback asset.

Make the mount part of startup

A destination can pass a checksum and still be a bad runtime location. Permissions can differ. A mount can arrive late during boot. A path can exist as an empty directory when the device is absent, letting a service write to the wrong filesystem without making much noise.

I checked ownership and effective access as the service identities that would use the data. Then I treated attachment and mounting as dependencies, not ambient facts. The runtime shouldn't start against a placeholder simply because the storage hasn't appeared yet.

Only after those conditions were explicit did I point the workload at the external device. Services came back by class, with stateful components ahead of the applications that depended on them. I watched actual process behavior and application access rather than accepting a successful mount command as end-to-end proof.

I also checked the negative case before calling the dependency complete. If the expected device was absent, startup needed to stop visibly instead of creating fresh state on the underlying directory. Otherwise I'd have old data sitting safely on external storage while a newly started service wrote somewhere else under the same apparent location. That's a bad morning wearing a normal-looking path.

This is one reason I resist turning local-first into an aesthetic. Hardware is the photogenic part. Dependency ordering decides whether an ordinary restart becomes recovery work.

Rehearse the failure boundary

The decisive test was deliberately physical in its logic: stop the runtime, detach the storage, restore the attachment, remount it, and restart. That exercise checked more than media availability. It checked whether shutdown was repeatable, whether the device returned where the services expected it, whether startup respected the dependency, and whether the applications could reopen their state.

I also confirmed that rollback remained available from the preserved source. A rollback I'm afraid to use because I modified it during cutover isn't much of a rollback.

Once the detach, remount, and restart sequence passed, the external device had earned its role in the runtime. The migration was no longer “the files seem to be over there.” It was a known operating sequence with a quiet source copy still available if the new boundary failed.

Local-first gives me low latency, direct control, and a useful degree of independence. It also leaves storage transitions nowhere to hide. I called this one done only after every writer was stopped, the sparse copy and checksum comparison agreed, the former source remained untouched, and the complete runtime returned after the storage had genuinely gone away and come back.