Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions examples/fuse-on-r2/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# syntax=docker/dockerfile:1

ARG TIGRISFS_VERSION=v1.2.1

FROM golang:1.24-alpine AS build

WORKDIR /app

COPY container_src/go.mod ./
RUN go mod download

COPY container_src/*.go ./

RUN CGO_ENABLED=0 GOOS=linux go build -o /server

FROM alpine:3.20

ARG TIGRISFS_VERSION

RUN apk add --no-cache ca-certificates fuse fuse-dev curl bash

RUN set -e; \
ARCH=$(uname -m); \
case "$ARCH" in \
x86_64) ARCH="amd64" ;; \
aarch64) ARCH="arm64" ;; \
esac; \
curl -fL "https://github.com/tigrisdata/tigrisfs/releases/download/${TIGRISFS_VERSION}/tigrisfs_${TIGRISFS_VERSION#v}_linux_${ARCH}.tar.gz" | \
tar -xzf - -C /usr/local/bin/

COPY --from=build /server /server

COPY container_src/startup.sh /startup.sh
RUN chmod +x /startup.sh

EXPOSE 8080

CMD ["/startup.sh"]
38 changes: 38 additions & 0 deletions examples/fuse-on-r2/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Cloudflare Containers + R2-backed FUSE mounts

**Refer to the [public documentation](https://developers.cloudflare.com/r2/examples/fuse-on-r2/) for more information**

This is a demo app that shows how to mount an R2 bucket into a Cloudflare Container!

1. A Worker as the front-end that proxies to a single container instance
2. A container with an R2 bucket mounted using [tigrisfs](https://github.com/tigrisdata/tigrisfs) at `$HOME/mnt/r2/<bucket_name`>
3. A Go application that uses `io/fs` to list files in the mounted R2 bucket and return them as JSON

Mounting object storage buckets as FUSE mounts allows applications to interact with the bucket as if it were a local filesystem: useful if you have apps that don't have native support for object storage (many!) and/or want to simplify operations.

The trade-off is that object storage is not exactly a POSIX compatible filesystem, nor is it local, and so you should not expect native, SSD-like performance. For many apps, this doesn't matter: reading a bunch of shared assets, bootstrapping a agent/sandbox, or providing a way to persist user-state are all common use cases and rarely I/O intensive.

## Deploying it

You'll need to provide your [R2 API credentials](https://developers.cloudflare.com/r2/api/tokens/) and Cloudflare account ID to the container.

1. Update `wrangler.jsonc` with the `BUCKET_NAME` and `ACCOUNT_ID` environment variables. These are OK to be public.
2. Use `npx wrangler@latest secret put AWS_ACCESS_KEY_ID` and `npx wrangler@latest secret put AWS_SECRET_ACCESS_KEY` to set your R2 credentials.
3. Ensure Docker is running locally.
4. `npx wrangler@latest deploy`

You can mount multiple buckets as you wish by updating the Dockerfile or doing it dynamically from within the application in your container.

To mount a bucket at a specific prefix, set the `R2_BUCKET_PREFIX` environment variable in `wrangler.json` or dynamically when creating a container instance.

## Learn More

To learn more about Containers, take a look at the following resources:

- [Container Documentation](https://developers.cloudflare.com/containers/) - learn about Containers
- [Container Class](https://github.com/cloudflare/containers) - learn about the Container helper class
- Learn more about Container [lifecycles](https://developers.cloudflare.com/containers/platform-details/architecture/)

## License

Apache-2.0 licensed. Copyright 2025, Cloudflare, Inc.
3 changes: 3 additions & 0 deletions examples/fuse-on-r2/container_src/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module server

go 1.24.3
114 changes: 114 additions & 0 deletions examples/fuse-on-r2/container_src/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package main

import (
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"path/filepath"
"syscall"
"time"
)

type FileInfo struct {
Name string `json:"name"`
IsDir bool `json:"is_dir"`
Size int64 `json:"size"`
}

type FileListResponse struct {
BucketName string `json:"bucket_name"`
MountPath string `json:"mount_path"`
Files []FileInfo `json:"files"`
Total int `json:"total"`
}

func listFilesHandler(w http.ResponseWriter, r *http.Request) {
bucketName := os.Getenv("BUCKET_NAME")
if bucketName == "" {
http.Error(w, "BUCKET_NAME environment variable not set", http.StatusInternalServerError)
return
}

home, err := os.UserHomeDir()
if err != nil {
http.Error(w, fmt.Sprintf("Failed to get home directory: %v", err), http.StatusInternalServerError)
return
}

mountPath := filepath.Join(home, "mnt", "r2", bucketName)

entries, err := os.ReadDir(mountPath)
if err != nil {
http.Error(w, fmt.Sprintf("Failed to read directory %s: %v", mountPath, err), http.StatusInternalServerError)
return
}

files := make([]FileInfo, 0, 10)
for i, entry := range entries {
if i >= 10 {
break
}

info, err := entry.Info()
if err != nil {
log.Printf("Warning: could not get info for %s: %v", entry.Name(), err)
continue
}

files = append(files, FileInfo{
Name: entry.Name(),
IsDir: entry.IsDir(),
Size: info.Size(),
})
}

response := FileListResponse{
BucketName: bucketName,
MountPath: mountPath,
Files: files,
Total: len(entries),
}

w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(response); err != nil {
log.Printf("Failed to encode JSON: %v", err)
http.Error(w, "Failed to encode response", http.StatusInternalServerError)
return
}
}

func main() {
stop := make(chan os.Signal, 1)
signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)

router := http.NewServeMux()
router.HandleFunc("/", listFilesHandler)

server := &http.Server{
Addr: ":8080",
Handler: router,
}

go func() {
log.Printf("Server listening on %s\n", server.Addr)
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatal(err)
}
}()

sig := <-stop
log.Printf("Received signal (%s), shutting down server...", sig)

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

if err := server.Shutdown(ctx); err != nil {
log.Fatal(err)
}

log.Println("Server shutdown successfully")
}
12 changes: 12 additions & 0 deletions examples/fuse-on-r2/container_src/startup.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#!/bin/sh
set -e

mkdir -p "$HOME/mnt/r2/${BUCKET_NAME}"

R2_ENDPOINT="https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.com"
echo "Mounting bucket ${BUCKET_NAME}..."
/usr/local/bin/tigrisfs --endpoint "${R2_ENDPOINT}" -f "${BUCKET_NAME}" "$HOME/mnt/r2/${BUCKET_NAME}${PREFIX:+:${PREFIX}}" &
sleep 3

echo "Starting server on :8080"
exec /server
Loading
Loading