| 1 | --- |
| 2 | title: Volumes |
| 3 | description: Durable network block storage for Hetzner Servers — attach with props or VolumeAttachment, mount into Services with the MountVolume binding. |
| 4 | --- |
| 5 | |
| 6 | A [`Volume`](/providers/hetzner/volume) is Hetzner's network block |
| 7 | storage: a durable disk (10 GB minimum) with its own lifecycle, |
| 8 | attachable to one Server at a time in the same location. Servers |
| 9 | come and go; Volumes keep the data. |
| 10 | |
| 11 | ## Create a Volume |
| 12 | |
| 13 | ```typescript |
| 14 | import * as Hetzner from "alchemy/Hetzner"; |
| 15 | |
| 16 | export const Data = Hetzner.Volume("data", { |
| 17 | size: 10, |
| 18 | format: "ext4", |
| 19 | location: "nbg1", |
| 20 | }); |
| 21 | ``` |
| 22 | |
| 23 | `size` is in GB and can **grow in place** on a later deploy — |
| 24 | shrinking is impossible. `format` (`ext4` or `xfs`) and `location` |
| 25 | are create-only: changing either **replaces the Volume and destroys |
| 26 | its data**. The location must match the Server's. |
| 27 | |
| 28 | ## Mount into a Service (recommended) |
| 29 | |
| 30 | The `MountVolume` binding is the highest-level path: inside a |
| 31 | [Service](/hetzner/compute/services)'s constructor, bind the Volume to a |
| 32 | path and use it as a plain directory at runtime. Pass the `Data` |
| 33 | declaration directly — no yielding required: |
| 34 | |
| 35 | ```typescript |
| 36 | export default class Api extends Hetzner.Service<Api>()( |
| 37 | "Api", |
| 38 | { server: Box, main: import.meta.url, port: 3000 }, |
| 39 | Effect.gen(function* () { |
| 40 | const mount = yield* Hetzner.MountVolume(Data, { path: "/data" }); |
| 41 | |
| 42 | return { |
| 43 | fetch: Effect.gen(function* () { |
| 44 | const fs = yield* FileSystem.FileSystem; |
| 45 | const text = yield* fs.readFileString(`${mount.path}/hello.txt`); |
| 46 | // ... |
| 47 | }), |
| 48 | }; |
| 49 | }).pipe(Effect.provide(Hetzner.MountVolumeLive)), |
| 50 | ) {} |
| 51 | ``` |
| 52 | |
| 53 | At deploy time the binding attaches the Volume to the Service's |
| 54 | Server, creates the directory, mounts the device, and appends an |
| 55 | `/etc/fstab` entry (`defaults,nofail`) so the mount survives |
| 56 | reboots. At runtime it hands you the resolved `mount.path` and |
| 57 | `mount.device`. Every step is idempotent, and two Services binding |
| 58 | the same `(volume, path)` on the same Server share one attach and |
| 59 | one mount. |
| 60 | |
| 61 | If the Server is ever replaced, the Volume is detached from the old |
| 62 | machine and re-attached to the new one on the next deploy — the |
| 63 | data rides along. |
| 64 | |
| 65 | ## Attach without a Service |
| 66 | |
| 67 | For servers that manage their own filesystems, attach declaratively |
| 68 | instead. Either from the Volume side: |
| 69 | |
| 70 | ```typescript |
| 71 | export const Data = Hetzner.Volume("data", { |
| 72 | size: 10, |
| 73 | location: "nbg1", |
| 74 | server: Box, |
| 75 | automount: true, |
| 76 | }); |
| 77 | ``` |
| 78 | |
| 79 | Or as a standalone |
| 80 | [`VolumeAttachment`](/providers/hetzner/volumeattachment) — useful |
| 81 | when the Volume and Server are declared far apart: |
| 82 | |
| 83 | ```typescript |
| 84 | yield* Hetzner.VolumeAttachment("data-on-box", { |
| 85 | volume: Data, |
| 86 | server: Box, |
| 87 | automount: true, |
| 88 | }); |
| 89 | ``` |
| 90 | |
| 91 | With `automount: true` Hetzner mounts the disk under |
| 92 | `/mnt/HC_Volume_<id>`; otherwise mount `volume.linuxDevice` |
| 93 | yourself (e.g. via cloud-init or `Hetzner.Ssh`). |
| 94 | |
| 95 | ## Where next |
| 96 | |
| 97 | - [Tutorial part 3](/hetzner/tutorial/part-3) — mount a Volume into |
| 98 | a Service step by step. |
| 99 | - [Services](/hetzner/compute/services) — the platform doing the |
| 100 | mounting. |
| 101 | - [`Volume` reference](/providers/hetzner/volume) — every prop and |
| 102 | attribute. |