Part 3: Persist Data with a Volume
Your Service from Part 2 is stateless. Writes to the container’s root disk die with the container. In this part you mount a Volume so files survive deploys.
Declare a Volume
Section titled “Declare a Volume”A Volume is a standalone resource on the Project. Add it next to the Project:
import * as Railway from "alchemy/Railway";
export const Site = Railway.Project("Site");
export const Data = Railway.Volume("Data", { project: Site, mountPath: "/data",});The volume is disconnected until a Service mounts it. mountPath is
the path in the container.
Mount a disk into the Service
Section titled “Mount a disk into the Service”Inside the Service’s init, Railway.MountVolume(Data, { path })
attaches that Volume at deploy time and hands you the path at
runtime:
import * as Railway from "alchemy/Railway";import * as Effect from "effect/Effect";import { HttpServerRequest } from "effect/unstable/http/HttpServerRequest";import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse";import { Site } from "./project.ts";import { Data, Site } from "./project.ts";
export default class Api extends Railway.Service<Api>()( "Api", { project: Site, main: import.meta.url, region: "us-west2", port: 3000, }, Effect.gen(function* () { const mount = yield* Railway.MountVolume(Data, { path: "/data" });
return { fetch: /* ... */, }; }),) {}MountVolume is a binding: at deploy time it tells the Service
“attach this Volume at /data”, and at runtime it hands you the
resolved mount.path.
Provide the binding layer
Section titled “Provide the binding layer”Bindings declare a capability; layers implement it. Provide
MountVolumeLive on the Service’s init Effect:
Effect.gen(function* () { const mount = yield* Railway.MountVolume(Data, { path: "/data" });
return { fetch: /* ... */, }; }), }).pipe(Effect.provide(Railway.MountVolumeLive)),) {}Resolve FileSystem in init
Section titled “Resolve FileSystem in init”Yield FileSystem in the outer Effect. Close over it in fetch.
Do not yield it per request.
import * as FileSystem from "effect/FileSystem";// ... Effect.gen(function* () { const mount = yield* Railway.MountVolume(Data, { path: "/data" }); const fs = yield* FileSystem.FileSystem;
return { fetch: /* ... */, }; }).pipe(Effect.provide(Railway.MountVolumeLive)),Write files with PUT /:name
Section titled “Write files with PUT /:name”The Volume is a directory. Use fs from init:
fetch: Effect.gen(function* () { const request = yield* HttpServerRequest; const url = new URL(request.url, "http://service"); if (url.pathname === "/health") { return HttpServerResponse.json({ ok: true }); } const file = `${mount.path}${url.pathname}`;
if (request.method === "PUT") { const body = yield* request.text; yield* fs.writeFileString(file, body).pipe(Effect.orDie); return HttpServerResponse.empty({ status: 204 }); }
return HttpServerResponse.text("Hello from Railway!");}),Read files with GET /:name
Section titled “Read files with GET /:name”Add the read branch, turning a missing file into a 404:
if (request.method === "PUT") { const body = yield* request.text; yield* fs.writeFileString(file, body).pipe(Effect.orDie); return HttpServerResponse.empty({ status: 204 });}
if (request.method === "GET") { const text = yield* fs.readFileString(file).pipe( Effect.catchAll(() => Effect.succeed(undefined)), ); if (text === undefined) { return HttpServerResponse.text("Not found", { status: 404 }); } return HttpServerResponse.text(text);}
return HttpServerResponse.text("Hello from Railway!");Yield the Volume from the Stack
Section titled “Yield the Volume from the Stack”import { Site } from "./src/project.ts";import { Data, Site } from "./src/project.ts";
Effect.gen(function* () { const site = yield* Site; yield* Data; const api = yield* Api;Deploy
Section titled “Deploy”bun alchemy deploynpm run alchemy deploypnpm alchemy deployyarn alchemy deployPlan: 1 to create, 1 to update + Data (Railway.Volume) ~ Api (Railway.Service) Proceed? ◉ Yes ○ No ✓ Data (Railway.Volume) created ✓ Api (Railway.Service) updated
During the Service’s deploy, Alchemy attaches the Volume at /data.
Try it out
Section titled “Try it out”# Store a file on the Volumecurl -X PUT https://myapp-api-dev-a1b2c3d4.up.railway.app/hello.txt -d 'Hello, Volume!'
# Read it backcurl https://myapp-api-dev-a1b2c3d4.up.railway.app/hello.txt# → Hello, Volume!Ship another code change and deploy — the file is still there.
You now have:
- A Volume in the Project mounted at
/dataon the API Service - A Service reading and writing it through Effect’s
FileSystem— no SDK, just files - Data that outlives deploys
In Part 4, you’ll store a Variable on the Project and read it from the Service, then tear the stack down.