Data

Each app gets its own directories, mounted into its container under /data/. Apps see the same path layout no matter what they have access to (only the directories they were granted are actually mounted), so the structure never changes when permissions do.

The three tiers

TierIn the containerBackingBacked upFor
Permanent/data/app_data/<app>Local diskYesSQLite databases, notes, config, small assets
Temporary/data/app_temp_data/<app>Local diskNot guaranteedThumbnails, transcodes, build artifacts, anything recreatable
Archive/data/app_archive/<app>JuiceFS, local or S3See belowBulk content: photos, video, attachments, model weights

Apps get permanent data by default and request the other two in their manifest (app_temp_data, app_archive). They should read the paths from BOTTLE_APP_DATA_DIR, BOTTLE_APP_TEMP_DIR and BOTTLE_APP_ARCHIVE_DIR rather than hardcoding them. See Creating an App for the app author's view.

The split between permanent and archive matters more than it looks. Permanent data is local disk with real fsync and strict POSIX semantics, which is what an embedded database needs: SQLite, LMDB, RocksDB and friends belong there and nowhere else. The archive tier is a network-shaped filesystem: fine for whole files, wrong for a write-ahead log or for fcntl locks used for correctness. An app that stores bulk content normally uses both, keeping its index in permanent data and the bytes in the archive.

An app can also request access_all_app_data, which mounts every app's directories read-write. This is for file browsers, backup tools and the like.

The archive tier

The archive is always a JuiceFS volume, so an app that asks for it installs anywhere. Only the object storage underneath differs:

  • Local (default): objects live on the instance's own disk. Nothing to configure, but there is no copy anywhere else, and the bundled backup app skips the archive tier.
  • S3: objects live in a bucket you supply, configured in the dashboard. Elastic and durable, at the cost of tens to hundreds of milliseconds on an uncached first read. Backups skip it, since the bytes already live in the bucket.

Switching from local to S3, or from one bucket to another, is done from the dashboard behind a confirmation. The objects are copied and verified, then the same volume is re-pointed at the new store; the metadata database is untouched, so every file, permission and owner is preserved. It fails open: if anything goes wrong before the switch commits, the volume keeps reading from the store it was already using.

Where it lives on disk

Everything sits under the instance's data directory (data_root_dir in config.toml, normally ~/.openhost/local_compute_space/):

PathContents
persistent_data/app_data/<app>/Permanent app data
persistent_data/app_archive_local_objects/JuiceFS objects, on the local backend only
persistent_data/openhost/Router database, TLS certificates and keys
temporary_data/app_temp_data/<app>/Temporary app data, plus that app's build and container logs
app_archive/The JuiceFS mount

The bundled backup app copies the app data under persistent_data/, which is the point of the split. It does not copy persistent_data/openhost/: router state is never mounted into any container, so no app can see it, and reaching it means SSH or the terminal in the dashboard.

Storage guard

Running a disk to zero on a machine that hosts your own data is worse than stopping early, so the instance reserves headroom. When free space drops below storage_min_free_mb (500 MB by default) the storage guard stops running apps until space is freed. Change the threshold in config.toml (or set it to 0 to switch the guard off), then restart.

This page lives in the Cloud in a Bottle repo. Suggest an edit. Prefer the raw markdown? Add .md to the URL, or grab the whole manual at all.md.