Ein Container läuft auf dem Laptop einwandfrei. Dasselbe Image kommt auf Azure App Service, startet, und App Service wartet. Es schickt Anfragen an Port 80 und an Port 8080, bekommt keine Antwort, weil die App auf 8000 lauscht, gibt auf, versucht es erneut, und die Nutzer sehen einen 503.
Dieses Erklärvideo zeigt, was App Service rund um einen eigenen Container tut: wie es das Image zieht, wie der Traffic den richtigen Port erreicht, was einen Neustart übersteht, wie Einstellungen ohne neuen Build hineinkommen und wo Sie suchen, wenn etwas schiefgeht. Eine Idee verbindet alles: Das Image ändert sich nie, alles andere wird von außen angeschlossen.
Das Wichtigste
- App Service findet nur die Ports 80 und 8080 von selbst. Jeder andere Port braucht die App-Einstellung
WEBSITES_PORT, und die App muss auf0.0.0.0lauschen, nicht auflocalhost. - Ziehen Sie das Image mit einer Managed Identity. Die Identität der Web-App bekommt die Rolle
AcrPullauf der Registry; nirgends wird ein Passwort gespeichert. - Das Dateisystem des Containers ist temporär. Nur
/homeübersteht Neustarts, und nur mit aktiviertem persistentem Speicher (App-EinstellungWEBSITES_ENABLE_APP_SERVICE_STORAGEauftrue). - Die Konfiguration kommt von außen. App-Einstellungen kommen als Umgebungsvariablen an, Slot-Einstellungen bleiben bei ihrem Slot, Geheimnisse kommen über Key-Vault-Verweise. So bedient ein Image jede Umgebung.
Das Image ziehen
Beim Anlegen einer Web-App für Container wählen Sie, woher das Image kommt. Azure Container Registry ist die empfohlene Quelle. Andere Registries funktionieren auch: Docker Hub, GitHub Container Registry oder jede Registry, die die Docker Registry API spricht.
Eine private Registry braucht eine Anmeldung, und dafür gibt es zwei Wege:
| Managed Identity | Admin-Benutzer | |
|---|---|---|
| Was es ist | eine Identität in Microsoft Entra ID, die zur Web-App gehört | Benutzername und Passwort der Registry |
| Zugriff | die Rolle AcrPull auf der Registry | der Admin-Benutzer muss in der Registry aktiviert sein |
| Geheimnisse | nirgends gespeichert | das Passwort liegt in der Konfiguration der App |
| Rotation | nichts zu rotieren | von Hand |
Eine systemzugewiesene Identität lebt und stirbt mit der App. Eine benutzerzugewiesene Identität ist eine eigene Ressource, die sich mehrere Apps teilen können.
az webapp create -g rg -p plan -n docproc \--container-image-name myacr.azurecr.io/docprocessor:v1az webapp identity assign -g rg -n docprocaz role assignment create --assignee $PRINCIPAL_ID \--role AcrPull --scope $ACR_IDaz webapp config set -g rg -n docproc \--acr-use-identity true --acr-identity [system]
Wann App Service zieht: Beim ersten Deployment zieht es jede Schicht, bei einem Neustart nur die geänderten Schichten, und beim Hochskalieren zieht jede neue Instanz das Image.
Neue Versionen: Zeigen Sie die App auf einen neuen Tag, dann startet App Service neu und zieht ihn. Pushen Sie dagegen ein neues Image unter demselben Tag, bemerkt App Service das nicht. Starten Sie die App neu oder schalten Sie Continuous Deployment ein: az webapp deployment container config --enable-cd true liefert eine Webhook-URL, und sobald Sie sie als Push-Webhook in der Registry eintragen, startet jeder Push die App neu.
Traffic und der Port
Anfragen aus dem Internet kommen bei den Frontends von App Service an. Die Frontends beenden die HTTPS-Verbindung und leiten reines HTTP in den Container weiter, an einen einzigen Port.
Diesen Port sucht App Service von selbst: 80 oder 8080. ASP.NET-Core-Container-Images lauschen seit .NET 8 auf 8080 und funktionieren deshalb ohne Einstellung. Jeder andere Port, etwa gunicorn auf 8000 oder Express auf 3000, braucht die App-Einstellung WEBSITES_PORT. Diese Einstellung liest App Service selbst; sie wird nicht als Umgebungsvariable an den Container weitergegeben.
Beim Start schickt App Service Warm-up-Anfragen und wartet auf irgendeine Antwort, selbst auf einen Fehler. Antwortet innerhalb des Start-Zeitlimits niemand, schlägt der Start fehl und App Service versucht es erneut. Das zeigt sich als 503-Fehler.
Noch eine Falle: Die App muss auf 0.0.0.0 lauschen, also auf allen Netzwerkschnittstellen. Lauscht sie nur auf localhost, erreichen Anfragen von außerhalb des Containers sie nie.
Ein Startbefehl ersetzt die CMD-Anweisung des Dockerfiles, der ENTRYPOINT bleibt:
az webapp config appsettings set -g rg -n docproc \--settings WEBSITES_PORT=8000az webapp config set -g rg -n docproc \--startup-file "gunicorn --bind=0.0.0.0:8000 app:app"
Dateien und Neustarts
Standardmäßig ist eine Datei, die die App schreibt, nach dem nächsten Neustart verschwunden. Das Dateisystem des Containers ist temporär: Startet die App neu oder zieht sie auf eine andere Maschine um, geht alles verloren, was im Container geschrieben wurde.
Für Dateien, die bleiben müssen, kann App Service persistenten Speicher unter /home einbinden. Bei eigenen Linux-Containern ist das standardmäßig aus; setzen Sie WEBSITES_ENABLE_APP_SERVICE_STORAGE auf true. Dann überstehen Dateien in /home Neustarts, jede Instanz einer hochskalierten App sieht dieselben Dateien, und der Platz zählt zum Speicherkontingent des App-Service-Plans.
/home ist eine Netzwerkfreigabe. Für Uploads und Ausgabedateien ist das in Ordnung, nicht aber für eine dateibasierte Datenbank wie SQLite, die Dateisperren braucht, die die Freigabe nicht bietet. Für große Datenmengen binden Sie Azure Storage ein oder nutzen eine echte Datenbank.
Einstellungen von außen
Das Image für Entwicklung, Staging und Produktion jeweils neu zu bauen, würde den Sinn eines Containers verfehlen. App-Einstellungen sind Name-Wert-Paare, die App Service beim Start des Containers als Umgebungsvariablen einspielt. Das Image bleibt gleich, nur die Variablen ändern sich.
import oslog_level = os.environ.get("LOG_LEVEL", "WARNING")storage = os.environ["STORAGE_ACCOUNT_NAME"]api_key = os.environ["API_KEY"]
- Verschlüsselt gespeichert. Alle App-Einstellungen werden verschlüsselt abgelegt.
- Namen: Buchstaben, Ziffern und Unterstriche. Ein verschachtelter .NET-Schlüssel wie
ConnectionStrings:Defaultwird alsConnectionStrings__Defaultgeschrieben. - Verbindungszeichenfolgen bekommen ein Präfix für den Datenbanktyp vor den Namen, zum Beispiel
SQLAZURECONNSTR_Defaultfür Azure SQL. Das hilft .NET-Apps; für Python oder Node sind normale App-Einstellungen einfacher. - Slot-Einstellungen: Mit Deployment-Slots markieren Sie die Werte, die zu einer Umgebung gehören, als Slot-Einstellungen. Beim Tausch von Staging in die Produktion wandert der Code, die Slot-Einstellungen bleiben bei ihrem Slot.
Für Geheimnisse nutzen Sie einen Key-Vault-Verweis als Wert der Einstellung. App Service holt das Geheimnis mit der Managed Identity der App, und der Code liest weiterhin nur eine Umgebungsvariable:
API_KEY = @Microsoft.KeyVault(SecretUri=https://myvault.vault.azure.net/secrets/api-key)
Nach einer Rotation kommt der neue Wert innerhalb von 24 Stunden an, oder sofort, wenn die App neu startet.
Warm und gesund
Ohne Traffic kann App Service eine untätige App nach etwa 20 Minuten entladen. Die nächste Anfrage wartet dann auf einen Kaltstart, und ein großes Image macht ihn langsamer. Always On verhindert das: App Service schickt alle fünf Minuten eine Anfrage an die App, sodass sie nie untätig wird. Dafür braucht es mindestens den Basic-Tarif.
Health Checks gehen einen Schritt weiter. Sie geben App Service einen Pfad, etwa /health. Jede Minute ruft App Service diesen Pfad auf jeder Instanz auf und erwartet einen Statuscode zwischen 200 und 299.
| Was passiert | Wann |
|---|---|
| Die Instanz verlässt den Load Balancer | nach zehn fehlgeschlagenen Checks in Folge (Standard) |
| Sie kehrt in den Load Balancer zurück | sobald sie wieder gesund antwortet |
| Sie wird ersetzt | nach einer Stunde im ungesunden Zustand |
Das hilft nur mit zwei oder mehr Instanzen: Bei einer einzigen Instanz gibt es keinen anderen Ort für den Traffic. Ein guter Health-Endpunkt prüft, was die App wirklich braucht, und liefert einen Fehler, wenn er es nicht erreicht:
@app.route("/health")def health():try:db.execute("SELECT 1")except Exception:return {"status": "unhealthy"}, 503return {"status": "healthy"}, 200
az webapp config set -g rg -n docproc --always-on trueaz webapp config set -g rg -n docproc \--generic-configurations '{"healthCheckPath": "/health"}'
Logs und wo Sie suchen
App Service fängt alles auf, was der Container auf die Standardausgabe und die Standardfehlerausgabe schreibt. Schalten Sie das Container-Logging ein, dann landen diese Zeilen unter /home/LogFiles. Der Log-Stream zeigt sie live, von jeder Instanz, jede Zeile mit ihrer Instanz-ID:
az webapp log config -g rg -n docproc \--docker-container-logging filesystemaz webapp log tail -g rg -n docproc
Für einen Blick hinter die Kulissen hat die Kudu-Seite (auch SCM-Seite, <app>.scm.azurewebsites.net) eine Umgebungsseite, die jede Variable auflistet, die die App bekommt. Kudu läuft in einem eigenen Container und sieht deshalb die Prozesse in Ihrem nicht. Dafür gibt es SSH: Das Image muss einen SSH-Server auf Port 2222 mitbringen, mit dem Root-Passwort, das App Service erwartet, und er ist nur über Azure erreichbar, nicht aus dem Internet. Für längere Aufbewahrung schicken Sie die Logs in einen Log-Analytics-Arbeitsbereich und fragen sie mit KQL ab.
| Symptom | Wo Sie suchen |
|---|---|
| Der Container startet nie | der Port (WEBSITES_PORT, 0.0.0.0) und der Log-Stream |
| Eine Einstellung scheint zu fehlen | die Umgebungsseite in Kudu |
| Das neue Image erscheint nicht | der Tag, oder die App neu starten |


