1---
2title: "Part 3: Persist Data with a Volume"
3description: Mount a Railway 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](/railway/tutorial/part-2) is stateless.
12Writes to the container's root disk die with the container. In this
13part you mount a Volume so files survive deploys.
14
15## Declare a Volume
16
17A Volume is a standalone resource on the Project. Add it next to
18the Project:
19
20```diff lang="typescript"
21// src/project.ts
22import * as Railway from "alchemy/Railway";
23
24export const Site = Railway.Project("Site");
25+
26+export const Data = Railway.Volume("Data", {
27+ project: Site,
28+ mountPath: "/data",
29+ region: "us-west2",
30+});
31```
32
33The volume is disconnected until a Service mounts it. `mountPath` is
34the path in the container. `region` must match the Service from
35Part 2 — a Volume cannot move, and attaching across regions fails
36the deploy.
37
38## Mount a disk into the Service
39
40Inside the Service's constructor, `Railway.MountVolume(Data, { path })`
41attaches that Volume at deploy time and hands you the path at
42runtime:
43
44```diff lang="typescript"
45// src/api.ts
46import * as Railway from "alchemy/Railway";
47import * as Effect from "effect/Effect";
48import { HttpServerRequest } from "effect/http/HttpServerRequest";
49import * as HttpServerResponse from "effect/http/HttpServerResponse";
50-import { Site } from "./project.ts";
51+import { Data, Site } from "./project.ts";
52
53export default class Api extends Railway.Service<Api>()(
54 "Api",
55 {
56 project: Site,
57 main: import.meta.url,
58 region: "us-west2",
59 port: 3000,
60 },
61 Effect.gen(function* () {
62+ const mount = yield* Railway.MountVolume(Data, { path: "/data" });
63
64 return {
65 fetch: /* ... */,
66 };
67 }),
68) {}
69```
70
71`MountVolume` is a **binding**: at deploy time it tells the Service
72"attach this Volume at `/data`", and at runtime it hands you the
73resolved `mount.path`.
74
75## Provide the binding layer
76
77Bindings declare a capability; layers implement it. Provide
78`MountVolumeLive` on the Service's constructor Effect:
79
80```diff lang="typescript"
81 Effect.gen(function* () {
82 const mount = yield* Railway.MountVolume(Data, { path: "/data" });
83
84 return {
85 fetch: /* ... */,
86 };
87- }),
88+ }).pipe(Effect.provide(Railway.MountVolumeLive)),
89) {}
90```
91
92## Resolve FileSystem in the constructor
93
94Yield `FileSystem` in the outer Effect. Close over it in `fetch`.
95Do not yield it per request.
96
97```diff lang="typescript"
98// src/api.ts
99+import * as FileSystem from "effect/FileSystem";
100// ...
101 Effect.gen(function* () {
102 const mount = yield* Railway.MountVolume(Data, { path: "/data" });
103+ const fs = yield* FileSystem.FileSystem;
104
105 return {
106 fetch: /* ... */,
107 };
108 }).pipe(Effect.provide(Railway.MountVolumeLive)),
109```
110
111## Write files with `PUT /:name`
112
113The Volume is a directory. Use `fs` from the constructor:
114
115```diff lang="typescript"
116fetch: Effect.gen(function* () {
117 const request = yield* HttpServerRequest;
118 const url = new URL(request.url, "http://service");
119 if (url.pathname === "/health") {
120 return HttpServerResponse.json({ ok: true });
121 }
122+ const file = `${mount.path}${url.pathname}`;
123+
124+ if (request.method === "PUT") {
125+ const body = yield* request.text;
126+ yield* fs.writeFileString(file, body).pipe(Effect.orDie);
127+ return HttpServerResponse.empty({ status: 204 });
128+ }
129+
130 return HttpServerResponse.text("Hello from Railway!");
131}),
132```
133
134## Read files with `GET /:name`
135
136Add the read branch, turning a missing file into a 404:
137
138```diff lang="typescript"
139if (request.method === "PUT") {
140 const body = yield* request.text;
141 yield* fs.writeFileString(file, body).pipe(Effect.orDie);
142 return HttpServerResponse.empty({ status: 204 });
143}
144
145+if (request.method === "GET") {
146+ const text = yield* fs.readFileString(file).pipe(
147+ Effect.catch(() => Effect.succeed(undefined)),
148+ );
149+ if (text === undefined) {
150+ return HttpServerResponse.text("Not found", { status: 404 });
151+ }
152+ return HttpServerResponse.text(text);
153+}
154
155return HttpServerResponse.text("Hello from Railway!");
156```
157
158## Yield the Volume from the Stack
159
160```diff lang="typescript"
161// alchemy.run.ts
162-import { Site } from "./src/project.ts";
163+import { Data, Site } from "./src/project.ts";
164
165 Effect.gen(function* () {
166 const site = yield* Site;
167+ yield* Data;
168 const api = yield* Api;
169```
170
171## Deploy
172
173<Tabs syncKey="pkgManager">
174 <TabItem label="bun" icon="bun">
175 <Code code="bun alchemy deploy" lang="sh" />
176 </TabItem>
177 <TabItem label="npm" icon="npm">
178 <Code code="npm run alchemy deploy" lang="sh" />
179 </TabItem>
180 <TabItem label="pnpm" icon="pnpm">
181 <Code code="pnpm alchemy deploy" lang="sh" />
182 </TabItem>
183 <TabItem label="yarn" icon="seti:yarn">
184 <Code code="yarn alchemy deploy" lang="sh" />
185 </TabItem>
186</Tabs>
187
188<Terminal content={`[u]Plan[/u]: [g]1 to create[/g], [y]1 to update[/y]
189
190[g]+[/g] [b]Data[/b] [d](Railway.Volume)[/d]
191[y]~[/y] [b]Api[/b] [d](Railway.Service)[/d]
192
193Proceed?
194◉ Yes ○ No
195[g]✓[/g] [b]Data[/b] [d](Railway.Volume)[/d] [g]created[/g]
196[g]✓[/g] [b]Api[/b] [d](Railway.Service)[/d] [y]updated[/y]`} />
197
198During the Service's deploy, Alchemy attaches the Volume at `/data`.
199
200## Try it out
201
202```sh
203# Store a file on the Volume
204curl -X PUT https://myapp-api-dev-a1b2c3d4.up.railway.app/hello.txt -d 'Hello, Volume!'
205
206# Read it back
207curl https://myapp-api-dev-a1b2c3d4.up.railway.app/hello.txt
208# → Hello, Volume!
209```
210
211Ship another code change and deploy — the file is still there.
212
213## Recap
214
215You now have:
216
217- A Volume in the Project mounted at `/data` on the API Service
218- A Service reading and writing it through Effect's `FileSystem` —
219 no SDK, just files
220- Data that outlives deploys
221
222In [Part 4](/railway/tutorial/part-4), you'll store a Variable on
223the Project and read it from the Service, then tear the stack down.