Persistent Volumes
Attach a writable disk to your service that survives redeploys and container restarts. Use it for uploads, caches, SQLite databases — anything that needs to outlive a single container.
/tmp — already mounted as tmpfs in every container. The platform exports HOSTSTACK_TMP_DIR=/tmp so portable apps can write to $HOSTSTACK_TMP_DIR/.... Use volumes for data that must persist.Declare in hoststack.yaml (recommended)
Infrastructure-as-Code: every deploy reconciles the declared disk into the service. The dashboard reflects what your yaml says.
services:
api:
type: web_service
start:
command: bun apps/api/src/index.ts
disk:
name: data
mountPath: /var/data
sizeGB: 10- name — short identifier (lowercase, hyphens). Treated like a docker volume name; changing it after data is written will detach the old disk.
- mountPath — absolute in-container path the disk mounts at.
- sizeGB — 10–10240 (10 GB–10 TB), default 10. Disk capacity in gibibytes; Hetzner block volumes have a hard 10 GB minimum. Billed at €0.10/GB·month for extra storage beyond the service-included disk.
Dashboard
Open the service → Volumes tab. You can attach, resize (grow only), and delete volumes there. Disks declared in hoststack.yaml show up automatically after the next deploy.
CLI
# List volumes attached to a service
hoststack volumes list <service-id>
# Attach a new volume
hoststack volumes create <service-id> data /var/data --size 10
# Grow a volume (cannot shrink)
hoststack volumes resize <service-id> <volume-id> 20
# Detach and deprovision
hoststack volumes delete <service-id> <volume-id>SDK
import { HostStack } from '@hoststack.dev/sdk';
const client = new HostStack({ apiKey: process.env.HOSTSTACK_API_KEY! });
await client.volumes.create('team_…', 'svc_…', {
name: 'data',
mountPath: '/var/data',
sizeGb: 10,
});
const { volumes } = await client.volumes.list('team_…', 'svc_…');
for (const v of volumes) {
console.log(v.name, v.mountPath, v.sizeGb, v.status);
}MCP
Available tools: list_volumes, create_volume, update_volume, delete_volume. They all work with service publicIds (svc_…) and volume publicIds (vol_…) — no need to look up numeric IDs first.
Backups, restore points and imports
Open the service → Volumes tab. Each volume has a restore action (roll back, or bring an archive in) and, for host-local volumes, a backup toggle.
- Off-site backups are opt-in per volume and off by default — block-backed volumes are already triple-replicated by Hetzner, so the toggle is refused on them. Switch it on and the volume is tarred and uploaded nightly at 02:00 UTC. A volume on your own machine has backups on from the moment it is created: that disk is one consumer SSD, and the backup is the only second copy there is.
- Restore points — the last 7 uploaded archives are kept and listed newest-first. A volume that has never had backups enabled has none, which is the honest answer rather than an empty promise: turn the toggle on before you need it.
- Restoring unpacks the archive over the volume's current contents. Files in the archive win; files not in it survive. That is a restore, not a reset — and it is refused while another restore, a clone or a move to another machine is still in flight, because two tars extracting into one directory produce a tree that is neither archive.
- Importing — upload a
.tar.gzof the directory and it is unpacked into the volume the same way. The browser PUTs straight to object storage rather than through the API, so the ceiling is the S3 single-PUT one: 5 GB per archive. - Migrate to durable storage — a legacy host-local volume can be moved onto a Hetzner block volume in place. Deploy the service first so its worker is known, then trigger the migration; the disk is created, the data copied, and the volume flips to block-backed only once the copy is acknowledged.
These are dashboard and REST-API operations. There is no hoststack volumes restore and no MCP tool for it — the CLI and MCP surfaces listed above are the complete ones.
Lifecycle & reliability
- Attach creates the disk on the host on the next deploy. A durable volume is created
pendingand only becomesactiveonce the disk actually exists and is attached — until you deploy, there is nothing mounted. Data then persists across redeploys, restarts, and service suspends. - Mount path — any application-owned absolute path works (
/var/lib/mysql,/var/www/html,/app/data)./tmpand/var/tmpare rejected: they are already tmpfs inside the container and would shadow the volume, so writes would go to RAM and vanish on restart./,/proc,/sys,/devand/etcare rejected as runtime-owned. Every configured mount is verified inside the container after it starts — a deploy fails rather than running an app without its disk. - Resize only supports growing. Filesystems can't safely shrink under live data.
- Delete marks the row
deletingand dispatches deprovision to the host agent. If the agent is offline, a 5-minute reaper retries until it acks. The disk is destroyed once the agent confirms, and deleting the volume deletes its restore points with it — take one last backup first if the data matters. - Multiple agents — a service is pinned to one host, so its volume lives on that host. Moving between hosts is a restore: back the volume up, create the volume on the new service, and restore the archive into it from the Volumes tab.
Billing
Storage is metered per GB-month. The control plane sums the sizeGb of every active volume across your team once a day and reports it to Stripe under volume_storage_gb. Your invoice line-items show the GB-month total. Volumes marked deleting stop billing immediately, even before the host agent finishes the deprovision.