Skip to content
HostStack Docs

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.

Container rootfs is read-only. Apps that just need scratch space (uploads, temp caches, mkdir on cold start) should target /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.

yaml
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

bash
# 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

typescript
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.

Lifecycle & reliability

  • Attach creates the disk on the host on the next deploy. A durable volume is created pending and only becomes active once 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). /tmp and /var/tmp are 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, /dev and /etc are 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 deleting and 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 — back up data first.
  • Multiple agents — a service is pinned to one host, so its volume lives on that host. Migrating between hosts is a manual restore from snapshot.

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.

Essential cookies only — for login sessions. No tracking. Details