Bun-Apps auf Fly.io deployen

Container-basiertes Deployment für Anwendungen mit mehr Ressourcenbedarf

6 Minuten
Bun-Apps auf Fly.io deployen
#Bun #Fly.io #Docker #Deployment

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:

FeatureCloudflare WorkersFly.io
AusführungszeitMax. 30s (paid)Unbegrenzt
Memory128 MBBis 8 GB
Persistente VolumesNeinJa
WebSocketsEingeschränktVoll
SQLite-DateienNeinJa
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?

AnwendungsfallEmpfehlung
Stateless API, globalCloudflare Workers
API mit SQLiteFly.io
WebSocket-ServerFly.io
Lange BerechnungenFly.io
Minimale KostenCloudflare (Free Tier)
Persistente DatenFly.io mit Volumes

Quellen