Cloudflare Workers sind ideal für leichtgewichtige Edge-Funktionen. Aber manchmal braucht man mehr: persistente Verbindungen, größere Bundles, SQLite-Dateien oder längere Ausführungszeiten. Fly.io füllt diese Lücke.
Tipp: Für einen Überblick über Hono auf verschiedenen Plattformen siehe Ultra-performante Web-APIs mit Hono.
Warum Fly.io?
Fly.io führt Container auf Servern weltweit aus – näher an den Nutzern als klassisches Hosting, aber mit mehr Möglichkeiten als reine Edge-Functions:
| Feature | Cloudflare Workers | Fly.io |
|---|---|---|
| Ausführungszeit | Max. 30s (paid) | Unbegrenzt |
| Memory | 128 MB | Bis 8 GB |
| Persistente Volumes | Nein | Ja |
| WebSockets | Eingeschränkt | Voll |
| SQLite-Dateien | Nein | Ja |
| Cold Starts | ~0ms | ~300ms |
Projekt-Setup
Ein minimales Bun-Projekt mit Hono:
mkdir my-api && cd my-api
bun init -y
bun add hono
// src/index.ts
import { Hono } from "hono";
import { logger } from "hono/logger";
const app = new Hono();
app.use("*", logger());
app.get("/", (c) => {
return c.json({
message: "Hello from Fly.io",
region: process.env.FLY_REGION || "local"
});
});
app.get("/health", (c) => c.text("ok"));
export default {
port: process.env.PORT || 3000,
fetch: app.fetch,
};
Dockerfile für Bun
FROM oven/bun:1-alpine AS base
WORKDIR /app
# Dependencies installieren
FROM base AS deps
COPY package.json bun.lockb ./
RUN bun install --frozen-lockfile --production
# Build (falls nötig)
FROM base AS build
COPY package.json bun.lockb ./
RUN bun install --frozen-lockfile
COPY . .
# RUN bun run build
# Production Image
FROM base AS runtime
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NODE_ENV=production
EXPOSE 3000
CMD ["bun", "run", "src/index.ts"]
Das Alpine-Image ist ~100 MB – deutlich kleiner als Node.js-Images.
Fly.io Konfiguration
# CLI installieren
brew install flyctl
# Einloggen
fly auth login
# App erstellen
fly launch
Die generierte fly.toml:
app = "my-bun-api"
primary_region = "fra"
[build]
[http_service]
internal_port = 3000
force_https = true
auto_stop_machines = "stop"
auto_start_machines = true
min_machines_running = 0
[checks]
[checks.health]
type = "http"
port = 3000
path = "/health"
interval = "10s"
timeout = "2s"
[[vm]]
memory = "256mb"
cpu_kind = "shared"
cpus = 1
Deployment
fly deploy
Die App läuft jetzt auf https://my-bun-api.fly.dev.
Environment Variables
# Einzelne Variable
fly secrets set DATABASE_URL="postgres://..."
# Aus .env-Datei
fly secrets import < .env.production
Im Code:
const dbUrl = process.env.DATABASE_URL;
Persistente Volumes
Für SQLite oder Datei-Storage:
# Volume erstellen (1 GB in Frankfurt)
fly volumes create data --size 1 --region fra
In fly.toml:
[mounts]
source = "data"
destination = "/data"
Im Code:
import { Database } from "bun:sqlite";
const db = new Database("/data/app.db");
Wichtig: Volumes sind an eine Region gebunden. Bei Multi-Region-Deployments braucht jede Region ihr eigenes Volume.
Multi-Region Deployment
# Zusätzliche Region hinzufügen
fly scale count 2 --region fra,iad
Fly.io routet Requests automatisch zur nächsten Region.
Read Replicas mit LiteFS
Für SQLite mit Multi-Region:
[mounts]
source = "litefs"
destination = "/litefs"
LiteFS repliziert SQLite-Änderungen automatisch zwischen Regionen.
Skalierung
# Horizontal: Mehr Machines
fly scale count 3
# Vertikal: Mehr Ressourcen
fly scale memory 512
fly scale vm shared-cpu-2x
Auto-Scaling basiert auf auto_stop_machines und auto_start_machines.
Logs und Monitoring
# Live Logs
fly logs
# Metriken im Dashboard
fly dashboard
Bun + SQLite auf Fly.io
Ein vollständiges Beispiel mit der nativen SQLite-Integration:
import { Hono } from "hono";
import { Database } from "bun:sqlite";
const db = new Database("/data/app.db");
// Schema erstellen
db.run(`
CREATE TABLE IF NOT EXISTS visits (
id INTEGER PRIMARY KEY AUTOINCREMENT,
path TEXT,
region TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
)
`);
const logVisit = db.prepare(
"INSERT INTO visits (path, region) VALUES (?, ?)"
);
const getStats = db.prepare(`
SELECT path, COUNT(*) as count
FROM visits
GROUP BY path
ORDER BY count DESC
LIMIT 10
`);
const app = new Hono();
app.use("*", async (c, next) => {
logVisit.run(c.req.path, process.env.FLY_REGION || "local");
await next();
});
app.get("/stats", (c) => {
return c.json(getStats.all());
});
export default {
port: process.env.PORT || 3000,
fetch: app.fetch,
};
Private Networking
Fly.io Apps können sich über ein privates Netzwerk erreichen:
// Service-to-Service Kommunikation
const response = await fetch("http://other-app.internal:3000/api");
Keine öffentliche Exposition nötig.
Vergleich: Wann was nutzen?
| Anwendungsfall | Empfehlung |
|---|---|
| Stateless API, global | Cloudflare Workers |
| API mit SQLite | Fly.io |
| WebSocket-Server | Fly.io |
| Lange Berechnungen | Fly.io |
| Minimale Kosten | Cloudflare (Free Tier) |
| Persistente Daten | Fly.io mit Volumes |