Portfolio-Site, OCI-Style

Meine Speaker-Seite lebt jetzt komplett in der Oracle Cloud. Kein Webserver, kein VM, keine Nginx-Config, keine systemd-Units. Nur Jekyll-Output in einem Object-Storage-Bucket und ein API Gateway davor. Dieser Post beschreibt die Architektur, die Fallstricke und warum es nicht der Weg ist, den ich zuerst eingeschlagen habe.

Ziel-Bild

  Browser (robbie.databee.org)
            │ CNAME auf OCI-Gateway-Hostname
            ▼
  OCI API Gateway (public, eu-frankfurt-1)
       TLS-Terminierung, Path-Rewrites
            │
            ├─ /          → bucket/o/index.html
            ├─ /{path*}   → bucket/o/${path}
            └─ /files/*   → assets-bucket/o/${path}
            │
            ▼
  OCI Object Storage
      robbie-databee-org      (Website-HTML, 2500+ Objekte)
      robbie-talks-assets     (Slides, Demos, Video-Clips)

Zwei Buckets, ein Gateway, kein Compute. Betriebskosten für das Traffic-Niveau einer Speaker-Seite: im Bereich von Cent pro Monat.

Warum nicht Load Balancer?

Das war mein erster Reflex und der erste Sackgassen-Moment. OCI Load Balancer kann Redirects und Path-Based-Routing auf verschiedene Backend-Sets — aber kein transparentes URL-Rewriting. Für Object-Storage-Hosting brauchst du genau das, weil die Web-URL /about/ auf den Bucket-Key about/index.html abgebildet werden muss. Nicht als 3xx-Redirect an den Browser, sondern intern.

Oracles eigener Blog empfiehlt für Object-Storage-fronted Static Sites API Gateway oder einen eigenen Webserver, nicht LB. Ich habe den Reflex mit zwei Stunden VCN-Konfiguration bestraft, bevor ich die Doku genauer gelesen habe. Lektion: auf das Werkzeug hören, das die Plattform selbst für das Problem vorsieht.

Die drei URL-Formen

Damit /foo, /foo/ und /foo/index.html alle funktionieren, ohne dass API Gateway String-Manipulation im Request-Path macht (was es nicht kann), lade ich jede Seite beim Deploy dreifach hoch: als foo/index.html (kanonisch), als foo/ (trailing-slash-Alias) und als foo (bare-Alias). Alle drei Objekte zeigen auf dieselbe HTML- Datei.

Storage ist billig genug, dass die Triple-Aufbewahrung nicht schmerzt. Für 120 Talks + diverse Pages sind das ~2500 Objekte. Der Deploy-Helper in Python macht das parallel über das OCI-SDK:

def iter_uploads(site_dir):
    for root, _, files in os.walk(site_dir):
        for name in files:
            local = Path(root) / name
            rel = Path(root).relative_to(site_dir) / name
            yield (str(local), rel.as_posix())
            if name == "index.html":
                rel_dir = Path(root).relative_to(site_dir).as_posix()
                if rel_dir and rel_dir != ".":
                    yield (str(local), f"{rel_dir}/")
                    yield (str(local), rel_dir)

MIME-Typen: die stille Falle

oci os object bulk-upload ist schnell und bequem, aber es setzt Content-Type nicht aus der Dateiendung. Alle Objekte bekommen application/octet-stream. Browser interpretieren das als “unbekannter Binärblob” und bieten Download statt Render. CSS lädt nicht, JavaScript wird nicht ausgeführt, und Chrome zeigt dir einen freundlichen Download-Dialog für eine Datei namens “Download”.

Lösung: im Deploy-Helper explizit Content-Type aus der Dateiendung setzen. Python macht das mit einem kleinen Mapping .css → text/css, .js → text/javascript, .html → text/html; charset=utf-8 — und Object Storage speichert das als Object-Metadata, das der Gateway an den Client durchreicht.

TLS: die Zwei-Resource-Lösung

Self-signed Cert zum Starten, Let’s Encrypt sobald DNS zeigt. Weil die DNS bei Strato liegt (kein OCI-DNS, also kein automatisches ACME via OCI Certificate Service), läuft die Erst-Ausstellung mit certbot --manual --preferred-challenges dns und einem einmaligen TXT-Record beim Registrar.

Spannender Teil: wie das Cert in API Gateway reinkommt. Erste Runde habe ich oci api-gateway certificate create benutzt — funktioniert, aber das Service-Limit für API-Gateway-Certificates ist 1 pro Region, und nach einem Delete ist der Zähler eventually-consistent. Zehn Minuten Warten, dann ging Create erst wieder. Das ist kein Fehler, sondern Design — und ein Problem, wenn du mal rotieren willst ohne Downtime.

Der Ausweg: OCI Certificate Service (certs-mgmt) hat ein anderes Quota, und der Gateway akzeptiert dessen OCIDs als certificate-id. Zwei Resource-Typen, beide verwendbar, nur das Certs-Mgmt-Quota ist weniger eng.

Dritte Lektion gratis: OCI API Gateway schickt das Intermediate- Cert nicht automatisch mit, auch wenn du es beim create-by-importing-config als cert-chain-pem übergibst. Der Gateway braucht zusätzlich ein CA-Bundle, das du separat als OCI-Certs-Mgmt-Resource anlegst und per --ca-bundles an den Gateway hängst. Ohne das sagt Chrome zwar “Zertifikat gültig”, aber trotzdem “Verbindung nicht sicher” — die Kette ist nicht komplett.

Content-Management für Vortrags-Schrottlöcher

Die alte Seite hatte 62 Talks in einem verschachtelten Ordnerbaum mit Leerzeichen und Umlauten in den Verzeichnis-Namen. Die neue Struktur ist flach: _talks/YYYY-MM-DD-slug.md. Ein Python-Skript hat die Migration gemacht — inklusive redirect_from: in jeder Datei, damit die alten Post-Style-URLs (/talks/2021/01/27/slug/) per jekyll-redirect-from 301 auf das neue Schema zeigen. SEO- Juice bleibt erhalten.

Lücken-Schließung für 2021–2026: Oracle ACE bietet einen HTML-Export seiner Contributions-Liste an. Ein weiteres Python-Script parst den Export, dedupliziert nach Datum + normalisiertem Titel gegen die bestehenden Markdown-Dateien, und schreibt neue Entries für alles, was fehlt. 58 Vorträge auf einen Schlag nachgeholt, 25 falsch-erkannte Sprachen via Heuristik-Korrektur gefixt.

Upcoming-Lifecycle

Jeder Vortrag hat ein status-Feld:

proposed  → Abstract eingereicht
accepted  → Zusage da, Upcoming-Widget zeigt ihn
scheduled → Termin fix, Slides-Arbeit läuft
delivered → gehalten, Recording verlinkt
archived  → komplett dokumentiert

Die Landing-Page hat zwei Tiles: “Triff mich” zeigt die nächsten accepted/scheduled-Talks, “Vergangene Vorträge” zeigt Zähler + letzten Eintrag. Auf /talks/ kommen die Upcoming oben, darunter Jahres-Archiv. Ein future: true im _config.yml — sonst baut Jekyll die Zukunfts-Talks gar nicht erst.

Analytics ohne Google Analytics

GA war der einzige Grund, einen Consent-Banner zu brauchen. OCI API Gateway schreibt Access-Logs, und über Service Connector Hub landen die in einem Object-Storage-Bucket. Mit GoAccess darüber fällt ein HTML-Dashboard raus: Seitenaufrufe, Referrer, User-Agents, Top-Pfade, 404s. Keine Cookies, kein JavaScript, kein Consent. GA raus, Seite wieder DSGVO-sauber.

Was das Setup kostet

  • OCI Object Storage: Always-Free für 10 GB, wir sind bei ~500 MB (inkl. aller Slides-Uploads)
  • API Gateway: nicht Always-Free, aber für Traffic in dieser Grössenordnung ~0,10–0,30 € / Monat
  • Compute: 0 €, läuft nichts
  • DNS: bei Strato sowieso schon bezahlt
  • Cert: Let’s Encrypt, 0 €

Summa summarum: unter 50 Cent pro Monat. Und der Stack ist vollständig in der OCI — für einen Oracle-Speaker ist das ein hübscher “eat your own dogfood”-Punkt auf der Speaker-Page selbst.

Was noch offen ist

  • Cert-Renewal vollautomatisch, ohne manuellen TXT-Record-Tanz
  • CI/CD via GitLab (aktuell läuft der Deploy lokal)
  • Log-Analytics-Pipeline vom Gateway bis zum Dashboard
  • WebP + Lazy-Loading für die verbliebenen grossen Bilder
  • Erweiterung über “nur Talks” hinaus: Workshops, Podcasts, Publikationen als eigene Collections

Den Deploy-Workflow dokumentiert ein dediziertes README im Repo. Wer das Muster selber fahren will: das Template ist auf GitLab und funktioniert mit wenig Anpassung für jede Jekyll-basierte Speaker-Seite.