All articles

Replace the machine. Keep the project.

Delete a machine without losing its project files. Follow a managed volume through a controller restart, a replacement machine, read-only access and explicit cleanup.

The project is fine. The machine is the part you want to replace.

Maybe you need a different toolchain. Maybe an experiment left the environment hard to trust. You want a clean machine without uploading the same project again or losing the files you just created.

A persistent machine helps until you delete it. A managed volume lets the data have a longer life than the machine.

This walkthrough writes one file, deletes its machine, restarts the Elixir controller, and reads that file from a replacement. Then it changes the file, checks a read-only mount, and removes everything explicitly. One small file makes the boundary easy to see before you put a real project behind it.

Local volumes arrived in SmolBox 0.4.0. The example below uses SmolBox 0.4.1, smolvm 1.22.0 and a Linux worker, with PostgreSQL holding the management records.

Give the project its own lifetime

A machine has its own identity, processes and disks. A managed local volume has a separate identity and a directory on the worker. At creation, you attach the volume at an approved guest path. The guest reads and writes that directory through the mount.

Deleting the machine ends the attachment. It does not delete the volume. A replacement on the same worker can then mount the same data.

The volume survives machine replacement; it still depends on that worker’s storage.

The first machine writes the file.

Original machineRunning · can read and write
Mounted at /mnt/volumes/data
Project volume2 GiB reserved
kept.txtoriginal

Its own identity. Kept until explicit deletion.

The file lives on the volume. The machine’s other files and processes have their own lifetime.

Follow one volume through machine replacement and cleanup. This is an explanatory model, not a live worker or a timing measurement.

Notice what stays in place: the volume. There is no export, copy, or restore between these machines. The replacement reads the existing directory.

That also makes the boundary clear. A package installed elsewhere on the original machine does not move with the project. Neither does a running development server or a variable in memory. Prepare tools through the replacement’s approved image and keep the data you want to reuse under the mount.

Approve where the data lives

Use the versioned durable host example for the complete setup. It includes the PostgreSQL adapter, migrations, worker configuration, command helpers and cleanup checks. Start from the v0.4.1 source in a separate checkout:

git clone --branch v0.4.1 --depth 1 https://github.com/hfiguera/smolbox.git smolbox-volume-demo
cd smolbox-volume-demo/examples/durable_host

Follow that README’s setup section before running the phases below. You need a Linux worker running smolvm 1.22.0, an approved Python artifact, PostgreSQL and the encryption and fingerprint key files. Keep the same worker, keys and store partition between processes. Use a fresh execution ID and partition for this experiment.

The extra approval is the worker’s actual canonical volume directory:

{:ok, policy} = SmolBox.VolumePolicy.new(
  "project-data",
  "/srv/smolvm/.local/share/smolvm/volumes"
)

That path is an example, not a universal default. Include the policy in the worker’s WorkerConfig; the runnable demo builds it from SMOLBOX_VOLUME_ROOT. SmolBox checks the worker’s returned path against this approved boundary. A mismatch leaves an uncertain record with its reservation held, rather than allowing a mount.

Also verify permissions under your actual worker configuration. This walkthrough uses an unprivileged worker with one host UID. It does not establish replacement writes or tenant isolation for root-run workers that assign different UIDs to VMs. The volume guide covers that boundary and host storage controls.

Write once, then delete the original

These are the two API pieces the demo combines. First, create the volume and confirm it is ready. A returned handle alone is not proof of successful provisioning.

{:ok, volume} = SmolBox.Volumes.create(runtime,
  scope: "team-a", id: "project-data", worker_id: "linux-1", size_gb: 2)

{:ok, %{state: :ready}} = SmolBox.Volumes.inspect(runtime, volume)

Then refer to that volume when creating the machine. The application supplies an approved artifact and resource profile; the caller does not supply a host source path.

{:ok, mount} = SmolBox.VolumeMount.new("project-data", "/mnt/volumes/data")

{:ok, spec} = SmolBox.ManagedMachineSpec.new(
  scope: "team-a", id: "builder-one",
  artifact: approved_artifact, profile: approved_profile, volumes: [mount])

{:ok, machine} = SmolBox.Machines.create(runtime, spec)

These snippets explain the API. The complete demo handles waiting, starting, command submission and observed deletion. After the README setup, run its first phase:

export SMOLBOX_EXECUTION_ID=volume-example
export SMOLBOX_STORE_PARTITION=volume-example
# Replace this with the directory your worker actually uses.
export SMOLBOX_VOLUME_ROOT=/srv/smolvm/.local/share/smolvm/volumes
mix ecto.migrate
mix run -e 'SmolBox.DurableHost.VolumeDemo.run("prepare")'

The original machine writes original to /mnt/volumes/data/kept.txt. The demo checks that deleting an attached volume is blocked, then stops and deletes the machine and verifies its absence.

At this point there are no machines, but still 2 GiB of volume reservation. The directory and file remain on the worker. That is the result we want: deleting compute has not silently discarded the project.

size_gb is an advisory reservation, not a filesystem quota. Host quotas and free space monitoring are separate responsibilities. The demo also reserves each machine’s own disks while it exists, within a 10 GiB worker budget.

Come back with a different machine

The first mix run process has exited. Start a new one with the same environment:

mix run -e 'SmolBox.DurableHost.VolumeDemo.run("resume")'

This controller recovers the volume record from PostgreSQL. It creates a new machine with a new identity, attaches the retained volume, and checks that kept.txt still contains original. It then writes replacement into that file.

The next check matters too. After deleting the replacement, the demo creates a third machine with readonly: true on its VolumeMount. That guest can read replacement, but its attempt to overwrite the file fails.

Point in the walkthroughWhat the check proves
Original deletedThe volume remains unattached, with its reservation held.
New controller, new machineThe same file can be read and modified after the controller restart and machine replacement.
Read-only attachmentThe guest can read the modified file, but cannot overwrite it.
Final cleanupThe machines and volume are deleted; disk and slot reservations are zero.

A read-only mount is still exclusive. SmolBox allows one machine attachment per volume, including stopped machines. It is not a way to share a cache among several concurrent guests. Mounts are fixed at machine creation; there is no hot attach or detach.

Finish the data’s lifetime deliberately

The resume phase deletes its final machine before deleting the volume. In your own application, the volume cleanup call uses the current record version:

{:ok, record} = SmolBox.Volumes.inspect(runtime, volume)
{:ok, result} = SmolBox.Volumes.delete(runtime, volume, record.version)
# Only result.state == :deleted confirms completed cleanup.

This deletion removes the worker directory and its contents. SmolBox requires the worker’s explicit acknowledgment before releasing the reservation. A lost response or unavailable worker is not evidence that the files are gone. Upstream provides no volume lookup endpoint for an independent API check afterward.

Deleted identity records remain for deduplication. Reusing an old ID will not create a fresh, empty volume. Use a new ID and partition for a new demo run; a refresh or restart should never quietly replace your data.

If a phase is interrupted, inspect its records before doing anything else. Do not rerun the entire phase blindly or remove the store to get past an uncertain state. The recovery procedure explains how to preserve evidence, quiesce pending requests and explicitly resolve owned storage.

I ran this exact released demo on physical Linux with PostgreSQL and smolvm 1.22.0. Both phases passed in separate Elixir processes. I also checked the file on the host between phases, verified the volume directory was gone afterward, and stopped the dedicated worker and database. The validation receipt records the checks and the initial setup failure caused by an incorrect volume root. This proves the small walkthrough on that host configuration, not host quotas or isolation across tenants.

What this gives you, and what it does not

A local volume fits a project or cache that should survive replacing its machine on one worker. You can rebuild the environment while keeping the files you chose to retain. Your application owns when that data is finally removed.

It is not a backup. Losing the worker disk can still lose the data. There is no replication or migration to another worker, and a replacement can modify or delete files on a writable mount. Keep backups separately when the data matters.

It is also a different choice from exports, checkpoints and branches. Those mechanisms reuse supported machine state. A volume keeps a separate data location alive. SmolBox 0.4.1 does not combine mounted machines with exports, checkpoint capture or restore, branches, or disk expansion.

The useful question is simple: does this data belong to the machine, or to the project that will outlive it? Put that decision in the lifecycle, then prove it with a file you can afford to delete.

Prove it with one file.

Run prepare and resume in separate Elixir processes. Verify the file survives, then finish with explicit machine and volume cleanup.

Open the volume walkthrough