Persistent volumes
Attach a writable disk to a service that survives redeploys and container restarts — for uploads, SQLite files, a CMS's content directory, anything that has to outlive one container.
What a volume is
A volume is a disk mounted into a service's container at a path you choose. A service can have several, each with its own name and mount path. Everything else in the container is thrown away on each deploy.
There are two kinds, and which one you have decides how its data is protected:
| Kind | Where it lives | Protection |
|---|---|---|
| Block-backed | A Hetzner Cloud block volume attached to the machine that runs the service. | Triple-replicated by Hetzner. Off-site backups are refused — they would add nothing. |
| Host-local | A Docker volume on the machine's own disk. Volumes on your own machine, volumes declared in hoststack.yaml, and volumes created before block storage. | One disk. Turn on nightly off-site backups — they are the only second copy. |
Attach a volume
Add the volume
In the dashboard, open the service, then Settings → Storage and click Add Volume. Give it a name, a mount path and a size. Or do the same from the terminal:
Deploy the service
Nothing is mounted until the next deploy. A block-backed volume starts as
pending; the deploy creates the disk, attaches it, and the volume turnsactive. After every deploy, HostStack checks inside the container that each configured volume is really mounted — a deploy fails rather than starting your app without its disk.
hoststack volumes create svc_... data /var/data --size 10
# It will mount on the next deploy.
hoststack deploy trigger svc_...import { HostStack } from '@hoststack.dev/sdk';
const hs = new HostStack({ apiKey: process.env.HOSTSTACK_API_KEY! });
const teamId = 1; // "Team ID" in the output of: hoststack whoami
const { volume } = await hs.volumes.create(teamId, 'svc_...', {
name: 'data',
mountPath: '/var/data',
sizeGb: 10,
});
// volume.status is 'pending' for a block-backed volume until the next deploy.Attach a 10 GB volume called "data" at /var/data to the
service svc_..., then redeploy it.services:
api:
type: web_service
disk:
name: data
mountPath: /var/data
sizeGB: 10Creating a volume needs a payment method on the team, because volumes are billed by size — see Billing.
Volume settings
namestringrequiredLowercase letters, digits and hyphens, up to 64 characters. It becomes part of the Docker volume's name on the host, so renaming means a different disk — pick it once.
mountPath stringrequiredThe absolute path inside the container, up to 500 characters. Any path your app owns works —
/var/lib/mysql,/var/www/html,/app/data. It may not contain..or//.Refused:
/tmpand/var/tmp, which are tmpfs and would hide the volume (writes would go to RAM and vanish on restart), and/,/proc,/sys,/devand/etc, which belong to the container runtime.sizeGb number10 to 10,240 GB (10 TB). 10 GB is Hetzner's minimum block volume size. The MCP
create_volumetool accepts at most 100 GB; use the dashboard, CLI or SDK for more.Default:
10
Declaring a disk in hoststack.yaml
A disk: block (one per service, using Render's field names, including the capital-GB sizeGB) is applied on every deploy and shows up in Settings → Storage afterwards. It behaves differently from a volume added through the dashboard or API:
- It is always a host-local Docker volume, never a block volume, so turn its backups on.
sizeGBdefaults to1. It is recorded on the volume and billed; a host-local volume has no fixed size.- A yaml disk replaces any other volume at the same
mountPath. - Renaming it mounts a new, empty disk. The old one is kept until you delete it.
See Infrastructure as code for the rest of the file.
Grow, change or delete a volume
hoststack volumes list svc_...
hoststack volumes resize svc_... vol_... 20 # grow to 20 GB
hoststack volumes delete svc_... vol_...const { volumes } = await hs.volumes.list(teamId, 'svc_...');
await hs.volumes.update(teamId, 'svc_...', 'vol_...', { sizeGb: 20 });
await hs.volumes.delete(teamId, 'svc_...', 'vol_...');Grow the "data" volume on svc_... to 20 GB.GrowVolumes only grow — a smaller size is rejected, because a filesystem cannot shrink safely under live data. A block volume is enlarged at Hetzner and its filesystem extended on the machine straight away.
- Move the mount
Change
mountPathin the dashboard's edit dialog, the SDK'supdateor MCP'supdate_volume. It takes effect on the next deploy.DeleteThe volume is marked
deletingand the machine is told to remove the disk. If the machine is offline, HostStack retries every 5 minutes until it confirms. Deleting destroys the data and the volume's restore points with it.
Backups and restore
Off-site backups are for host-local volumes. They are off by default — except on a volume on your own machine, where they are on from the moment it is created: that disk is the only copy there is. On a block-backed volume the toggle is refused.
hoststack volumes backup svc_... vol_... on # nightly backups
hoststack volumes backups svc_... vol_... # list restore points
hoststack volumes restore svc_... vol_... 41 --yesawait hs.volumes.update(teamId, 'svc_...', 'vol_...', { backupEnabled: true });
const { restorePoints } = await hs.volumes.listRestorePoints(teamId, 'svc_...', 'vol_...');
await hs.volumes.restore(teamId, 'svc_...', 'vol_...', { backupId: restorePoints[0]!.id });Turn on backups for the "data" volume on svc_..., and show me
its restore points.- Nightly backup
At 02:00 UTC each opted-in volume is archived as a
.tar.gzand uploaded to off-site object storage. A machine that was off at 02:00 catches up when it reconnects.- Restore points
The 7 most recent uploaded archives, newest first. Turning backups on schedules them — it does not take one — so check that a restore point exists before you rely on it. Turning backups off keeps the archives you already have.
RestoreUnpacks the archive over the volume's current contents: files in the archive overwrite what is there, and files that are not in it stay. Nothing is snapshotted first, so there is no undo. The service keeps running during a restore — suspend it first if something writes to the volume.
ImportIn Settings → Storage, the restore dialog also takes a
.tar.gzyou upload, with paths relative to the mount point. The browser uploads straight to object storage, so the limit is S3's single-upload ceiling: 5 GB per archive.
A restore or import is refused while the volume is not active, while another restore is still running (two archives unpacking into one directory produce a tree that is neither), or while the volume is being cloned or moved. Restoring requires the admin role.
Move a volume to block storage
A host-local volume on a HostStack machine can be moved onto a block volume in place: in Settings → Storage, choose Migrate to durable storage. The service must have been deployed first, so HostStack knows which machine holds the data. The block volume is created, the data copied, and the volume only switches to block-backed once the copy is confirmed. Volumes on your own machine stay host-local — a Hetzner block volume can only attach to a Hetzner server.
Billing
Volumes cost €0.10 per GB-month of their configured size. Usage is measured once a day in GB-days, so a 10 GB volume kept for a 30-day month costs €1.00, and resizing or deleting shows up on the next day's measurement.
- A volume marked
deletingis no longer counted, even before the disk has been removed. - A block-backed volume is not billed until its disk has actually been created — a volume you added but never deployed costs nothing.
See Billing for how usage appears on the invoice.
Troubleshooting
- Files vanish after a restart
The app is writing to
/tmpor another path outside the volume. Only the volume'smountPathsurvives a restart or deploy; check where the app actually writes.- Volume stays pending
A block-backed volume becomes
activeduring the next deploy. Deploy the service; if it is still pending afterwards, the volume shows the provisioning error in Settings → Storage.- Deploy fails on volume mount verification
A configured volume was not mounted in the new container, so the deploy stopped rather than run the app without its data. The deploy log names the missing path.
- Permission denied or read-only file system
Containers run with a read-only root filesystem and without the capability that lets root ignore file ownership. See Container runtime for what that means for your image.
hoststack volumes list svc_...Next steps
- Use a managed databasePostgres, MySQL and more with nightly dumps, instead of a database on a volume.
- Run on your own machineHow volumes, backups and linking work on hardware you enrol.
- Declare it in hoststack.yamlService shape, env, scaling and a disk in one file.
- Understand billingHow metered storage, bandwidth and build minutes are charged.
Was this page helpful?