Ich hab mir meinen eigenen Social Hub gebaut. Also eine einzige Seite, auf der alles zusammenläuft: LinkedIn, Instagram, der Blog, der Podcast, der Newsletter, meine Speaker-Termine. Ein Link, den ich weitergebe, und dahinter steckt der ganze Röstmeister-Kosmos statt drei einzelner URLs, von denen ich beim Tippen sowieso den vierten vergesse.
Falls dir der Begriff nichts sagt: Solche „eine Seite für alle Kanäle“ laufen oft unter Namen wie Linktree. Das sind fertige Bio-Link-Tools, SaaS, generisch, deine Daten liegen bei einem Dritten, und für alles jenseits der Basics zahlst du. Ich wollte das gleiche Ergebnis, aber selbst gebaut und in meiner Hand.
Also hab ich mir gedacht: Das kann ich mir selbst bauen. Auf Azure. Und weil ich es später erweitern will, ohne mich jedes Mal durchs Portal zu klicken, mit Infrastructure as Code. Ein Kollege, dem ich das Ergebnis gezeigt hab, meinte nur: schon ’ne coole Sache. Das war der Ansporn, es sauber zu Ende zu bringen und öffentlich zu machen.
Kleiner Reality-Check vorweg, wer mich kennt weiß es: Ich bin kein Entwickler. Ich bin Consultant. Ich hab bei Bicep und beim Express-Server ab und zu eine KI um Rat gefragt, so wie man einen Kollegen fragt, der die Ecke besser kennt. Aber genau das ist der Punkt. Man muss kein Entwickler sein, um mal einen Proof of Concept aufzusetzen und zu schauen, was draus wird. Rausgekommen ist eine Seite, die seit drei Monaten produktiv läuft. Kosten bisher: null Euro.
Den kompletten, lauffähigen Code habe ich als öffentliches Blueprint-Repo bereitgestellt, sauber anonymisiert, MIT-Lizenz: github.com/DerM365Roestmeister/m365-social-hub-blueprint. Fork it, trag deine Links ein, deploy. In diesem Artikel gehen wir von der Idee über die Architektur bis zum echten Bicep-Code und dem Deployment.
Die Idee: bewusst simpel
Meine Anforderungen waren überschaubar. Eine Seite. Alle Links, Publikationen und Events darauf. Schnell, kein Tracking, kein Drittanbieter dazwischen. Volle Kontrolle über Design und Daten.
Was ich ausdrücklich nicht wollte: eine Datenbank, ein CMS, einen Build-Prozess mit halbem Frontend-Framework. Für eine Seite mit ein paar Link-Karten ist das mit Kanonen auf Spatzen. Also: eine statische index.html, ausgeliefert von einem winzigen Node/Express-Server. Mehr nicht. Der Avatar ist reines CSS, Initialen im Kreis, damit ich nicht mal ein Bild irgendwo hosten muss.
Die Entscheidung gegen ein fertiges Bio-Link-Tool war schnell getroffen. Ich wollte kein fremdes Branding, meine Daten nicht bei einem Dritten und keine Paywall für zwei Extra-Buttons. Der Preis dafür ist etwas Eigenarbeit. Der Gewinn ist, dass mir das Ding komplett gehört.
Warum Azure App Service
Für M365-Admins ist Azure kein fremder Planet. Gleicher Tenant, gleiches Billing, gleiche Kachel im Portal. Wenn du ohnehin täglich in Entra ID und Conditional Access unterwegs bist, ist „mal eben eine kleine Web-App hosten“ kein Kontextwechsel, sondern der Nachbargang.
App Service ist für so eine kleine Node-App genau richtig. Du musst keinen Server patchen, HTTPS ist eingebaut, und falls es doch mal größer werden soll, skalierst du hoch statt neu zu bauen. Der eigentliche Aufhänger für alle, die „mal eben was Kleines“ hosten wollen, ist aber der Preis: Ich fahre die Seite auf dem F1-Tier, dem Free Tier. Drei Monate live, null Euro Rechnung.
Azure Static Web Apps wäre für eine reine statische Seite auch ein Kandidat gewesen. Ich habe mich trotzdem für App Service mit einem kleinen Express-Server entschieden, weil ich zwei Dinge selbst in der Hand haben wollte: eigene Security-Header (Content-Security-Policy, HSTS und Kollegen) und einen /health-Endpoint für die Health-Checks. Das ist Geschmackssache, aber es war meine bewusste Entscheidung, kein Zufall.
Warum Bicep statt Klicken oder roher CLI
Angefangen hat das Projekt unspektakulär: mit imperativen az-Befehlen. Resource Group anlegen, Plan anlegen, Web App anlegen. Schritt für Schritt, brav der Reihe nach.
Das funktioniert. Beim ersten Mal. Das Problem kommt beim zweiten Mal. Willst du die Seite nochmal in einer anderen Region aufsetzen oder einem Kollegen zeigen, wie es geht, dann kopierst du Befehle aus einer alten Notiz zusammen und hoffst, dass du nichts vergessen hast. Keine Versionierung, keine Nachvollziehbarkeit, welche Ressource wann warum entstanden ist.
Bicep löst genau das. Du beschreibst den Zielzustand deklarativ, legst die Datei in Git, und ein einziger Befehl baut die komplette Infrastruktur. Für M365-Admins ist das gedanklich dasselbe Muster wie eine Configuration Baseline oder PowerShell DSC: Du sagst nicht „mach Schritt 1, dann 2, dann 3“, du sagst „so soll es am Ende aussehen“.
Und ja, Bicep kompiliert am Ende zu ARM-JSON. Aber du liest und schreibst eine saubere Konfigurationsdatei statt einer JSON-Wand mit spitzen Klammern. Kein ARM-Krieg. Das war der Moment, in dem ich als Nicht-Entwickler gemerkt habe: Das kann ich lesen. Das kann ich anpassen.
Die Architektur
Das Ganze ist absichtlich klein gehalten. Eine Resource Group, darin ein App Service Plan auf F1, darauf eine Web App mit Node 20 LTS, die per Express die statische index.html ausliefert. Keine Datenbank, kein CDN, keine Custom Domain im Standardfall.
rg-<appName>.npm install läuft beim Deploy automatisch auf der Plattform.index.html ausliefern, fertig.Der komplette Code dafür liegt offen im Blueprint-Repo: github.com/DerM365Roestmeister/m365-social-hub-blueprint.
Der Bicep-Code im Detail
Jetzt zum Kern. Alle Snippets sind 1:1 aus dem öffentlichen Repo, nichts nachgestellt.
Alles in einem Deploy: Subscription-Scope
Der Trick, der mir das manuelle az group create erspart, steckt gleich in Zeile eins von infra/main.bicep. Das Template arbeitet auf Subscription-Ebene und legt die Resource Group selbst mit an:
targetScope = 'subscription'
@description('Name of the Web App. Must be globally unique across Azure.')
param appName string
@description('Azure region for all resources.')
param location string = 'westeurope'
@description('App Service Plan SKU. F1 is the free tier.')
param sku string = 'F1'
resource rg 'Microsoft.Resources/resourceGroups@2024-03-01' = {
name: resourceGroupName
location: location
}
module appService 'modules/appservice.bicep' = {
name: 'appServiceDeployment'
scope: rg
params: {
appName: appName
location: location
sku: sku
nodeVersion: nodeVersion
enableCustomDomain: enableCustomDomain
customDomainName: customDomainName
}
}
Die Pointe: Ein einziger az deployment sub create erstellt Resource Group, Plan und Web App in einem Rutsch. Kein Vorspiel, kein „erst die Gruppe, dann der Rest“.
Das App-Service-Modul
Die eigentlichen Ressourcen liegen in einem separaten Modul, infra/modules/appservice.bicep. Das ist das Modul-Pattern in Bicep: Das Subscription-Template kümmert sich um die Resource Group, das Modul läuft im Scope dieser Gruppe und baut Plan und App. Sauberer, als alles in eine Datei zu quetschen.
resource appServicePlan 'Microsoft.Web/serverfarms@2023-12-01' = {
name: appServicePlanName
location: location
kind: 'linux'
properties: {
reserved: true
}
sku: {
name: sku
}
}
resource webApp 'Microsoft.Web/sites@2023-12-01' = {
name: appName
location: location
properties: {
serverFarmId: appServicePlan.id
siteConfig: {
linuxFxVersion: 'NODE|${nodeVersion}'
appSettings: [
{
name: 'SCM_DO_BUILD_DURING_DEPLOYMENT'
value: 'true'
}
]
}
httpsOnly: true
}
}
Zwei Dinge sind wichtig. linuxFxVersion sagt App Service, welche Runtime laufen soll. Und SCM_DO_BUILD_DURING_DEPLOYMENT auf true sorgt dafür, dass Azure beim Deploy selbst npm install fährt. Du lädst deinen Code hoch, den Rest macht die Plattform. httpsOnly erzwingt HTTPS, ohne dass du dich drum kümmern musst.
Parameter statt Hardcoding
Das hier ist für mich der eigentliche Aha-Moment gewesen, warum sich der ganze Blueprint überhaupt teilen lässt. In infra/main.bicepparam stehen keine echten Namen, nur Platzhalter:
using 'main.bicep'
param appName = 'change-me-app-name'
param location = 'westeurope'
param sku = 'F1'
param nodeVersion = '20-lts'
param enableCustomDomain = false
param customDomainName = ''
So macht man Infrastruktur-Code wiederverwendbar. Kein Tenant-Bezug im Template, nur Werte, die jede und jeder selbst ausfüllt. Genau deshalb kann ich das Repo öffentlich stellen und du kannst es 1:1 forken, ohne dass irgendwo meine Daten kleben.
Custom Domain, aber nur wenn du willst
Das Template wächst mit, ohne dass du bei F1 festhängst. Custom Domain und Managed Certificate werden nur angelegt, wenn du den Schalter umlegst. In Bicep sind das konditionale Ressourcen mit if:
resource hostNameBinding 'Microsoft.Web/sites/hostNameBindings@2023-12-01' = if (enableCustomDomain) {
parent: webApp
name: customDomainName
properties: {
siteName: appName
hostNameType: 'Verified'
}
}
Custom Domains gibt es auf dem F1 Free Tier nicht. Dafür brauchst du mindestens B1. Steht so auch als Kommentar im Code. Ein Parameter, ein Redeploy, fertig. Aber es kostet dann eben.
Deployment in der Praxis
So sieht der Ablauf aus, den ich tatsächlich benutze.
az deployment sub what-if \
--location westeurope \
--template-file infra/main.bicep \
--parameters infra/main.bicepparam
Sieht das gut aus, kommt der echte Deploy:
az deployment sub create \
--location westeurope \
--template-file infra/main.bicep \
--parameters infra/main.bicepparam
Und danach der Code:
zip -r deploy.zip . -x "node_modules/*" -x ".git/*" -x "*.zip"
az webapp deploy \
--name <appName> \
--resource-group rg-<appName> \
--src-path deploy.zip \
--type zip
Der klassische Stolperstein bei Node auf App Service ist übrigens das Startverhalten. Wenn die App nicht hochkommt, liegt es fast immer daran, dass der Startbefehl oder der PORT nicht stimmt. Deshalb liest der Express-Server den Port aus process.env.PORT, den App Service selbst setzt, und fällt lokal auf 3000 zurück. Genau die Art Detail, bei der ich mir eine zweite Meinung von der KI geholt habe, statt eine Stunde im Log zu suchen.
Die Kosten-Realität, ohne Schönfärberei
Nochmal klar: drei Monate live, F1-Tier, 0 € Rechnung. Das ist kein Marketing, das ist meine Abrechnung.
| Tier | Was du bekommst | Für wen geeignet |
|---|---|---|
| F1 · Standard im Blueprint 0 € pro Monat |
Kein Custom Domain, Tageskontingent von 60 CPU-Minuten (danach 403 bis zum Reset), kein „Always On“, also gelegentlich ein Cold Start | Persönliche Bio-Seite mit überschaubarem Traffic. Für eine produktive Kunden-App der falsche Ort. |
| B1 · Upgrade-Pfad rund 13 € pro Monat |
Custom Domain möglich, dedizierte Compute-Ressourcen, kein Tages-Quota | Wer eine eigene Domain will oder verlässliche Erreichbarkeit braucht. |
Der Umstieg ist dank Bicep trivial: sku von F1 auf B1, enableCustomDomain auf true, redeploy. Preise ändern sich, also immer kurz auf der Azure-Preisseite gegenprüfen.
Vom eigenen Projekt zum öffentlichen Blueprint
Warum ein separates, bereinigtes Repo, statt einfach meinen Live-Code zu zeigen? Weil in meiner produktiven Version echte Links, echte Termine und mein Tenant-Bezug drinstecken. Das gehört nicht in die Öffentlichkeit. Gleichzeitig wollte ich keinen Pseudo-Code zeigen, der „so ähnlich“ aussieht, sondern echten, lauffähigen Code.
Die Lösung: eine fiktive Beispiel-Persona, Platzhalter-Links, MIT-Lizenz. Bewusst so gebaut, dass du es forkst, deine Inhalte einträgst und deployst, ohne irgendwo meinen Kram herauspulen zu müssen. Das ist der Unterschied zwischen einem Screenshot und einem Blueprint.
Repo hier: github.com/DerM365Roestmeister/m365-social-hub-blueprint · Fork das Repo, trag deine eigenen Links ein, az deployment sub create — und du hast in 10 Minuten deine eigene Version, für 0 €.
Was bleibt
Ein persönlicher Social Hub muss weder teuer noch kompliziert sein. Mit App Service auf F1 kostet er nichts, mit Bicep ist die komplette Infrastruktur in Minuten reproduzierbar und versioniert.
Und man muss dafür kein Entwickler sein. Ein bisschen Neugier, ein bisschen Copy-Paste mit Verstand, ab und zu eine KI als Beifahrer. Der Rest ist Zielzustand beschreiben statt Schritte abarbeiten.
Der beste Proof of Concept ist der, der drei Monate später immer noch läuft und keinen Cent gekostet hat.
Das nimmst du mit
- App Service F1 hostet eine kleine Node-Seite kostenlos, mit den bekannten Grenzen: kein Custom Domain, 60 CPU-Minuten am Tag, Cold Start
- Bicep beschreibt die Infrastruktur deklarativ und versionierbar — dasselbe Denkmuster wie eine Configuration Baseline
targetScope = 'subscription'legt die Resource Group gleich mit an, ein Deploy statt drei Klicks- Parameter statt Hardcoding macht dein Template teilbar, ganz ohne Tenant-Bezug im Code
az deployment sub what-ifals Trockenlauf, bevor echte Ressourcen entstehen- Der Umstieg auf eine Custom Domain (B1, rund 13 € im Monat) ist ein Parameter plus Redeploy
- Du musst kein Entwickler sein, um einen POC zu wagen — forken, deployen, schauen was draus wird
Du hast das Repo geforkt oder eine Idee, was noch rein müsste? Schreib mir auf LinkedIn.
#EspressoM365Fusion