Deploying azure container apps
Skill alexpizarro/azure-lean-stack-skills/skills/deploying-azure-container-apps
Azure apps that cost nothing when nobody's using them. A Claude Code skill pack — 14 composable skills, 37+ documented gotchas, branch-per-env CI/CD.
npx -y skills add alexpizarro/azure-lean-stack-skills --skill deploying-azure-container-appsAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Deploys Docker containers to Azure Container Apps with scale-to-zero, multi-container sidecars, shared managed environments, and Container Apps Jobs for batch workloads. Use when SWA + FC1 don't fit — long-running servers, WebSocket/SSE streaming, custom Docker runtimes, scheduled or queue-driven background jobs, or multi-process workloads with sidecars.
SKILL.md
7.4 KB, as published. Nobody here has run it
Deploying Azure Container Apps
Scale-to-zero Docker containers for workloads SWA + FC1 can't handle: long-running servers, WebSocket/SSE, streaming, custom runtimes, multi-process sidecars, and run-to-completion background jobs.
When to use Container Apps
| Need | Use |
|---|---|
| CRUD API + React frontend | SWA — not this |
| Timer/queue trigger, AI workload, < 30 min | FC1 — not this |
| Long-running HTTP server, > 30s requests | Container App (this skill) |
| WebSocket / SSE / streaming | Container App with --request-timeout 1800 |
| Sidecar (e.g. headless browser, embedded DB) | Container App multi-container |
| Batch / cron / one-shot processor | Container Apps Job (this skill, jobs section) |
| Multiple related apps sharing logs/networking | Shared managed environment (this skill) |
Cost defaults
| State | Cost |
|---|---|
Idle (minReplicas: 0) | $0 |
| Active (0.25 vCPU, 0.5 GiB) | pennies/month at low traffic |
Always warm (minReplicas: 1) | ~$5/month (no cold start) |
| Log Analytics workspace | bound by dailyQuotaGb on the workspace (see instrumenting-azure-app-insights) |
Critical pattern: share the managed environment
A Microsoft.App/managedEnvironments resource is the unit that owns networking + Log Analytics. One environment can host many Container Apps and Jobs. Sharing saves Log Analytics workspace cost and simplifies VNet wiring.
// One env created once
module env 'modules/managedEnv.bicep' = { ... }
// Multiple apps reuse it
module appA 'modules/containerApp.bicep' = {
params: { managedEnvironmentId: env.outputs.id, ... }
}
module appB 'modules/containerApp.bicep' = {
params: { managedEnvironmentId: env.outputs.id, ... }
}
module syncJob 'modules/containerAppJob.bicep' = {
params: { environmentId: env.outputs.id, ... }
}
See references/multi-app-shared-env.md.
Container Apps Jobs — scale-to-zero by nature
For run-to-completion workloads (batch processing, sync, scheduled crawls), use Microsoft.App/jobs instead of a Container App. A Job only runs when triggered; there are no idle replicas.
Triggers:
Manual— started viaaz containerapp job start(with optional--env-varsfor per-run inputs)Schedule— cron expressionEvent— Azure queue/event source
resource job 'Microsoft.App/jobs@2024-03-01' = {
properties: {
environmentId: env.id
workloadProfileName: 'Consumption' // no idle billing
configuration: {
triggerType: 'Manual'
replicaTimeout: 600 // seconds — max single-run duration
replicaRetryLimit: 0
manualTriggerConfig: {
parallelism: 1
replicaCompletionCount: 1
}
}
template: { ... }
}
}
Trigger with per-run inputs:
az containerapp job start \
--name "$JOB_NAME" \
--resource-group "$RG" \
--env-vars "INPUT_ID=$ID" "BLOB_PATH=$PATH"
Multi-container sidecar pattern
A Container App can run multiple containers in a single replica, sharing localhost networking. Useful for embedded dependencies like a headless browser:
template:
containers:
- name: app
image: $ACR/app:$TAG
env:
- name: BROWSER_URL
value: http://localhost:11235 # talks to sidecar
- name: browser
image: docker.io/unclecode/crawl4ai:0.8.6 # pin — avoid :latest drift
Apply via YAML to keep both containers atomic:
az containerapp update --name "$APP_NAME" --resource-group "$RG" --yaml app.yaml
See references/sidecar-pattern.md.
Secrets via secretref:
Container App secrets are write-only — values can't be read back. Link to env vars:
az containerapp secret set --name "$APP_NAME" --resource-group "$RG" \
--secrets "openai-key=$OPENAI_API_KEY"
az containerapp update --name "$APP_NAME" --resource-group "$RG" \
--set-env-vars "OPENAI_API_KEY=secretref:openai-key"
Gotcha: updating a secret value doesn't restart replicas. Force a revision restart:
REVISION=$(az containerapp show --name "$APP_NAME" --resource-group "$RG" \
--query "properties.latestRevisionName" -o tsv)
az containerapp revision restart --name "$APP_NAME" --resource-group "$RG" --revision "$REVISION"
Request timeout for SSE / WebSocket
Default is 240 seconds. Streaming connections drop at 4 minutes without this:
az containerapp ingress update --name "$APP_NAME" --resource-group "$RG" --request-timeout 1800
Maximum: 3600 (1 hour).
Probes (health checks)
Liveness + readiness probes prevent traffic to a starting container and recycle stuck ones:
probes:
- type: liveness
httpGet: { path: /health, port: 8000 }
initialDelaySeconds: 10
periodSeconds: 30
- type: readiness
httpGet: { path: /health, port: 8000 }
initialDelaySeconds: 5
periodSeconds: 10
See references/probes.md.
Image registry — GHCR vs ACR
| Registry | When |
|---|---|
| GHCR (ghcr.io) | Public images, GitHub-hosted projects. Free for public repos. |
| Docker Hub | Avoid anonymously (100 pulls/6h rate limit). Pin tags if used. |
| ACR (Azure Container Registry) | Private images, fine-grained RBAC. ~$5/month Basic tier. |
For non-secret images, GHCR is the cheapest option. See references/ghcr-vs-acr.md.
Dockerfile template
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build && npm prune --production
EXPOSE 8000
ENV NODE_ENV=production
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:8000/health || exit 1
CMD ["node", "dist/server.js"]
Key points:
- Alpine for small images (faster cold start)
- Build with devDeps, then
npm prune --production - Use
wgetnotcurl(Alpine doesn't bundle curl) - Always expose
/health
Composes with
- scaffolding-azure-bicep-infrastructure — for the modular toggle to add ACA
- optimizing-azure-blob-storage-cost — when the container uses blob storage
- instrumenting-azure-app-insights — for the Log Analytics workspace + alerts
- Microsoft
azure-diagnosticsfor live container debugging via the Azure MCP
Templates
| File | Purpose |
|---|---|
| templates/containerApp.bicep | HTTP Container App with scale-to-zero |
| templates/containerAppJob.bicep | Manual-trigger Container Apps Job |
| templates/managedEnv.bicep | Shared managed environment (Consumption profile) |
| templates/multi-container.yaml | YAML for sidecar pattern |