1---
2title: "Part 3: Persist Data with a Volume"
3description: Mount a Fly disk into your Service with MountVolume and store files that survive deploys.
4sidebar:
5 order: 3
6---
7
8import Terminal from "../../../../components/Terminal.astro";
9import { Code, Tabs, TabItem } from "@astrojs/starlight/components";
10
11Your Service from [Part 2](/fly/tutorial/part-2) is stateless.
12Writes to the Machine's root disk die with the Machine. In this
13part you mount a Volume so files survive deploys.
14
15:::caution[One Volume, one Machine]
16A Fly Volume attaches to one Machine. Two Services cannot mount
17the same disk. `count: 3` creates three independent Volumes, one
18per replica.
19:::
20
21## Mount a disk into the Service
22
23There is no standalone Volume resource. Inside the Service's constructor,
24`Fly.MountVolume({ path, sizeGb })` creates a per-replica disk in
25the Service's App and region:
26
27```diff lang="typescript"
28// src/api.ts
29import * as Fly from "alchemy/Fly";
30import * as Effect from "effect/Effect";
31import { HttpServerRequest } from "effect/http/HttpServerRequest";
32import * as HttpServerResponse from "effect/http/HttpServerResponse";
33
34export default class Api extends Fly.Service<Api>()(
35 "Api",
36 { main: import.meta.url, region: "iad", port: 3000 },
37 Effect.gen(function* () {
38+ const mount = yield* Fly.MountVolume({ path: "/data", sizeGb: 1 });
39
40 return {
41 fetch: /* ... */,
42 };
43 }),
44) {}
45```
46
47`MountVolume` is a **binding**: at deploy time it tells the Service
48"create a 1 GB Volume and put it in `config.mounts` at `/data`", and
49at runtime it hands you the resolved `mount.path`.
50
51## Provide the binding layer
52
53Bindings declare a capability; layers implement it. Provide
54`MountVolumeLive` on the Service's constructor Effect:
55
56```diff lang="typescript"
57 Effect.gen(function* () {
58 const mount = yield* Fly.MountVolume({ path: "/data", sizeGb: 1 });
59
60 return {
61 fetch: /* ... */,
62 };
63- }),
64+ }).pipe(Effect.provide(Fly.MountVolumeLive)),
65) {}
66```
67
68## Resolve FileSystem in the constructor
69
70Yield `FileSystem` in the outer Effect. Close over it in `fetch`.
71Do not yield it per request.
72
73```diff lang="typescript"
74// src/api.ts
75+import * as FileSystem from "effect/FileSystem";
76// ...
77 Effect.gen(function* () {
78 const mount = yield* Fly.MountVolume({ path: "/data", sizeGb: 1 });
79+ const fs = yield* FileSystem.FileSystem;
80
81 return {
82 fetch: /* ... */,
83 };
84 }).pipe(Effect.provide(Fly.MountVolumeLive)),
85```
86
87## Write files with `PUT /:name`
88
89The Volume is a directory. Use `fs` from the constructor:
90
91```diff lang="typescript"
92fetch: Effect.gen(function* () {
93 const request = yield* HttpServerRequest;
94 const url = new URL(request.url, "http://service");
95 if (url.pathname === "/health") {
96 return HttpServerResponse.json({ ok: true });
97 }
98+ const file = `${mount.path}${url.pathname}`;
99+
100+ if (request.method === "PUT") {
101+ const body = yield* request.text;
102+ yield* fs.writeFileString(file, body).pipe(Effect.orDie);
103+ return HttpServerResponse.empty({ status: 204 });
104+ }
105+
106 return HttpServerResponse.text("Hello from Fly!");
107}),
108```
109
110## Read files with `GET /:name`
111
112Add the read branch, turning a missing file into a 404:
113
114```diff lang="typescript"
115if (request.method === "PUT") {
116 const body = yield* request.text;
117 yield* fs.writeFileString(file, body).pipe(Effect.orDie);
118 return HttpServerResponse.empty({ status: 204 });
119}
120
121+if (request.method === "GET") {
122+ const text = yield* fs.readFileString(file).pipe(
123+ Effect.catch(() => Effect.succeed(undefined)),
124+ );
125+ if (text === undefined) {
126+ return HttpServerResponse.text("Not found", { status: 404 });
127+ }
128+ return HttpServerResponse.text(text);
129+}
130
131return HttpServerResponse.text("Hello from Fly!");
132```
133
134## Deploy
135
136<Tabs syncKey="pkgManager">
137 <TabItem label="bun" icon="bun">
138 <Code code="bun alchemy deploy" lang="sh" />
139 </TabItem>
140 <TabItem label="npm" icon="npm">
141 <Code code="npm run alchemy deploy" lang="sh" />
142 </TabItem>
143 <TabItem label="pnpm" icon="pnpm">
144 <Code code="pnpm alchemy deploy" lang="sh" />
145 </TabItem>
146 <TabItem label="yarn" icon="seti:yarn">
147 <Code code="yarn alchemy deploy" lang="sh" />
148 </TabItem>
149</Tabs>
150
151<Terminal content={`[u]Plan[/u]: [y]1 to update[/y]
152
153[y]~[/y] [b]Api[/b] [d](Fly.Service)[/d]
154
155Proceed?
156◉ Yes ○ No
157[g]✓[/g] [b]Api[/b] [d](Fly.Service)[/d] [y]updated[/y]`} />
158
159During the Service's deploy, Alchemy creates the Volume and writes
160it into the Machine's `config.mounts` at `/data`. The disk lives in
161the Service's region (`iad`).
162
163## Try it out
164
165```sh
166# Store a file on the Volume
167curl -X PUT https://myapp-api-dev-a1b2c3d4.fly.dev/hello.txt -d 'Hello, Volume!'
168
169# Read it back
170curl https://myapp-api-dev-a1b2c3d4.fly.dev/hello.txt
171# → Hello, Volume!
172```
173
174Ship another code change and deploy — the file is still there. If
175the Machine is replaced, the Volume re-attaches on the next deploy.
176
177## Recap
178
179You now have:
180
181- A 1 GB Volume in `iad` mounted at `/data` on the API Machine
182- A Service reading and writing it through Effect's `FileSystem` —
183 no SDK, just files
184- Data that outlives deploys and Machine replacement
185
186In [Part 4](/fly/tutorial/part-4), you'll read a secret from `.env`
187in the Service, then tear the stack down.