An API needs a language model. Put the model inside the API image, and every model update means a new API build. Run it as a separate service, and there is one more endpoint to secure and to scale. Azure App Service has a third way: the model runs right next to the API, in its own container, on the same localhost.
This explainer covers what an App Service sidecar is, how to configure the main container and its sidecars, how the containers talk and share files, how to find the one that failed, and when a separate service is the better choice. One picture helps with all of it: one instance of the app is an apartment. The main container has the only front door to the street, and every sidecar is a room down the hallway whose room number is its port.
Key points
- One main container, up to nine sidecars. Only the main container receives traffic from the internet, on port 80 or 8080. Sidecars are reached from inside, at
localhost:<port>. - One lifecycle, one budget. The containers start, stop and scale together, and they share the CPU and memory of the App Service plan. A memory-hungry model slows down the API next to it.
- Site containers, not classic settings. Sidecar apps define one container resource each;
WEBSITES_PORTand theDOCKER_REGISTRY_SERVER_*settings don't apply. The target port is only a label. - localhost and
/home. Containers call each other atlocalhostwith unique ports and exchange files in the shared/homefolder. Treat both like a real dependency: timeouts, readiness, recreatable files.
One app, several containers
A sidecar is an extra container that runs next to the main container, inside the same App Service app. An app has exactly one main container and up to nine sidecars.
- Traffic: only the main container receives traffic from the internet. The sidecars are reached from inside, over localhost.
- Lifecycle: App Service runs the containers as one app. They start together and 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.
- Images: each container keeps its own image, so a new model image ships without touching the API image.
The price is isolation. All containers on an instance share the CPU and memory of the App Service plan, so a model that takes most of the memory slows down the API, even when the API itself needs very little. They also share one network and one environment: only run images you trust.
In the portal, App Service also offers ready-made sidecars, such as a Redis cache or a Phi language model for local inference.
Configure main and sidecars
Sidecar apps use their own configuration model, called site containers. A classic custom container is one value (DOCKER| plus the image name); a sidecar app has one resource per container instead. Before sidecars, App Service ran several containers with Docker Compose, which retires on March 31, 2027; sidecars are its successor.
Create the app with the --sitecontainers-app option, or convert an existing custom container app. Classic settings like WEBSITES_PORT and the DOCKER_REGISTRY_SERVER_* settings don't configure site containers.
az webapp create -g rg -p plan -n chatapi \--sitecontainers-appaz webapp sitecontainers convert -g rg -n myapp \--mode sitecontainers
Every container gets a unique name, a full image reference and a role. Exactly one has isMain set to true, and it must listen on port 80 or 8080: those are the only ports for traffic from outside. Each sidecar gets its own name, image and port.
az webapp sitecontainers create -g rg -n chatapi \--container-name chat-api --is-main \--image myacr.azurecr.io/chat-api:v1 --target-port 8080az webapp sitecontainers create -g rg -n chatapi \--container-name model-server \--image myacr.azurecr.io/model-server:v1 \--target-port 11434
The target port is only a label. It routes nothing and 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 (--sitecontainers-spec-file) creates or updates every container:
[ { "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" }] } } ]
Image pulls: 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, and that identity needs the AcrPull role. Pin a fixed tag, not latest: a moving tag can mean a different image after the next restart. Then check what arrived with az webapp sitecontainers list, which shows every container, its role, its port and its image.
Localhost and a shared folder
All containers on an instance share one network namespace. So the API calls the model at http://localhost:11434: not by a container name, not by a service name. 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.
Local doesn't mean reliable. The model can restart, run out of memory, or simply take too long. 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.
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: the API can start long before the model has loaded. 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.
@app.get("/health/ready")def ready():try:r = requests.get(MODEL_HEALTH, timeout=5)r.raise_for_status()return {"status": "ready"}, 200except requests.RequestException:return {"status": "not ready"}, 503
WEBSITE_WARMUP_PATH=/health/readyWEBSITE_WARMUP_STATUSES=200
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 are needed. Write to a temporary name first, then rename it, so the reader never sees a half-written file:
shared = Path("/home/models")tmp = shared / "manifest.json.tmp"tmp.write_text(json.dumps(info))tmp.replace(shared / "manifest.json")
Two rules keep this working: don't set WEBSITES_ENABLE_APP_SERVICE_STORAGE to false, which removes the shared folder, and treat these files as recreatable. Data that must last belongs in Azure Storage or in a database.
When something fails
A sidecar failure usually shows up as an error in the main app. Work from the outside in, one boundary at a time, and change only one thing at a time:
- Definitions.
az webapp sitecontainers listcatches a missing sidecar, a wrong tag or two main containers;showprints one full definition, andstatustells you whether it runs. - Image pull. A pull fails before any code runs. Check the image name and tag, the identity and its
AcrPullrole. A fresh role assignment can take a few minutes, so wait before switching to passwords. The registry must also accept Azure Resource Manager tokens (its default); if that is turned off, pulls fail with "token validation failed". - Start.
az webapp sitecontainers log --container-name model-servershows 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. - 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.
- The localhost call. Its error tells you where to look:
| Error | Meaning |
|---|---|
| Connection refused | nothing listens on that port |
| Timeout | the model is slow, still loading, or out of resources |
| HTTP error | the connection worked: check the request or the model server |
Sidecar or separate service?
| A sidecar fits when | A separate service is better when |
|---|---|
| every instance needs exactly one local helper | several apps share the model |
| it starts, stops and scales with the app | it needs a GPU or its own scaling |
| examples: a small model for one API, a telemetry collector, a local cache | it needs its own security boundary, or must stay up while the app restarts |
Before you decide, measure: how much memory the model needs to load, what both containers use together under real load, and the peak when containers start at the same time during a deployment or scale-out. 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 the API stops fighting the model for memory.



