• English icon
  • German icon
Erklärvideo

Sidecar-Container auf Azure App Service: eine App, ein localhost

· Reihe: Container auf Azure

azureazure-app-servicesidecarscontainersdevops

Das Video ist auf Englisch.

Eine API braucht ein Sprachmodell. Packen Sie das Modell ins API-Image, bedeutet jedes Modell-Update einen neuen API-Build. Betreiben Sie es als eigenen Dienst, haben Sie einen Endpunkt mehr, den Sie absichern und skalieren müssen. Azure App Service bietet einen dritten Weg: Das Modell läuft direkt neben der API, in einem eigenen Container, auf demselben localhost.

Dieses Erklärvideo zeigt, was ein Sidecar in App Service ist, wie Sie den Hauptcontainer und seine Sidecars konfigurieren, wie die Container miteinander sprechen und Dateien teilen, wie Sie den fehlerhaften Container finden und wann ein eigener Dienst die bessere Wahl ist. Ein Bild hilft dabei: Eine Instanz der App ist eine Wohnung. Nur der Hauptcontainer hat die Haustür zur Straße, und jeder Sidecar ist ein Zimmer am Flur, dessen Zimmernummer sein Port ist.

Das Wichtigste

  • Ein Hauptcontainer, bis zu neun Sidecars. Nur der Hauptcontainer erhält Traffic aus dem Internet, auf Port 80 oder 8080. Sidecars erreichen Sie von innen über localhost:<port>.
  • Ein Lebenszyklus, ein Budget. Die Container starten, stoppen und skalieren gemeinsam und teilen sich CPU und Arbeitsspeicher des App Service Plans. Ein speicherhungriges Modell bremst die API daneben.
  • Site Containers statt klassischer Einstellungen. Sidecar-Apps definieren je Container eine eigene Ressource; WEBSITES_PORT und die Einstellungen DOCKER_REGISTRY_SERVER_* gelten nicht. Der Target Port ist nur eine Beschriftung.
  • localhost und /home. Die Container rufen sich über localhost mit eindeutigen Ports auf und tauschen Dateien im gemeinsamen Ordner /home aus. Behandeln Sie beides wie eine echte Abhängigkeit: Timeouts, Readiness, wiederherstellbare Dateien.

Eine App, mehrere Container

Ein Sidecar ist ein zusätzlicher Container, der neben dem Hauptcontainer in derselben App-Service-App läuft. Eine App hat genau einen Hauptcontainer und bis zu neun Sidecars.

  • Traffic: Nur der Hauptcontainer erhält Traffic aus dem Internet. Die Sidecars erreichen Sie von innen, über localhost.
  • Lebenszyklus: App Service betreibt die Container als eine App. Sie starten und stoppen gemeinsam. Skaliert die App hoch, bekommt jede neue Instanz den Hauptcontainer und alle seine Sidecars; skaliert sie herunter, geht das ganze Set.
  • Images: Jeder Container behält sein eigenes Image. Ein neues Modell-Image geht also live, ohne das API-Image anzufassen.

Der Preis ist die Isolation. Alle Container einer Instanz teilen sich CPU und Arbeitsspeicher des App Service Plans. Ein Modell, das den Großteil des Speichers belegt, bremst die API, selbst wenn die API kaum etwas braucht. Außerdem teilen sie sich ein Netzwerk und eine Umgebung: Betreiben Sie nur Images, denen Sie vertrauen.

Im Portal bietet App Service auch fertige Sidecars an, etwa einen Redis-Cache oder ein Phi-Sprachmodell für lokale Inferenz.

Hauptcontainer und Sidecars konfigurieren

Sidecar-Apps haben ein eigenes Konfigurationsmodell, die Site Containers. Ein klassischer Custom Container ist ein einziger Wert (DOCKER| plus Image-Name); eine Sidecar-App hat stattdessen eine Ressource pro Container. Vor den Sidecars betrieb App Service mehrere Container mit Docker Compose; Docker Compose wird am 31. März 2027 eingestellt, Sidecars sind der Nachfolger.

Legen Sie die App mit der Option --sitecontainers-app an, oder konvertieren Sie eine bestehende Custom-Container-App. Klassische Einstellungen wie WEBSITES_PORT und DOCKER_REGISTRY_SERVER_* konfigurieren Site Containers nicht.

az webapp create -g rg -p plan -n chatapi \
--sitecontainers-app
az webapp sitecontainers convert -g rg -n myapp \
--mode sitecontainers

Jeder Container bekommt einen eindeutigen Namen, eine vollständige Image-Referenz und eine Rolle. Genau einer hat isMain auf true, und er muss auf Port 80 oder 8080 lauschen: Nur diese Ports nehmen Traffic von außen an. Jeder Sidecar bekommt eigenen Namen, eigenes Image und eigenen Port.

az webapp sitecontainers create -g rg -n chatapi \
--container-name chat-api --is-main \
--image myacr.azurecr.io/chat-api:v1 --target-port 8080
az webapp sitecontainers create -g rg -n chatapi \
--container-name model-server \
--image myacr.azurecr.io/model-server:v1 \
--target-port 11434

Der Target Port ist nur eine Beschriftung. Er leitet nichts weiter und ändert den Prozess nicht; das Image selbst muss auf diesem Port lauschen.

Einstellungen gibt es in zwei Geltungsbereichen. App-Einstellungen erreichen jeden Container. Container-Variablen gehören zu einem Container, und in einer Container-Definition ist der Wert der Variablen der Name einer App-Einstellung, nicht der Wert selbst. Eine Spec-Datei enthält das ganze Set als JSON, sodass ein einziger Befehl (--sitecontainers-spec-file) alle Container anlegt oder aktualisiert:

[ { "name": "chat-api", "properties": { … } },
{ "name": "model-server", "properties": {
"image": "myacr.azurecr.io/model-server:v1",
"targetPort": "11434", "isMain": false,
"authType": "UserAssigned",
"userManagedIdentityClientId": "<client-id>",
"environmentVariables": [{ "name": "MODEL_NAME",
"value": "MODEL_NAME_VALUE" }] } } ]

Images ziehen: Beide Images können aus Azure Container Registry kommen. Mit einer User-assigned Managed Identity nennt jeder Container den Auth-Typ und die Client-ID der Identität, und diese Identität braucht die Rolle AcrPull. Verwenden Sie einen festen Tag statt latest: Ein wandernder Tag kann nach dem nächsten Neustart ein anderes Image bedeuten. Prüfen Sie danach mit az webapp sitecontainers list, was angekommen ist: jeder Container mit Rolle, Port und Image.

Localhost und ein gemeinsamer Ordner

Alle Container einer Instanz teilen sich einen Network Namespace. Die API ruft das Modell also über http://localhost:11434 auf: nicht über einen Container-Namen, nicht über einen Service-Namen. Deshalb muss auch jeder Port eindeutig sein; zwei Prozesse können nicht auf demselben Port lauschen. Skalieren Sie auf drei Instanzen, ruft jede API das Modell auf ihrer eigenen Instanz auf. Die Adresse ändert sich nie.

Lokal heißt nicht zuverlässig. Das Modell kann neu starten, keinen Speicher mehr haben oder schlicht zu lange brauchen. Behandeln Sie den Aufruf wie jeden Remote-Aufruf: ein kurzer Timeout für den Verbindungsaufbau, eine Deadline für die ganze Anfrage und Wiederholungen nur, wenn der Aufruf gefahrlos wiederholt werden kann.

MODEL = "http://localhost:11434/v1/chat/completions"
timeout = httpx.Timeout(10.0, connect=2.0)
async with httpx.AsyncClient(timeout=timeout) as client:
async with asyncio.timeout(30):
r = await client.post(MODEL, json=body)

Readiness: Die API kann lange vor dem Modell bereit sein. Geben Sie der API einen Readiness-Check, der zuerst das Modell fragt und mit 503 antwortet, bis das Modell bereit ist. Richten Sie die Warm-up-Anfragen von App Service mit WEBSITE_WARMUP_PATH auf diesen Pfad und akzeptieren Sie mit WEBSITE_WARMUP_STATUSES nur eine 200; dann wartet der Traffic, bis das Modell bereit ist.

@app.get("/health/ready")
def ready():
try:
r = requests.get(MODEL_HEALTH, timeout=5)
r.raise_for_status()
return {"status": "ready"}, 200
except requests.RequestException:
return {"status": "not ready"}, 503
WEBSITE_WARMUP_PATH=/health/ready
WEBSITE_WARMUP_STATUSES=200

Dateien: App Service bindet denselben Ordner /home in jeden Container ein. Der Model-Server schreibt ein Manifest nach /home/models, und die API liest genau denselben Pfad; zusätzliche Volume Mounts sind nicht nötig. Schreiben Sie zuerst unter einem temporären Namen und benennen Sie die Datei dann um, damit der Leser nie eine halb geschriebene Datei sieht:

shared = Path("/home/models")
tmp = shared / "manifest.json.tmp"
tmp.write_text(json.dumps(info))
tmp.replace(shared / "manifest.json")

Zwei Regeln halten das funktionsfähig: Setzen Sie WEBSITES_ENABLE_APP_SERVICE_STORAGE nicht auf false, das entfernt den gemeinsamen Ordner, und behandeln Sie diese Dateien als wiederherstellbar. Daten, die bleiben müssen, gehören in Azure Storage oder in eine Datenbank.

Wenn etwas ausfällt

Ein Sidecar-Fehler zeigt sich meist als Fehler in der Haupt-App. Arbeiten Sie sich von außen nach innen vor, eine Grenze nach der anderen, und ändern Sie immer nur eine Sache:

  1. Definitionen. az webapp sitecontainers list zeigt einen fehlenden Sidecar, einen falschen Tag oder zwei Hauptcontainer; show gibt eine vollständige Definition aus, und status sagt, ob der Container läuft.
  2. Image ziehen. Ein Pull scheitert, bevor Ihr Code läuft. Prüfen Sie Image-Name und Tag, die Identität und ihre Rolle AcrPull. Eine frische Rollenzuweisung kann ein paar Minuten dauern; warten Sie, bevor Sie auf Passwörter umsteigen. Die Registry muss außerdem Azure-Resource-Manager-Tokens akzeptieren (die Voreinstellung); ist das abgeschaltet, scheitern Pulls mit „token validation failed".
  3. Start. az webapp sitecontainers log --container-name model-server zeigt nur die Ausgabe dieses Containers. Achten Sie auf einen Exit-Code, eine fehlende Variable, einen falschen Port oder einen Prozess, der sich beendet, statt im Vordergrund zu bleiben.
  4. Arbeitsspeicher. Neustarts ohne Fehlermeldung bedeuten oft, dass dem Modell der Speicher ausging. Ein Modell zu laden braucht viel Speicher, und jedes Hochskalieren lädt es auf der neuen Instanz erneut. Vergleichen Sie die CPU- und Speichermetriken mit dem Zeitpunkt des Fehlers.
  5. Der localhost-Aufruf. Sein Fehler zeigt, wo Sie suchen müssen:
FehlerBedeutung
Connection refusedauf dem Port lauscht nichts
Timeoutdas Modell ist langsam, lädt noch oder hat keine Ressourcen mehr
HTTP-Fehlerdie Verbindung stand: Prüfen Sie die Anfrage oder den Model-Server

Sidecar oder eigener Dienst?

Ein Sidecar passt, wennEin eigener Dienst ist besser, wenn
jede Instanz genau einen lokalen Helfer brauchtmehrere Apps sich das Modell teilen
er mit der App startet, stoppt und skalierter eine GPU oder eigene Skalierung braucht
Beispiele: ein kleines Modell für eine API, ein Telemetrie-Collector, ein lokaler Cacheer eine eigene Sicherheitsgrenze braucht oder laufen muss, während die App neu startet

Messen Sie, bevor Sie entscheiden: wie viel Speicher das Modell zum Laden braucht, was beide Container zusammen unter echter Last verbrauchen und den Spitzenwert, wenn bei einem Deployment oder beim Hochskalieren Container gleichzeitig starten. Wählen Sie einen Plan für diese gemeinsame Spitze plus Reserve. Belegt das Modell den Großteil der Instanz oder braucht es eine andere Skalierung, ziehen Sie es aus: Das kostet einen Netzwerk-Hop, aber die API kämpft nicht mehr mit dem Modell um Speicher.

Weiterführende Links

Video-Transkript (Englisch)

Your API needs a language model. Put it inside the API image, and every model update means a new API build. Run it as a separate service, and you get one more endpoint to secure and to scale. App Service has a third way: the model runs right next to your API, in its own container, on the same localhost.

In this video, you will see what a sidecar is, how to set up the main container and its sidecars, how they talk and share files, how to find the one that failed, and when a separate service is the better choice. Picture one instance of your app as an apartment. The main container has the only front door to the street. Every sidecar is a room down the hallway, and its room number is its port.

So what is a sidecar? It's an extra container that runs next to your main container, inside the same App Service app. An app has exactly one main container, and up to nine sidecars. Only the main container receives traffic from the internet. The sidecars are reached from inside, over localhost. App Service runs them as one app. They start together, and they stop together. When the app scales out, every new instance gets the main container and all of its sidecars. When it scales in, the whole set goes. Each container keeps its own image. So you can ship a new model without touching the API image. The price is isolation. All containers on an instance share the CPU and memory of your App Service plan. A model that takes most of the memory slows down your API, even when the API itself needs very little. They also share one network and one environment. So only run images you trust. In the portal, App Service even offers ready-made sidecars, like a Redis cache, or a Phi language model for local inference.

How do you tell App Service which container is which? Sidecar apps use their own configuration model, called site containers. A classic custom container is one value: DOCKER, then the image name. A sidecar app has one resource per container instead. Before sidecars, App Service ran several containers with Docker Compose. Docker Compose retires on March 31, 2027, and sidecars are its successor. You create the app with the sitecontainers-app option, or convert an existing custom container app. But careful: classic settings like WEBSITES_PORT, and the Docker registry settings, don't configure site containers. Every container gets a unique name, a full image reference, and a role. Exactly one has isMain set to true. The main container must listen on port 80 or 8080. Those are the only ports for traffic from outside. The model server is a sidecar. It gets its own name, its own image, and its own port: 11434. That target port is only a label. It routes nothing, and it doesn't change the process. The image itself must listen on that port. Settings come in two scopes. App settings reach every container. Container variables belong to one container. And in a container definition, the variable's value is the name of an app setting, not the value itself. A spec file holds the whole set in JSON, so one command creates or updates every container. Both images can come from Azure Container Registry. With a user-assigned managed identity, each container names the auth type and the identity's client ID. That identity needs the AcrPull role. And pin a fixed tag, not latest. A moving tag can mean a different image after the next restart. Then check what arrived: the list command shows every container, its role, its port, and its image.

Now the API has to reach the model. All containers on an instance share one network namespace. So the API calls localhost, port 11434. Not a container name, not a service name. Just localhost. That's also why every port must be unique. Two processes can't listen on the same port. Scale out to three instances, and each API calls the model on its own instance. The address never changes. But local doesn't mean reliable. The model can restart, run out of memory, or simply take too long. So treat the call like any remote call: a short timeout to connect, a deadline for the whole request, and retries only when the call is safe to repeat. In Python, that's a client with a connect timeout, and a deadline around the request. There is also a timing problem. The API can start long before the model has loaded. So give the API a readiness check that asks the model first, and answers 503 until the model is ready. Point App Service's warm-up requests at that path with WEBSITE_WARMUP_PATH, and accept only a 200 with WEBSITE_WARMUP_STATUSES. Then traffic waits until the model is ready. Here is the readiness check, and the two settings. For files, App Service mounts the same /home folder into every container. The model server writes a manifest to /home/models, and the API reads the very same path. No extra volume mounts needed. Write to a temporary name first, then rename it. The reader never sees a half-written file. Two rules keep this working. Don't set WEBSITES_ENABLE_APP_SERVICE_STORAGE to false: that removes the shared folder. And treat these files as recreatable. Data that must last belongs in Azure Storage, or in a database.

When a sidecar fails, you usually see it as an error in the main app. So work from the outside in, one boundary at a time, and change only one thing at a time. First: did the configuration arrive? The list command catches a missing sidecar, a wrong tag, or two main containers. Show prints one full definition, and status tells you whether it runs. Second: the image pull. A pull fails before any of your code runs. Check the image name and tag, the identity, and its AcrPull role. A fresh role assignment can take a few minutes, so wait before you switch to passwords. The registry must also accept Azure Resource Manager tokens, which is its default. If someone turned that off, pulls fail with token validation failed. Third: the start. The log command, with a container name, shows only that container's output. Look for an exit code, a missing variable, a wrong port, or a process that exits instead of staying in the foreground. Fourth: memory. Restarts without any error often mean the model ran out of room. Loading a model needs a lot of memory, and every scale-out loads it again on the new instance. Compare the CPU and memory metrics with the time of the failure. Fifth: the localhost call. Its error tells you where to look. Connection refused: nothing listens on that port. A timeout: the model is slow, still loading, or out of resources. An HTTP error: the connection worked, so look at the request, or at the model server itself.

So when is a sidecar the right choice? When every instance of your app needs exactly one local helper, that starts, stops and scales with it. A small model for one API, a telemetry collector, a local cache. A separate service is better when several apps share the model, when it needs a GPU or its own scaling, its own security boundary, or when it must stay up while your app restarts. Before you decide, measure. How much memory does the model need to load? What do both containers use together, under real load? And during a deployment or a scale-out, containers can start at the same time. Pick a plan for that combined peak, plus headroom. If the model takes most of the instance, or needs different scaling, move it out. That costs a network hop, but your API stops fighting the model for memory.

To sum it up. A sidecar is a container that lives and scales with your main container, in the same app. One container is main, on port 80 or 8080. Every sidecar gets its own unique port. They talk over localhost, and share files in /home. When something fails, check the definitions, the pull, the start, the memory, and the localhost call, in that order. And when the model outgrows the instance, give it its own service.

One instance, one localhost, one shared budget. If this was useful, subscribe for more practical .NET and Azure.

Brauchen Sie das in Ihrer eigenen Azure-Umgebung?

Ich entwickle, migriere und betreibe .NET-Plattformen auf Azure, mit Azure DevOps und Terraform. Wenn Ihr Team das sauber aufsetzen möchte, sehen Sie sich mein Angebot an.

Zu den Leistungen