Fly.io’s object storage is actually just a clever, distributed filesystem built on top of their existing global Anycast network.
Let’s see it in action. Imagine you have a web app deployed on Fly.io and you need to let users upload profile pictures.
First, you’ll need to set up an object storage bucket. This is done via the flyctl CLI:
flyctl object-store create my-app-images --app my-app
This command creates a bucket named my-app-images associated with your application my-app. Fly.io handles the underlying distributed storage infrastructure for you.
Now, in your application code (let’s say it’s a Node.js app using Express), you can interact with this bucket. You’ll need the Fly.io SDK.
const { Storage } = require('@fly/storage');
const storage = new Storage('my-app-images'); // Reference the bucket name
// ... inside an Express route handler for uploads ...
app.post('/upload', async (req, res) => {
const file = req.files.profilePic; // Assuming Multer or similar for file handling
const filename = `users/${req.user.id}/profile.jpg`; // Unique filename
await storage.put(filename, file.data); // Upload the file
res.send(`File uploaded to: ${storage.url(filename)}`);
});
// ... inside an Express route handler for serving images ...
app.get('/images/:userId/profile.jpg', async (req, res) => {
const filename = `users/${req.params.userId}/profile.jpg`;
const file = await storage.get(filename);
if (!file) {
return res.status(404).send('Not found');
}
res.set('Content-Type', file.type);
res.send(file.body);
});
The storage.put(filename, file.data) command uploads the file data to the specified filename within your bucket. storage.get(filename) retrieves it. The storage.url(filename) method generates a publicly accessible URL for the object, which Fly.io’s global network then serves.
The core problem this solves is decoupling file storage from your application servers. Instead of managing disks or external object storage services, you have a built-in, globally distributed solution. When you deploy your app, the object storage configuration travels with it. This means your app can access its storage bucket from any region it’s deployed to, with low latency. The storage itself is sharded and replicated across Fly.io’s infrastructure, providing durability and availability. Your application instances don’t need to know where the data is physically stored; they just interact with the logical bucket.
When you upload a file, Fly.io’s internal systems decide which underlying storage nodes will host replicas of that object. When another instance of your app, potentially in a different continent, needs to retrieve that file, the request is routed through Fly.io’s Anycast network to the nearest available replica, minimizing latency. This is why storage.url() works globally – the URL points to a logical resource, and Fly.io’s network resolves it to the optimal physical location for the requesting client.
The storage.url() method generates a pre-signed URL that is valid for a short period, but you can also configure public access for specific buckets or objects if needed, though direct serving from your app as shown above is often preferred for control. The underlying storage is an eventually consistent distributed system, meaning that immediately after an upload, a read from a different geographic location might not see the latest version. However, this consistency window is typically very small, in the order of seconds.
The next concept to explore is managing object lifecycle and permissions.