Level: Intermediate
Estimated duration: 6–8 hours
Last updated: June 2026
Table of Contents
- Introduction to Azure Containers
- Part 1 – Deploying Azure Container Instances
- Part 2 – Managing Azure Container Instances
- Part 3 – Integrating ACI with Azure Services
- Part 4 – Optimizing Performance and Costs
- Part 5 – Monitoring and Securing ACI
- Complete Code Examples
- Comparison Tables
- Glossary
Introduction to Azure Containers
Why containers change everything
Containers have revolutionized the way we package, deploy, and manage applications. Where a virtual machine virtualizes the full hardware stack (CPU, memory, OS), a container shares the host OS kernel but isolates the application process in its own namespace. This fundamental difference translates to:
- Ultra-fast startup: seconds instead of minutes
- Increased density: dozens of containers on a single host vs. a few VMs
- Portability: “it works on my machine” → “it works everywhere”
- Reproducibility: the Dockerfile is the executable documentation
Containers have also made large-scale microservices architecture possible, where each service can be deployed, updated, and scaled independently from the others.
Overview of Azure container services
graph TD
subgraph AZURE_CONTAINERS["Azure Container Services"]
ACI["Azure Container Instances\nServerless, on-demand\nNo orchestration"]
ACA["Azure Container Apps\nEvent-driven microservices\nKEDA autoscaling"]
AKS["Azure Kubernetes Service\nFull orchestration\nComplex workloads"]
APPSERV["Azure App Service\nWeb applications\nTraditional PaaS"]
ACR["Azure Container Registry\nPrivate registry\nDocker images"]
end
ACR -->|Provides images| ACI
ACR -->|Provides images| ACA
ACR -->|Provides images| AKS
ACR -->|Provides images| APPSERV
style ACI fill:#f59e0b,color:#000
style ACA fill:#8b5cf6,color:#fff
style AKS fill:#3b82f6,color:#fff
style APPSERV fill:#10b981,color:#fff
style ACR fill:#ef4444,color:#fff
Part 1 – Deploying Azure Container Instances
When to use ACI?
Azure Container Instances (ACI) is a serverless container service: you deploy a container without having to manage any server, cluster, or orchestrator. It is the simplest and fastest way to deploy a container in Azure.
ACI excels in the following scenarios:
- Batch tasks and CI/CD jobs: ephemeral workers that run and stop
- Event processing: responding to Azure Functions or Logic Apps triggers
- Rapid prototyping: testing an image before deploying it to AKS
- Bursting from AKS: using ACI as a virtual node to absorb load spikes
- Development environments: on-demand instances for developers
ACI is NOT suitable for:
- Applications requiring native automatic horizontal scaling
- Complex microservice orchestration (→ AKS or Container Apps)
- Workloads requiring high-performance data volumes (→ AKS with Premium disks)
- Applications with complex state and shared volumes (→ AKS with StatefulSets)
Full comparison table:
| Service | Orchestration | Scaling | Complexity | Primary use case |
|---|---|---|---|---|
| ACI | ❌ No | Manual / Event | ⭐ Very simple | Batch jobs, prototyping, bursting |
| Container Apps | ✅ Basic (KEDA) | Auto (scale-to-zero) | ⭐⭐ Simple | Microservices, event-driven APIs |
| AKS | ✅ Full (K8s) | Auto (HPA/VPA/KEDA) | ⭐⭐⭐⭐ Advanced | Complex production workloads |
| App Service | ❌ No | Auto (plans) | ⭐⭐ Moderate | Web applications, REST APIs |
| Azure Functions | ❌ No | Auto (Consumption) | ⭐⭐ Moderate | Event-driven functions, serverless |
Azure Container Registry (ACR)
Before deploying containers, you need to store images in a registry. ACR is Azure’s native private registry, integrated with all Azure container services.
sequenceDiagram
participant DEV as Developer
participant CODE as Source Code
participant DOCKER as Docker / ACR Build
participant ACR as Azure Container\nRegistry (ACR)
participant ACI as Azure Container\nInstance
DEV->>CODE: Write code + Dockerfile
DEV->>DOCKER: docker build -t myapp:v1 .
DOCKER-->>DEV: Image built locally
DEV->>DOCKER: docker push myregistry.azurecr.io/myapp:v1
DOCKER->>ACR: Push image
ACR-->>DEV: Image stored
DEV->>ACI: az container create --image myregistry.azurecr.io/myapp:v1
ACI->>ACR: Pull image
ACR-->>ACI: Image downloaded
ACI-->>DEV: Container running
Create and use ACR
# ===== CREATE THE REGISTRY =====
az acr create \
--resource-group my-rg \
--name myregistry \
--sku Standard \
--admin-enabled true # Required for classic ACI authentication
# ===== BUILD AND PUSH FROM AZURE (without local Docker) =====
# ACR Tasks allows building directly in the cloud
# Build from the current directory (with a Dockerfile)
az acr build \
--registry myregistry \
--image myapp:v1 \
.
# Build with a specific tag from GitHub
az acr build \
--registry myregistry \
--image myapp:$(git rev-parse --short HEAD) \
--file Dockerfile \
https://github.com/my-org/my-repo.git
# ===== LOCAL BUILD WITH DOCKER =====
# Build the image locally
docker build -t myapp:v1 .
# Tag for ACR
docker tag myapp:v1 myregistry.azurecr.io/myapp:v1
# Authenticate to ACR
az acr login --name myregistry
# Push the image
docker push myregistry.azurecr.io/myapp:v1
# ===== MANAGE IMAGES =====
# List images in ACR
az acr repository list --name myregistry --output table
# List tags for an image
az acr repository show-tags \
--name myregistry \
--repository myapp \
--output table
# Delete a tag
az acr repository delete \
--name myregistry \
--image myapp:v1-old \
--yes
# ===== ACR ADMIN CREDENTIALS =====
# Retrieve the admin username and password (for ACI)
az acr credential show --name myregistry --output table
Deploy a Container Instance
Via Azure CLI (recommended method for automation)
# ===== SIMPLE DEPLOYMENT =====
# Image from Docker Hub (public image)
az container create \
--resource-group my-rg \
--name my-container \
--image nginx:latest \
--dns-name-label my-app-dns \
--ports 80 \
--cpu 1 \
--memory 1.5 \
--restart-policy Always \
--output table
# Retrieve the application URL
az container show \
--resource-group my-rg \
--name my-container \
--query "ipAddress.fqdn" \
--output tsv
# ===== DEPLOYMENT FROM ACR =====
# Retrieve ACR credentials
ACR_USERNAME=$(az acr credential show \
--name myregistry \
--query username \
--output tsv)
ACR_PASSWORD=$(az acr credential show \
--name myregistry \
--query "passwords[0].value" \
--output tsv)
# Create the container with private image
az container create \
--resource-group my-rg \
--name my-app \
--image myregistry.azurecr.io/myapp:v1 \
--registry-login-server myregistry.azurecr.io \
--registry-username $ACR_USERNAME \
--registry-password $ACR_PASSWORD \
--dns-name-label my-app-prod \
--ports 80 443 \
--cpu 2 \
--memory 3 \
--os-type Linux \
--restart-policy Always \
--environment-variables \
NODE_ENV=production \
APP_PORT=80 \
--secure-environment-variables \
DB_PASSWORD="$(az keyvault secret show --vault-name myvault --name db-password --query value --output tsv)" \
--output table
# ===== MANAGEMENT COMMANDS =====
# View container state
az container show \
--resource-group my-rg \
--name my-app \
--output table
# View logs
az container logs \
--resource-group my-rg \
--name my-app
# Stop the container
az container stop \
--resource-group my-rg \
--name my-app
# Start the container
az container start \
--resource-group my-rg \
--name my-app
# Restart
az container restart \
--resource-group my-rg \
--name my-app
# Delete
az container delete \
--resource-group my-rg \
--name my-app \
--yes
Container Groups: Multi-container deployments
A Container Group is the fundamental ACI concept: a group of containers that share resources and lifecycle. It is the ACI equivalent of a Kubernetes Pod.
graph TD
subgraph CG["Container Group: my-app-group\nIP: 40.x.x.x"]
subgraph SHARED["Shared resources"]
CPU["4 vCPU total\n8 GB RAM total"]
NET["localhost communication"]
IP["Single public IP"]
end
C1["Container: frontend\nNginx:latest\n1 vCPU, 1.5 GB\nPort: 80"]
C2["Container: api\nmyregistry.azurecr.io/api:v1\n2 vCPU, 4 GB\nPort: 8080"]
C3["Container: sidecar\nfluentd:latest\n0.5 vCPU, 0.5 GB\nPort: 24224"]
end
INTERNET[Internet] -->|HTTP:80| IP
C1 -->|localhost:8080| C2
C2 -->|localhost:24224| C3
style CG fill:#1e3a5f,color:#fff
style SHARED fill:#0f172a,color:#fff
Shared properties in a Container Group:
| Shared resource | Description |
|---|---|
| Physical host | Same Azure machine (physical or virtual) |
| CPU and Memory | Limits configured at the group level |
| Lifecycle | Created, started, stopped, deleted together |
| Internal communication | Via localhost on any port |
| External IP address | A single IP for the entire group |
| DNS Label (FQDN) | A shared FQDN for the group |
| External volumes | Azure Files mounted in containers |
Deploying a Container Group via YAML
# container-group.yaml
# Deploy with: az container create --resource-group my-rg --file container-group.yaml
apiVersion: 2021-10-01
name: my-app-group
type: Microsoft.ContainerInstance/containerGroups
location: eastus
properties:
# Group containers
containers:
# Main container: web server
- name: frontend
properties:
image: nginx:alpine
resources:
requests:
cpu: 1.0
memoryInGb: 1.5
limits:
cpu: 2.0
memoryInGb: 3.0
ports:
- port: 80
protocol: TCP
environmentVariables:
- name: APP_ENV
value: production
livenessProbe:
httpGet:
path: /health
port: 80
initialDelaySeconds: 10
periodSeconds: 15
failureThreshold: 3
readinessProbe:
httpGet:
path: /ready
port: 80
initialDelaySeconds: 5
periodSeconds: 10
failureThreshold: 3
# Backend API container
- name: api
properties:
image: myregistry.azurecr.io/api:v1.2.0
resources:
requests:
cpu: 2.0
memoryInGb: 4.0
ports:
- port: 8080
protocol: TCP
environmentVariables:
- name: NODE_ENV
value: production
- name: DB_HOST
value: mydb.postgres.database.azure.com
- name: DB_PASSWORD
secureValue: "REPLACE_WITH_SECURE_VALUE"
volumeMounts:
- name: data-volume
mountPath: /app/data
# Sidecar container: log collection
- name: log-collector
properties:
image: fluent/fluentd:v1.16-debian-1
resources:
requests:
cpu: 0.5
memoryInGb: 0.5
environmentVariables:
- name: FLUENTD_CONF
value: fluent.conf
# Network configuration
osType: Linux
restartPolicy: Always
ipAddress:
type: Public
ports:
- protocol: TCP
port: 80
dnsNameLabel: my-app-group-2026 # Must be unique in the region
# Shared volumes
volumes:
- name: data-volume
azureFile:
shareName: myfileshare
storageAccountName: mystorageaccount
storageAccountKey: "REPLACE_WITH_STORAGE_KEY"
# Private registry credentials (if needed)
imageRegistryCredentials:
- server: myregistry.azurecr.io
username: myregistry
password: "REPLACE_WITH_ACR_PASSWORD"
Deploying a Container Group via ARM JSON
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"containerGroupName": {
"type": "string",
"defaultValue": "my-app-group"
},
"acrPassword": {
"type": "securestring"
},
"dbPassword": {
"type": "securestring"
}
},
"resources": [
{
"type": "Microsoft.ContainerInstance/containerGroups",
"apiVersion": "2021-10-01",
"name": "[parameters('containerGroupName')]",
"location": "[resourceGroup().location]",
"properties": {
"containers": [
{
"name": "webapp",
"properties": {
"image": "myregistry.azurecr.io/webapp:latest",
"resources": {
"requests": {
"cpu": 1,
"memoryInGb": 2
}
},
"ports": [
{
"port": 80,
"protocol": "TCP"
}
],
"environmentVariables": [
{
"name": "DB_PASSWORD",
"secureValue": "[parameters('dbPassword')]"
}
]
}
}
],
"imageRegistryCredentials": [
{
"server": "myregistry.azurecr.io",
"username": "myregistry",
"password": "[parameters('acrPassword')]"
}
],
"osType": "Linux",
"restartPolicy": "Always",
"ipAddress": {
"type": "Public",
"ports": [
{
"port": 80,
"protocol": "TCP"
}
],
"dnsNameLabel": "[parameters('containerGroupName')]"
}
}
}
],
"outputs": {
"containerFqdn": {
"type": "string",
"value": "[reference(parameters('containerGroupName')).ipAddress.fqdn]"
}
}
}
Rule: Use YAML to deploy containers only. Use ARM JSON if you need to deploy other Azure resources (VNet, NSG, etc.) at the same time.
Part 2 – Managing Azure Container Instances
Updating an ACI
Managing ACI updates is an important topic because not all properties can be modified in place.
flowchart TD
UPDATE{Which property\nto modify?}
UPDATE -->|Image, env vars,\ncommands, ports| YAML["Re-deploy the YAML\naz container create\n--file updated.yaml"]
UPDATE -->|OS type, CPU/RAM,\nGPU, restart policy,\nnetwork profile,\navailability zone| DELETE["1. Delete the container\n2. Recreate with\nnew specs"]
YAML --> RESULT1[Container updated\nwithout interruption*]
DELETE --> RESULT2[Service interruption\nduring recreation]
style YAML fill:#10b981,color:#fff
style DELETE fill:#f97316,color:#fff
# ===== UPDATE VIA YAML (re-deploy) =====
# The 'create' command with --file updates if the group already exists
az container create \
--resource-group my-rg \
--file container-group-updated.yaml
# ===== UPDATE IMAGE VIA CLI =====
# (Creates a new container group with the new image)
az container create \
--resource-group my-rg \
--name my-app \
--image myregistry.azurecr.io/myapp:v2.0.0 \
--registry-login-server myregistry.azurecr.io \
--registry-username $ACR_USERNAME \
--registry-password $ACR_PASSWORD \
--dns-name-label my-app-prod \
--ports 80 \
--cpu 2 \
--memory 3
# ===== STATUS CHECK =====
az container show \
--resource-group my-rg \
--name my-app \
--query "{Status:instanceView.state, Image:containers[0].image, IP:ipAddress.ip}" \
--output table
Scaling ACI
ACI has no native horizontal scaling. Here are the available options:
graph LR
subgraph NATIVE["Native ACI scaling"]
VERT["Vertical Scaling\n(Modify CPU/RAM)\nRequires recreation"]
end
subgraph CUSTOM["Custom scaling"]
MULTI["Multiple instances\n(deployed manually)"]
MONITOR["Azure Monitor\n+ Automation/Functions\nto trigger creation"]
LOGAPP["Logic Apps\nScheduled scaling"]
end
subgraph ALTERNATIVE["Recommended alternatives"]
AKS["AKS\nHorizontal Pod Autoscaler"]
ACA["Container Apps\nKEDA autoscaling"]
end
NATIVE -.->|"Limitation: service interruption"| CUSTOM
CUSTOM -.->|"Limitation: complexity"| ALTERNATIVE
style NATIVE fill:#f97316,color:#fff
style CUSTOM fill:#3b82f6,color:#fff
style ALTERNATIVE fill:#10b981,color:#fff
Recommendation: If your workload requires automatic horizontal scaling, evaluate Azure Container Apps or AKS. ACI is optimized for single-instance workloads or batch deployments.
Health Probes
Probes are mechanisms to check the state of the container. They allow Azure to know whether your application is ready to receive traffic and whether it continues to function correctly.
sequenceDiagram
participant ACI as Azure (ACI Orchestrator)
participant APP as Container Application
ACI->>APP: Container created
Note over APP: Bootstrap in progress (loading state)
loop Readiness Probe (is it ready?)
ACI->>APP: GET /ready ?
APP-->>ACI: 503 Service Unavailable
Note over ACI: Container not yet ready
end
APP-->>ACI: 200 OK (application started)
Note over ACI: Container marked READY\nCan receive traffic
loop Liveness Probe (is it alive?) - continuous
ACI->>APP: GET /health ?
APP-->>ACI: 200 OK (alive)
end
APP-->>ACI: Timeout (5xx or no response)
Note over ACI: Liveness Probe FAIL
ACI->>APP: Restart Container (per restart policy)
Complete probe configuration in YAML:
# Probes configuration in a Container Group YAML
containers:
- name: my-app
properties:
image: myregistry.azurecr.io/myapp:v1
resources:
requests:
cpu: 1
memoryInGb: 2
# Liveness Probe: "Is the container still alive?"
livenessProbe:
httpGet:
path: /health/live # Health check endpoint
port: 80
httpHeaders:
- name: X-Health-Check
value: liveness
initialDelaySeconds: 30 # Wait 30s before starting
periodSeconds: 15 # Check every 15 seconds
timeoutSeconds: 5 # Request timeout
successThreshold: 1 # 1 success to pass to OK
failureThreshold: 3 # 3 failures before restarting
# Readiness Probe: "Is the container ready to receive traffic?"
readinessProbe:
httpGet:
path: /health/ready
port: 80
initialDelaySeconds: 5 # Shorter initial delay (check quickly)
periodSeconds: 10
timeoutSeconds: 3
successThreshold: 1
failureThreshold: 3
# Probe via exec command (alternative to HTTP)
# livenessProbe:
# exec:
# command:
# - /bin/sh
# - -c
# - "curl -f http://localhost:80/health || exit 1"
# initialDelaySeconds: 30
# periodSeconds: 15
Restart Policies
| Policy | Description | Use case |
|---|---|---|
| Always | Restarts if the container stops or fails | Continuous web services |
| Never | Never restarts | One-shot tasks, batch jobs |
| OnFailure | Restarts only on failure (exit code ≠ 0) | Jobs that can succeed or fail |
Environment variables and secrets
# Environment variables in YAML
environmentVariables:
# Regular variable (visible in logs and portal)
- name: APP_ENV
value: production
- name: APP_PORT
value: "8080"
- name: LOG_LEVEL
value: info
# Secret variable (hidden in portal and logs)
- name: DB_PASSWORD
secureValue: "MySecurePassword123!"
- name: API_SECRET_KEY
secureValue: "sk-abc123xyz789"
- name: JWT_SECRET
secureValue: "superJWTsecretValue"
⚠️ Important:
secureValueentries are hidden in the Azure portal and API responses, but they are still stored in the Container Group definition. In production, use Azure Key Vault + Managed Identity for maximum security.
Retrieving secrets from Key Vault via CLI before deployment:
# Secure deployment script retrieving secrets from Key Vault
VAULT_NAME="mykeyvault"
RESOURCE_GROUP="my-rg"
# Retrieve secrets from Key Vault
DB_PASSWORD=$(az keyvault secret show \
--vault-name $VAULT_NAME \
--name db-password \
--query value \
--output tsv)
API_KEY=$(az keyvault secret show \
--vault-name $VAULT_NAME \
--name api-key \
--query value \
--output tsv)
# Deploy with retrieved secrets
az container create \
--resource-group $RESOURCE_GROUP \
--name my-app \
--image myregistry.azurecr.io/myapp:v1 \
--secure-environment-variables \
"DB_PASSWORD=$DB_PASSWORD" \
"API_KEY=$API_KEY" \
--dns-name-label my-app \
--ports 80
# Clear local variables
unset DB_PASSWORD API_KEY
Start / Stop / Restart and Exec
# ===== LIFECYCLE =====
# Stop (releases compute, preserves configuration)
az container stop \
--resource-group my-rg \
--name my-app
# Start
az container start \
--resource-group my-rg \
--name my-app
# Restart
az container restart \
--resource-group my-rg \
--name my-app
# ===== EXECUTE COMMANDS INSIDE THE CONTAINER =====
# Open an interactive shell
az container exec \
--resource-group my-rg \
--name my-app \
--container-name my-app \
--exec-command "/bin/sh"
# Run a non-interactive command
az container exec \
--resource-group my-rg \
--name my-app \
--exec-command "ls -la /app"
# ===== LOGS =====
# View logs in real time
az container logs \
--resource-group my-rg \
--name my-app \
--follow
# Logs from a specific container in a group
az container logs \
--resource-group my-rg \
--name my-app-group \
--container-name frontend
# ===== DETAILED INFORMATION =====
# View all events and container state
az container show \
--resource-group my-rg \
--name my-app \
--query "{
Status: instanceView.state,
Image: containers[0].image,
IP: ipAddress.ip,
FQDN: ipAddress.fqdn,
Events: containers[0].instanceView.events
}" \
--output json
Part 3 – Integrating ACI with Azure Services
ACI and Virtual Networks
Deploying ACI in a Virtual Network allows establishing private and secure connections with other Azure resources.
graph TD
subgraph INTERNET["Internet"]
USER[Users]
end
subgraph AZURE_VNET["Azure VNet (10.0.0.0/16)"]
subgraph APP_SUBNET["App Subnet (10.0.1.0/24)"]
ACI["ACI - Container\n(IP: 10.0.1.4)"]
end
subgraph AGW_SUBNET["AppGW Subnet (10.0.2.0/24)"]
AGW["Application Gateway\n(Public IP)"]
end
subgraph DB_SUBNET["DB Subnet (10.0.3.0/24)"]
SQL["Azure SQL Database\n(IP: 10.0.3.5)"]
end
end
USER -->|HTTPS| AGW
AGW -->|"HTTP (backend pool)"| ACI
ACI -->|"Port 1433 (private)"| SQL
style INTERNET fill:#1e293b,color:#fff
style AZURE_VNET fill:#1e3a5f,color:#fff
style AGW fill:#10b981,color:#fff
style ACI fill:#f59e0b,color:#000
style SQL fill:#3b82f6,color:#fff
ACI networking types:
| Type | Public IP | VNet | Use case |
|---|---|---|---|
| Public | ✅ Yes | ❌ No | Direct access from internet |
| Private | ❌ No | ✅ Yes | Private internal connections |
| None | ❌ No | ❌ No | Containers without network connectivity |
# ===== CREATE ACI IN A VNET =====
# 1. Create the VNet and subnet
az network vnet create \
--resource-group my-rg \
--name my-vnet \
--address-prefix "10.0.0.0/16" \
--subnet-name aci-subnet \
--subnet-prefix "10.0.1.0/24"
# 2. Delegate the subnet to ACI (required)
az network vnet subnet update \
--resource-group my-rg \
--vnet-name my-vnet \
--name aci-subnet \
--delegations "Microsoft.ContainerInstance/containerGroups"
# 3. Create the ACI in the VNet
az container create \
--resource-group my-rg \
--name aci-private \
--image myregistry.azurecr.io/myapp:v1 \
--vnet my-vnet \
--subnet aci-subnet \
--ports 80 \
--ip-address Private \
--restart-policy Always
# 4. Verify the assigned private IP
az container show \
--resource-group my-rg \
--name aci-private \
--query "ipAddress.ip" \
--output tsv
ACI with Application Gateway
The Application Gateway allows publicly exposing an ACI deployed in private mode (without a direct public IP).
# ===== FULL SETUP ACI + APPLICATION GATEWAY =====
RESOURCE_GROUP="my-rg"
LOCATION="eastus"
VNET_NAME="my-vnet"
# 1. Create the VNet with two subnets
az network vnet create \
--resource-group $RESOURCE_GROUP \
--name $VNET_NAME \
--address-prefix "10.0.0.0/16" \
--subnet-name aci-subnet \
--subnet-prefix "10.0.1.0/24" \
--location $LOCATION
# Add subnet for Application Gateway
az network vnet subnet create \
--resource-group $RESOURCE_GROUP \
--vnet-name $VNET_NAME \
--name appgw-subnet \
--address-prefix "10.0.2.0/24"
# 2. Delegate the ACI subnet
az network vnet subnet update \
--resource-group $RESOURCE_GROUP \
--vnet-name $VNET_NAME \
--name aci-subnet \
--delegations "Microsoft.ContainerInstance/containerGroups"
# 3. Create the private ACI
ACI_IP=$(az container create \
--resource-group $RESOURCE_GROUP \
--name my-app-private \
--image nginx:alpine \
--vnet $VNET_NAME \
--subnet aci-subnet \
--ports 80 \
--ip-address Private \
--query "ipAddress.ip" \
--output tsv)
echo "ACI IP: $ACI_IP"
# 4. Create a public IP for Application Gateway
az network public-ip create \
--resource-group $RESOURCE_GROUP \
--name appgw-pip \
--sku Standard \
--allocation-method Static
# 5. Create Application Gateway
az network application-gateway create \
--resource-group $RESOURCE_GROUP \
--name my-app-gateway \
--location $LOCATION \
--sku Standard_v2 \
--capacity 2 \
--vnet-name $VNET_NAME \
--subnet appgw-subnet \
--public-ip-address appgw-pip \
--frontend-port 80 \
--http-settings-port 80 \
--http-settings-protocol Http \
--routing-rule-type Basic \
--servers $ACI_IP \
--priority 100
echo "Application Gateway created. Traffic: Internet → AppGW → ACI (private)"
ACI with Azure Storage (Persistent Volumes)
Containers are ephemeral by nature: if a container is restarted, data written to its local filesystem is lost. Azure Files allows mounting a persistent file share inside the container.
graph LR
subgraph ACI1["ACI Instance 1\n(running)"]
APP1["Application"]
MNT1["/data\n(mounted)"]
end
subgraph ACI2["ACI Instance 2\n(replacement after crash)"]
APP2["Application"]
MNT2["/data\n(mounted)"]
end
subgraph STORAGE["Azure Storage"]
FILESHARE["Azure File Share\n(persistent data)"]
end
APP1 -->|Writes file| MNT1
MNT1 <-->|Bidirectional sync| FILESHARE
MNT2 <-->|Bidirectional sync| FILESHARE
APP2 -->|Reads file| MNT2
ACI1 -->|Container crash| ACI2
style FILESHARE fill:#3b82f6,color:#fff
#!/bin/bash
# setup-aci-with-storage.sh
RESOURCE_GROUP="my-rg"
STORAGE_ACCOUNT="myacistorage$(openssl rand -hex 4)"
FILE_SHARE_NAME="myfileshare"
LOCATION="eastus"
echo "=== Creating Storage Account ==="
az storage account create \
--name $STORAGE_ACCOUNT \
--resource-group $RESOURCE_GROUP \
--location $LOCATION \
--sku Standard_LRS \
--kind StorageV2
echo "=== Creating File Share ==="
az storage share create \
--name $FILE_SHARE_NAME \
--account-name $STORAGE_ACCOUNT \
--quota 100 # 100 GB maximum
echo "=== Retrieving storage key ==="
STORAGE_KEY=$(az storage account keys list \
--account-name $STORAGE_ACCOUNT \
--resource-group $RESOURCE_GROUP \
--query "[0].value" \
--output tsv)
echo "=== Deploying ACI with volume ==="
az container create \
--resource-group $RESOURCE_GROUP \
--name aci-with-storage \
--image mcr.microsoft.com/azuredocs/aci-helloworld \
--dns-name-label aci-storage-demo-$(openssl rand -hex 4) \
--ports 80 \
--azure-file-volume-account-name $STORAGE_ACCOUNT \
--azure-file-volume-account-key $STORAGE_KEY \
--azure-file-volume-share-name $FILE_SHARE_NAME \
--azure-file-volume-mount-path /data
echo "=== Verifying the mount ==="
# Execute a command in the container to verify the mount
az container exec \
--resource-group $RESOURCE_GROUP \
--name aci-with-storage \
--exec-command "ls -la /data"
echo "=== Writing a test file to the volume ==="
az container exec \
--resource-group $RESOURCE_GROUP \
--name aci-with-storage \
--exec-command "sh -c 'echo \"Hello from ACI\" > /data/test.txt'"
echo "=== Checking the file in Azure Storage ==="
az storage file list \
--share-name $FILE_SHARE_NAME \
--account-name $STORAGE_ACCOUNT \
--output table
echo ""
echo "Data in /data is persistent!"
echo "Even if the ACI is restarted or recreated, files will remain in Azure Files."
ACI with Azure Functions: Dynamic container creation
# function_app.py - Azure Function that creates ACI instances on demand
# HTTP trigger: POST /api/create-container
import azure.functions as func
import json
import logging
from azure.identity import DefaultAzureCredential
from azure.mgmt.containerinstance import ContainerInstanceManagementClient
from azure.mgmt.containerinstance.models import (
ContainerGroup,
Container,
ContainerGroupNetworkProtocol,
ContainerPort,
EnvironmentVariable,
IpAddress,
Port,
ResourceRequests,
ResourceRequirements,
OperatingSystemTypes,
ContainerGroupRestartPolicy
)
import os
import uuid
app = func.FunctionApp()
@app.function_name(name="CreateContainerInstance")
@app.route(route="create-container", methods=["POST"])
def create_container(req: func.HttpRequest) -> func.HttpResponse:
"""
Creates an Azure Container Instance on demand.
Expected JSON body:
{
"name": "my-container",
"image": "nginx:latest",
"cpu": 1.0,
"memory": 1.5
}
"""
logging.info("CreateContainerInstance function triggered")
try:
req_body = req.get_json()
except ValueError:
return func.HttpResponse(
"Invalid JSON request body",
status_code=400
)
# Required parameters
container_name = req_body.get("name", f"aci-{str(uuid.uuid4())[:8]}")
image = req_body.get("image", "nginx:latest")
cpu = float(req_body.get("cpu", 1.0))
memory = float(req_body.get("memory", 1.5))
# Configuration from Function App environment variables
subscription_id = os.environ["AZURE_SUBSCRIPTION_ID"]
resource_group = os.environ["RESOURCE_GROUP_NAME"]
location = os.environ.get("LOCATION", "eastus")
# Authentication via Function App Managed Identity
credential = DefaultAzureCredential()
client = ContainerInstanceManagementClient(credential, subscription_id)
# Container definition
container = Container(
name=container_name,
image=image,
resources=ResourceRequirements(
requests=ResourceRequests(
memory_in_gb=memory,
cpu=cpu
)
),
ports=[ContainerPort(port=80)],
environment_variables=[
EnvironmentVariable(name="CREATED_BY", value="azure-function"),
EnvironmentVariable(name="CREATION_TIME", value=str(func.utcnow()))
]
)
# Container Group definition
container_group = ContainerGroup(
location=location,
containers=[container],
os_type=OperatingSystemTypes.LINUX,
restart_policy=ContainerGroupRestartPolicy.ALWAYS,
ip_address=IpAddress(
ports=[Port(protocol=ContainerGroupNetworkProtocol.TCP, port=80)],
type="Public",
dns_name_label=container_name
)
)
try:
# Create the Container Group
poller = client.container_groups.begin_create_or_update(
resource_group_name=resource_group,
container_group_name=container_name,
container_group=container_group
)
result = poller.result()
response_data = {
"status": "created",
"containerName": result.name,
"fqdn": result.ip_address.fqdn if result.ip_address else None,
"ip": result.ip_address.ip if result.ip_address else None,
"provisioningState": result.provisioning_state
}
logging.info(f"Container created: {result.name}")
return func.HttpResponse(
json.dumps(response_data),
mimetype="application/json",
status_code=201
)
except Exception as e:
logging.error(f"Error creating container: {str(e)}")
return func.HttpResponse(
json.dumps({"error": str(e)}),
mimetype="application/json",
status_code=500
)
Managed Identity for ACI: Secure access to Azure resources
#!/bin/bash
# setup-aci-managed-identity.sh
RESOURCE_GROUP="my-rg"
LOCATION="eastus"
IDENTITY_NAME="aci-managed-identity"
KEY_VAULT_NAME="my-keyvault-$(openssl rand -hex 4)"
SECRET_NAME="mysupersecret"
SECRET_VALUE="supermariosecret"
echo "=== STEP 1: Create the User-Assigned Managed Identity ==="
az identity create \
--resource-group $RESOURCE_GROUP \
--name $IDENTITY_NAME \
--location $LOCATION
# Retrieve the necessary IDs
IDENTITY_ID=$(az identity show \
--resource-group $RESOURCE_GROUP \
--name $IDENTITY_NAME \
--query id \
--output tsv)
IDENTITY_CLIENT_ID=$(az identity show \
--resource-group $RESOURCE_GROUP \
--name $IDENTITY_NAME \
--query clientId \
--output tsv)
IDENTITY_PRINCIPAL_ID=$(az identity show \
--resource-group $RESOURCE_GROUP \
--name $IDENTITY_NAME \
--query principalId \
--output tsv)
echo "Identity Resource ID: $IDENTITY_ID"
echo "Identity Client ID: $IDENTITY_CLIENT_ID"
echo ""
echo "=== STEP 2: Create the Key Vault and secret ==="
az keyvault create \
--name $KEY_VAULT_NAME \
--resource-group $RESOURCE_GROUP \
--location $LOCATION \
--enable-rbac-authorization false # Use Access Policies here
# Give the current user admin access to the KV
CURRENT_USER_ID=$(az ad signed-in-user show --query id --output tsv)
az keyvault set-policy \
--name $KEY_VAULT_NAME \
--object-id $CURRENT_USER_ID \
--secret-permissions get list set delete
# Create the secret
az keyvault secret set \
--vault-name $KEY_VAULT_NAME \
--name $SECRET_NAME \
--value $SECRET_VALUE
echo ""
echo "=== STEP 3: Grant Key Vault access to the Managed Identity ==="
az keyvault set-policy \
--name $KEY_VAULT_NAME \
--object-id $IDENTITY_PRINCIPAL_ID \
--secret-permissions get list
echo ""
echo "=== STEP 4: Create ACI with the Managed Identity ==="
az container create \
--resource-group $RESOURCE_GROUP \
--name aci-with-identity \
--image mcr.microsoft.com/azure-cli \
--os-type Linux \
--cpu 1 \
--memory 1 \
--assign-identity $IDENTITY_ID \
--command-line "tail -f /dev/null" # Keep container active
echo ""
echo "=== STEP 5: Read the secret from the container ==="
# Wait for the container to be ready
sleep 30
az container exec \
--resource-group $RESOURCE_GROUP \
--name aci-with-identity \
--exec-command "sh -c \"
# Login with the Managed Identity
az login --identity --username $IDENTITY_CLIENT_ID
# Read the secret from Key Vault
SECRET=\$(az keyvault secret show \
--vault-name $KEY_VAULT_NAME \
--name $SECRET_NAME \
--query value \
--output tsv)
echo 'Secret retrieved: '$SECRET
\""
echo ""
echo "=== Setup complete ==="
echo "The ACI can now access Key Vault secrets via its Managed Identity."
echo "No credentials stored in the container!"
Securing HTTPS with Nginx and SSL certificates
# aci-nginx-https.yaml - ACI with Nginx performing SSL termination
apiVersion: 2021-10-01
name: aci-nginx-https
type: Microsoft.ContainerInstance/containerGroups
location: eastus
properties:
containers:
- name: nginx-ssl
properties:
image: nginx:alpine
resources:
requests:
cpu: 0.5
memoryInGb: 0.5
ports:
- port: 443
protocol: TCP
- port: 80
protocol: TCP
volumeMounts:
- name: nginx-config
mountPath: /etc/nginx/conf.d
- name: ssl-certs
mountPath: /etc/nginx/ssl
- name: webapp
properties:
image: myregistry.azurecr.io/webapp:v1
resources:
requests:
cpu: 1.0
memoryInGb: 1.5
ports:
- port: 8080
osType: Linux
restartPolicy: Always
ipAddress:
type: Public
ports:
- protocol: TCP
port: 443
- protocol: TCP
port: 80
dnsNameLabel: my-app-https
volumes:
- name: nginx-config
secret:
nginx.conf: <BASE64_ENCODED_NGINX_CONF>
- name: ssl-certs
secret:
ssl.crt: <BASE64_ENCODED_CERT>
ssl.key: <BASE64_ENCODED_KEY>
# Script to generate certificates and deploy
# 1. Generate a self-signed certificate (development only)
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout ssl.key \
-out ssl.crt \
-subj "/C=US/ST=Washington/L=Seattle/O=MyCompany/CN=*.my-app.azurecontainer.io"
# 2. Encode in Base64
SSL_CRT_B64=$(base64 -w 0 ssl.crt)
SSL_KEY_B64=$(base64 -w 0 ssl.key)
# 3. Create the Nginx configuration
cat > nginx.conf << 'EOF'
server {
listen 80;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
ssl_certificate /etc/nginx/ssl/ssl.crt;
ssl_certificate_key /etc/nginx/ssl/ssl.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
location / {
proxy_pass http://localhost:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto https;
}
}
EOF
NGINX_CONF_B64=$(base64 -w 0 nginx.conf)
echo "Base64 encoding complete."
echo "Replace the placeholders in the YAML and deploy with:"
echo "az container create --resource-group my-rg --file aci-nginx-https.yaml"
Part 4 – Optimizing Performance and Costs
Understanding the ACI billing model
ACI follows a pay-as-you-go model: you pay exactly for what you consume, to the second.
pie title Typical ACI cost factors
"Compute (vCPU)" : 45
"Memory (GB)" : 35
"Network (egress)" : 15
"Storage (volumes)" : 5
Breakdown of cost factors:
| Factor | Billing unit | Cost impact |
|---|---|---|
| vCPU | vCPU-second | $$$ - Primary factor |
| Memory | GB-second | $$ - Second factor |
| Outbound network | GB of outgoing data | $ - Variable |
| Azure Files volumes | GB-month | $ - If used |
| ACR images | GB stored-month | $ - Marginal |
| Azure region | Multiplied by region price | Varies from -30% to +40% |
Key optimization: Stop or delete containers as soon as they are no longer needed. A stopped container (Stopped state) does not charge for compute.
Resource optimization techniques
flowchart TD
START[Container deployed]
MONITOR[Monitor usage\nAzure Monitor + Metrics]
MONITOR --> OVER{Over-consumption?}
MONITOR --> UNDER{Under-utilization?}
OVER -->|CPU throttling\nOOM kills| INCREASE["Increase\nCPU/RAM"]
UNDER -->|< 30% utilization| DECREASE["Reduce\nCPU/RAM"]
INCREASE --> REDEPLOY[Redeploy\nwith new specs]
DECREASE --> REDEPLOY
REDEPLOY --> MONITOR
style START fill:#3b82f6,color:#fff
style MONITOR fill:#10b981,color:#fff
style OVER fill:#ef4444,color:#fff
style UNDER fill:#f59e0b,color:#000
Script to deploy an ACI with configurable resources:
#!/bin/bash
# deploy-aci-with-sizing.sh
RESOURCE_GROUP="my-rg"
CONTAINER_NAME="my-app-optimized"
IMAGE="myregistry.azurecr.io/myapp:v1"
# ===== RECOMMENDED SIZES BY WORKLOAD =====
# Small size: lightweight API, simple batch job
CPU_SMALL=0.5
MEM_SMALL=1.0
# Standard size: typical web application
CPU_STANDARD=1.0
MEM_STANDARD=2.0
# Large size: intensive application
CPU_LARGE=2.0
MEM_LARGE=4.0
# ===== DEPLOY WITH STANDARD SIZE =====
az container create \
--resource-group $RESOURCE_GROUP \
--name $CONTAINER_NAME \
--image $IMAGE \
--cpu $CPU_STANDARD \
--memory $MEM_STANDARD \
--dns-name-label $CONTAINER_NAME \
--ports 80 \
--restart-policy Always
echo "Container deployed with $CPU_STANDARD vCPU and $MEM_STANDARD GB RAM"
echo ""
echo "To monitor resource usage:"
echo "1. Go to Azure Portal → Container Instances → $CONTAINER_NAME → Metrics"
echo "2. Or use the CLI command below:"
echo ""
echo "az monitor metrics list \\"
echo " --resource \$(az container show --resource-group $RESOURCE_GROUP --name $CONTAINER_NAME --query id --output tsv) \\"
echo " --metric 'CpuUsage,MemoryUsage' \\"
echo " --interval PT1M \\"
echo " --output table"
Spot Instances ACI
Spot Instances allow using unused Azure capacity at a reduced rate, ideal for non-critical workloads.
# ===== DEPLOY A SPOT ACI =====
# NOTE: Default quota is 0 - Microsoft support request required
# Check Spot quota in your region
az vm list-usage \
--location eastus \
--query "[?contains(name.value, 'containerInstances')]" \
--output table
# Once quota is granted, deploy a spot container
az container create \
--resource-group my-rg \
--name my-app-spot \
--image nginx:latest \
--cpu 1 \
--memory 1 \
--location eastus2 \
--priority Spot \
--restart-policy Never # Spots can be interrupted
Important: Spot Instances can be interrupted by Azure without notice. Use them only for interruption-tolerant workloads (batch jobs, tests, dev environments).
Part 5 – Monitoring and Securing ACI
Available metrics in ACI
graph TD
subgraph METRICS["ACI Metrics"]
CPU["CPU Utilization\n(Millicores)"]
MEM["Memory Utilization\n(Bytes)"]
NET_IN["Network Bytes Received\n(Bytes/sec)"]
NET_OUT["Network Bytes Transmitted\n(Bytes/sec)"]
end
subgraph AGGREGATIONS["Aggregations"]
AVG["Average - General trend"]
MAX["Maximum - Load peaks"]
MIN["Minimum - Minimum usage"]
end
METRICS --> AGGREGATIONS
subgraph USE["USE Method"]
UTIL["Utilization\n(how much is consumed?)"]
SAT["Saturation\n(is it under pressure?)"]
ERR["Errors\n(is it failing?)"]
end
subgraph RED["RED Method"]
RATE["Rate\n(requests/sec)"]
ERRORS["Errors\n(% of errors)"]
DUR["Duration\n(response time)"]
end
CPU --> UTIL
MEM --> UTIL
NET_IN --> RATE
Azure Monitor configuration for ACI
# ===== CREATE A LOG ANALYTICS WORKSPACE =====
LOG_WORKSPACE_ID=$(az monitor log-analytics workspace create \
--resource-group my-rg \
--workspace-name aci-monitoring \
--sku PerGB2018 \
--query customerId \
--output tsv)
LOG_WORKSPACE_KEY=$(az monitor log-analytics workspace get-shared-keys \
--resource-group my-rg \
--workspace-name aci-monitoring \
--query primarySharedKey \
--output tsv)
# ===== DEPLOY ACI WITH LOG ANALYTICS =====
az container create \
--resource-group my-rg \
--name my-app-monitored \
--image myregistry.azurecr.io/myapp:v1 \
--cpu 1 \
--memory 1.5 \
--dns-name-label my-app-monitored \
--ports 80 \
--log-analytics-workspace $LOG_WORKSPACE_ID \
--log-analytics-workspace-key $LOG_WORKSPACE_KEY
echo "Container deployed with integrated monitoring"
echo ""
echo "Available tables in Log Analytics:"
echo "- ContainerEvent_CL: container events (start, stop, restart)"
echo "- ContainerInstanceLog_CL: application logs (stdout)"
KQL queries to analyze ACI logs:
// View all recent container events
ContainerEvent_CL
| where TimeGenerated > ago(1h)
| project TimeGenerated, ContainerGroupName_s, Name_s, Message
| order by TimeGenerated desc
// Application logs (stdout)
ContainerInstanceLog_CL
| where TimeGenerated > ago(1h)
| where ContainerGroup_s == "my-app-monitored"
| project TimeGenerated, Message, ContainerName_s
| order by TimeGenerated desc
// Count HTTP 5xx errors in logs
ContainerInstanceLog_CL
| where Message contains "5" and Message contains "HTTP"
| summarize ErrorCount = count() by bin(TimeGenerated, 5m)
| render timechart
// Restart events
ContainerEvent_CL
| where Message contains "restart" or Message contains "Pulled"
| project TimeGenerated, ContainerGroupName_s, Message
| order by TimeGenerated desc
Creating Azure Monitor alerts
# ===== CREATE A CPU ALERT =====
# Retrieve the container ID
ACI_ID=$(az container show \
--resource-group my-rg \
--name my-app-monitored \
--query id \
--output tsv)
# Create an action group (email notification)
az monitor action-group create \
--resource-group my-rg \
--name aci-alerts-group \
--short-name aci-alerts \
--action email admin admin@company.com
ACTION_GROUP_ID=$(az monitor action-group show \
--resource-group my-rg \
--name aci-alerts-group \
--query id \
--output tsv)
# Create a CPU > 80% alert
az monitor alert create \
--resource-group my-rg \
--name "ACI-CPU-High" \
--target $ACI_ID \
--condition "avg CpuUsage > 800" \
--description "CPU utilization > 80% on my-app" \
--window-size 5m \
--evaluation-frequency 1m \
--action $ACTION_GROUP_ID \
--severity 2
# Create a memory > 90% alert
az monitor alert create \
--resource-group my-rg \
--name "ACI-Memory-High" \
--target $ACI_ID \
--condition "avg MemoryUsage > 1.35GB" \
--description "Memory > 90% of 1.5 GB on my-app" \
--window-size 5m \
--evaluation-frequency 1m \
--action $ACTION_GROUP_ID \
--severity 2
echo "Alerts configured:"
echo "- CPU > 80% → Email admin"
echo "- Memory > 90% → Email admin"
Cost management and budgets
# ===== CONFIGURE AN AZURE BUDGET =====
# Create a monthly budget with 90% alert
az consumption budget create \
--budget-name "ACI-Monthly-Budget" \
--amount 100 \
--time-grain Monthly \
--start-date "2026-01-01" \
--end-date "2027-12-31" \
--category Cost \
--notification-thresholds 50 90 100 \
--contact-emails admin@company.com
# ===== ANALYZE COSTS =====
# View costs by resource group for the current month
START=$(date -d "$(date +%Y-%m-01)" +%Y-%m-%d)
END=$(date +%Y-%m-%d)
az consumption usage list \
--start-date $START \
--end-date $END \
--query "[?contains(instanceName, 'aci')].{Resource:instanceName, Cost:pretaxCost, Currency:currency}" \
--output table
# ===== ENABLE AZURE ADVISOR RECOMMENDATIONS =====
# View cost recommendations for containers
az advisor recommendation list \
--category Cost \
--output table
Complete Code Examples
Optimized Dockerfile for ACI
# Multi-stage build to reduce image size
# Stage 1: Build
FROM node:20-alpine AS builder
WORKDIR /app
# Copy dependency files
COPY package*.json ./
RUN npm ci --only=production
# Copy source code
COPY . .
RUN npm run build
# Stage 2: Production (minimal image)
FROM node:20-alpine AS production
# Update security packages
RUN apk update && apk upgrade && apk add --no-cache \
curl \
&& rm -rf /var/cache/apk/*
# Create a non-root user
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001
WORKDIR /app
# Copy only what is necessary
COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist
COPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modules
USER nodejs
EXPOSE 3000
# Health check embedded in Dockerfile
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD curl -f http://localhost:3000/health || exit 1
CMD ["node", "dist/server.js"]
Complete ACI deployment script with monitoring
#!/bin/bash
# deploy-aci-complete.sh
set -euo pipefail
# ===== CONFIGURATION =====
RESOURCE_GROUP="production-rg"
LOCATION="eastus"
CONTAINER_NAME="my-app-prod"
ACR_NAME="myregistry"
IMAGE_TAG="v1.2.3"
DNS_LABEL="my-app-$(openssl rand -hex 4)"
VAULT_NAME="my-keyvault"
echo "=== Complete ACI deployment ==="
echo "Container: $CONTAINER_NAME"
echo "Image: $IMAGE_TAG"
# 1. Retrieve ACR credentials
echo "[1/6] Retrieving ACR credentials..."
ACR_USERNAME=$(az acr credential show --name $ACR_NAME --query username --output tsv)
ACR_PASSWORD=$(az acr credential show --name $ACR_NAME --query "passwords[0].value" --output tsv)
# 2. Retrieve secrets from Key Vault
echo "[2/6] Retrieving Key Vault secrets..."
DB_PASSWORD=$(az keyvault secret show \
--vault-name $VAULT_NAME \
--name db-password \
--query value \
--output tsv)
JWT_SECRET=$(az keyvault secret show \
--vault-name $VAULT_NAME \
--name jwt-secret \
--query value \
--output tsv)
# 3. Retrieve storage key (if volume required)
echo "[3/6] Retrieving storage key..."
STORAGE_KEY=$(az storage account keys list \
--account-name mystorageaccount \
--resource-group $RESOURCE_GROUP \
--query "[0].value" \
--output tsv)
# 4. Deploy the container
echo "[4/6] Deploying container..."
az container create \
--resource-group $RESOURCE_GROUP \
--name $CONTAINER_NAME \
--image $ACR_NAME.azurecr.io/myapp:$IMAGE_TAG \
--registry-login-server $ACR_NAME.azurecr.io \
--registry-username $ACR_USERNAME \
--registry-password $ACR_PASSWORD \
--dns-name-label $DNS_LABEL \
--ports 80 443 \
--cpu 2 \
--memory 4 \
--os-type Linux \
--restart-policy Always \
--environment-variables \
NODE_ENV=production \
APP_PORT=80 \
LOG_LEVEL=info \
--secure-environment-variables \
"DB_PASSWORD=$DB_PASSWORD" \
"JWT_SECRET=$JWT_SECRET" \
--azure-file-volume-account-name mystorageaccount \
--azure-file-volume-account-key $STORAGE_KEY \
--azure-file-volume-share-name uploads \
--azure-file-volume-mount-path /app/uploads \
--log-analytics-workspace "workspace-id" \
--log-analytics-workspace-key "workspace-key"
# 5. Verify the deployment
echo "[5/6] Verifying deployment..."
sleep 30
STATUS=$(az container show \
--resource-group $RESOURCE_GROUP \
--name $CONTAINER_NAME \
--query "instanceView.state" \
--output tsv)
if [ "$STATUS" != "Running" ]; then
echo "[ERROR] Container not started! Status: $STATUS"
echo "Logs:"
az container logs --resource-group $RESOURCE_GROUP --name $CONTAINER_NAME
exit 1
fi
# 6. Display connection information
echo "[6/6] Deployment successful!"
FQDN=$(az container show \
--resource-group $RESOURCE_GROUP \
--name $CONTAINER_NAME \
--query "ipAddress.fqdn" \
--output tsv)
echo ""
echo "=== Deployment information ==="
echo "Status: $STATUS"
echo "FQDN: $FQDN"
echo "URL: http://$FQDN"
echo ""
echo "Useful commands:"
echo " Logs: az container logs --resource-group $RESOURCE_GROUP --name $CONTAINER_NAME"
echo " Shell: az container exec --resource-group $RESOURCE_GROUP --name $CONTAINER_NAME --exec-command /bin/sh"
echo " Stop: az container stop --resource-group $RESOURCE_GROUP --name $CONTAINER_NAME"
# Clear sensitive variables
unset DB_PASSWORD JWT_SECRET ACR_PASSWORD STORAGE_KEY
Comparison Tables
ACI vs AKS vs Container Apps vs App Service
| Criterion | ACI | AKS | Container Apps | App Service |
|---|---|---|---|---|
| Orchestration | ❌ None | ✅ Full Kubernetes | ✅ Basic (Dapr/KEDA) | ❌ None |
| Horizontal Scaling | ❌ Manual | ✅ HPA/KEDA/VPA | ✅ Scale-to-zero | ✅ Auto |
| Operational complexity | ⭐ Very low | ⭐⭐⭐⭐ High | ⭐⭐ Low | ⭐⭐ Low |
| Startup time | ⭐⭐ 30-60 sec | ⭐⭐⭐ Seconds (pods) | ⭐⭐⭐ Seconds | ⭐⭐ Variable |
| Pricing | Pay per use (seconds) | VM nodes + Managed | Pay per use | Monthly plan |
| Persistent volumes | ✅ Azure Files | ✅ Multi-options | ✅ Azure Files | ✅ Built-in |
| Networking | VNet, Public, None | Full VNet | VNet, Ingress | VNet Integration |
| Windows | ✅ Yes | ✅ Yes | ❌ Linux only | ✅ Yes |
| Multi-container | ✅ Container Groups | ✅ Pods | ✅ Sidecars | ❌ No |
| Primary use case | Batch jobs, bursting | Prod microservices | Event-driven, APIs | Web applications |
Comparison of storage types for ACI
| Type | Persistence | Performance | Sharing | ACI use case |
|---|---|---|---|---|
| Container filesystem | ❌ Ephemeral | ⭐⭐⭐ Fast | ❌ No | Temporary cache, temp files |
| Azure Files (SMB) | ✅ Persistent | ⭐⭐ Moderate | ✅ Multi-containers | Shared data, uploads |
| Azure Files (NFS) | ✅ Persistent | ⭐⭐⭐ Better | ✅ Multi-containers | Intensive Linux workloads |
| emptyDir | ❌ Ephemeral | ⭐⭐⭐ Fast | ✅ Within the group | Sharing between group containers |
| Secret volume | ❌ In-memory | ⭐⭐⭐ RAM speed | ❌ No | Certificates, sensitive configs |
ACI technical limits
| Resource | Default limit | Maximum limit |
|---|---|---|
| vCPU per container | 4 vCPU | 4 vCPU |
| RAM per container | 16 GB | 16 GB |
| Containers per group | 60 | 60 |
| Volume mounts | 1 per container | 1 per container |
| Exposed ports | 5 | 5 |
| Max duration (Spot) | Interruptible | - |
| Public IPs | 1 per group | 1 per group |
Glossary
| Term | Definition |
|---|---|
| ACI | Azure Container Instances - serverless container deployment service |
| Container Group | ACI deployment unit, like a Kubernetes Pod, grouping multiple containers |
| ACR | Azure Container Registry - private registry for storing Docker images |
| Readiness Probe | Check whether the container is ready to receive traffic |
| Liveness Probe | Continuous check whether the container is still alive |
| Restart Policy | Container restart strategy (Always, Never, OnFailure) |
| secureValue | Environment variable hidden in the Azure portal and logs |
| Azure Files | Managed SMB/NFS file share service in Azure |
| Application Gateway | Layer 7 load balancer with WAF, SSL termination |
| Private Link | Private connection to an Azure service via the Azure network backbone |
| Managed Identity | Azure-managed identity allowing access to resources without credentials |
| User-assigned MI | Independent Managed Identity reusable by multiple resources |
| System-assigned MI | Managed Identity tied to a resource’s lifecycle |
| KEDA | Kubernetes Event-Driven Autoscaler - event-based scaling |
| Spot Instance | Instance using unused Azure capacity at a reduced rate |
| Log Analytics | Workspace for collecting and analyzing Azure logs |
| KQL | Kusto Query Language - query language for Log Analytics |
| USE Method | Monitoring framework: Utilization, Saturation, Errors |
| RED Method | Monitoring framework: Rate, Errors, Duration |
| Azure Cost Management | Azure service for analyzing and controlling cloud spending |
| Vertical Scaling | Increasing/reducing CPU/RAM resources of a container (requires recreation) |
| Horizontal Scaling | Adding/removing instances of the same container |
| Pay-as-you-go | Billing model based on actual consumption |
| Container Image | Immutable package containing everything needed to run an application |
| Dockerfile | Declarative script for building a Docker image |
| Multi-stage build | Dockerfile technique for reducing the final image size |
| FQDN | Fully Qualified Domain Name - full domain name of an ACI |
| DNS Label | Part of the FQDN configured for the ACI (must be unique in the region) |
| Azure Monitor | Centralized Azure service for resource monitoring |
| Action Group | Group of actions to trigger when an Azure Monitor alert fires |
| Azure Advisor | Recommendation service for optimizing Azure resources |
Part 1 – Deploying Azure Container Instances
When to use ACI?
| Service | Use case |
|---|---|
| Azure Container Instances (ACI) | Isolated containers, short-lived, no orchestration required |
| Azure Kubernetes Service (AKS) | Orchestration, automatic scalability, complex workloads |
| Azure Container Apps | Microservices with event-driven scaling, sidecars |
| Azure App Service | Web applications, APIs, built-in horizontal scaling |
ACI is ideal for:
- Batch tasks or ephemeral CI/CD workers.
- Prototyping and development.
- Simple scaling (manual or event-based with Logic Apps/Functions).
- Bursting from AKS (backing up a Kubernetes cluster with ACI).
Azure Container Registry (ACR)
Complete workflow
Source code (.NET/Node/Python)
↓ Docker build (Dockerfile)
Container Image
↓ docker push / az acr build
Azure Container Registry (ACR)
↓ Referenced in ACI deployment
Container Instance running
Create and push an image to ACR
# Create an ACR
az acr create --resource-group myRG --name myRegistry --sku Standard
# Enable Admin user (for ACI authentication)
az acr update --name myRegistry --admin-enabled true
# Build and push from Azure (without local Docker)
az acr build --registry myRegistry --image myapp:v1 .
Deploy a Container Instance
Via Azure Portal (wizard)
- Search “Container instances” → Create.
- Choose: subscription, resource group, container name, region.
- Image source: Quickstart, Docker Hub, Private registry (ACR).
- For ACR: enable Admin user, enter credentials.
- Configure CPU/RAM, ports, DNS label.
- Review + Create.
Feature availability by region
- Availability Zones: not available everywhere.
- Confidential SKU: enhanced security, limited availability.
- Spot capacity: discount, variable availability.
Container Groups
Concept
- Group = set of containers sharing resources.
- Containers in the same group share:
- Host machine (same physical/virtual infrastructure).
- CPU and memory (limits configured at group level).
- Lifecycle (created, started, stopped together).
- Communication via localhost (any port).
- External IP address (single for the group).
- DNS label (shared FQDN).
- External volumes (Azure Files).
Deployment via ARM (JSON) or YAML
# YAML example for a Container Group
apiVersion: 2021-10-01
name: my-container-group
type: Microsoft.ContainerInstance/containerGroups
location: eastus
properties:
containers:
- name: frontend
properties:
image: myacr.azurecr.io/frontend:v1
resources:
requests:
cpu: 1
memoryInGb: 1.5
ports:
- port: 80
- name: sidecar
properties:
image: myacr.azurecr.io/sidecar:v1
resources:
requests:
cpu: 0.5
memoryInGb: 0.5
osType: Linux
ipAddress:
type: Public
ports:
- protocol: TCP
port: 80
Rule: YAML for containers only. ARM JSON if you also need to deploy other Azure resources at the same time.
Part 2 – Integrating ACI with Azure Services
ACI and Virtual Networks
ACI networking types
| Type | Description |
|---|---|
| Public | Public IP assigned, accessible from internet |
| Private | Deployed in a VNet, private IP, no direct public IP |
| None | No public IP or VNet, logs/commands accessible |
Why deploy in a VNet?
- Private connection to databases (SQL, Cosmos DB) in the VNet.
- Faster and more secure connections.
- Monitoring of inbound/outbound traffic.
- No direct internet exposure.
# Deploy ACI in a VNet
az container create \
--resource-group myRG \
--name aci-private \
--image mcr.microsoft.com/azuredocs/aci-helloworld \
--vnet myVNet \
--subnet mySubnet
ACI with Application Gateway
Architecture
Internet
↓ HTTPS
Application Gateway (public IP)
↓ HTTP (backend pool)
ACI (private IP in VNet)
Advantages
- Secure exposure of the private ACI.
- Horizontal load balancing.
- Scalability: multiple containers in the backend pool.
- No direct container exposure.
ACI with Azure Storage (Volumes)
The ephemeral container problem
- Containers can be restarted → data loss on local filesystem.
- Solution: mount an Azure File Share as a volume.
Azure Files as a persistent volume
ACI writes /tmp/fileshare/data.csv
↓ (mounted as volume)
Azure File Share
↓ (shared)
New ACI picks up the files
# Create a Storage Account
az storage account create \
--name mystorageaccount \
--resource-group myRG \
--sku Standard_LRS
# Create a File Share
az storage share create \
--name myfileshare \
--account-name mystorageaccount
# Retrieve the key
STORAGE_KEY=$(az storage account keys list \
--account-name mystorageaccount \
--query "[0].value" -o tsv)
# Create ACI with mounted volume
az container create \
--resource-group myRG \
--name aci-with-storage \
--image myimage \
--azure-file-volume-account-name mystorageaccount \
--azure-file-volume-account-key $STORAGE_KEY \
--azure-file-volume-share-name myfileshare \
--azure-file-volume-mount-path /data
ACI with Azure Functions
Use case: Create an ACI via HTTP trigger
User → HTTP request
↓
Azure Function (HTTP trigger)
↓ (Azure SDK)
Azure Container Instance created dynamically
- Abstracts the complexity of ACI creation.
- The user accesses a simple HTTP endpoint.
- The Function manages the parameters and creation via the Azure SDK.
Part 3 – Managing Azure Container Instances
Updating an ACI
Properties modifiable via YAML re-deploy
- Image, commands, environment variables, ports.
Properties requiring deletion and recreation
- OS type (Linux ↔ Windows).
- CPU/Memory/GPU resources (vertical scaling).
- Restart policy.
- Network profile.
- Availability zone.
Scaling ACI
- No native horizontal scaling (unlike AKS or Container Apps).
- Horizontal scaling “possible”: create multiple instances of the same container manually.
- Automation possible via Azure Monitor → trigger creation/deletion via Azure Automation or Functions.
- Vertical scaling: possible but requires container recreation.
Health Probes
Readiness Probe
- Executed between creation and when the container is ready to receive traffic.
- Continuously asks: “Are you ready?”
- The container does not receive traffic until it is Ready.
Liveness Probe
- Executed throughout the container’s lifetime.
- Continuously asks: “Are you alive?”
- If it fails → the container is marked as failed.
livenessProbe:
exec:
command:
- /bin/sh
- -c
- cat /tmp/healthy
initialDelaySeconds: 5
periodSeconds: 10
failureThreshold: 3
timeoutSeconds: 5
Restart Policies
| Policy | Description |
|---|---|
| Always | Restarts if the container stops or fails |
| Never | Never restarts (one-shot task) |
| OnFailure | Restarts only on failure (exit code ≠ 0) |
Environment variables
environmentVariables:
- name: APP_ENV
value: production # Regular variable
- name: DB_PASSWORD
secureValue: s3cr3t123 # Secret variable (not displayed)
⚠️ Never put credentials in plain text in the YAML. Use Azure Key Vault + Managed Identity in production.
Commands inside the container
# Via Azure Portal → Container → Connect tab
# Or via CLI
az container exec \
--resource-group myRG \
--name my-container \
--exec-command /bin/sh
Logs
# View logs
az container logs \
--resource-group myRG \
--name my-container
Part 4 – Optimizing ACI Performance and Costs
ACI cost factors
| Factor | Description |
|---|---|
| Compute | vCPU × duration + memory × duration |
| Network | Outbound data transfers |
| Storage | Persistent volumes, image sizes |
| Runtime | The longer the container runs, the more it costs |
| Region | Prices vary by Azure region |
Billing model
- Pay-as-you-go: billing per second of usage.
- Stopping the container = releases compute resources.
- Spot instances (unused Azure capacity) = significant reduction for non-critical workloads.
- ⚠️ Default quota is 0 → support request required (may take several days).
Optimizing resources
| Problem | Solution |
|---|---|
| Overprovisioning | Monitor actual usage via Azure Monitor, reduce CPU/RAM |
| Underprovisioning | Monitor crashes and throttling, increase resources |
| Long-running containers | Stop/delete after task completion |
| Large images | Use lightweight base images (Alpine), multi-stage builds |
CPU and Memory parameters (YAML/CLI)
az container create \
--cpu 1.0 \ # vCPU
--memory 1.5 \ # GB RAM
...
Part 5 – Monitoring and Securing ACI
Available metrics
- CPU utilization (%).
- Memory utilization (%).
- Network bytes received/transmitted.
Aggregation statistics
- Maximum: highest value in the period.
- Minimum: lowest value.
- Average: average over the period.
Monitoring methods: USE vs RED
| USE (Infrastructure) | RED (User-Facing) |
|---|---|
| Utilization | Rate (requests/sec) |
| Saturation | Errors (% requests failed) |
| Errors | Duration (response time) |
Creating Azure Monitor alerts
Azure Monitor → Alerts → Create Alert Rule
↓
Resource: ACI
Signal: CPU Usage
Condition: Max > 80% (over 1 minute)
Action Group: send email
Severity: Warning
Identity for ACI – Access to Azure resources
Managed Identity for ACI
Workflow:
1. Create a User-Assigned Managed Identity
2. Assign permissions (e.g.: Key Vault Secrets User)
3. Create ACI with the assigned identity
4. Inside the container: az login --identity --username <client-id>
5. Access Key Vault secrets
# Create managed identity
az identity create --name my-identity --resource-group myRG
# Get the ID
IDENTITY_ID=$(az identity show --name my-identity --query id -o tsv)
# Create ACI with identity
az container create \
--assign-identity $IDENTITY_ID \
...
Network security: Azure Private Link with ACI
- ACI in a VNet can access Key Vault via Private Link (private tunnel in the Azure backbone).
- Prevents traffic from going over the internet (even if both are in Azure).
- Disables the internet connection for the target service, everything goes through the private tunnel.
HTTPS with ACI (Nginx SSL Termination)
Architecture
Browser (HTTPS) → Container (Nginx)
├── SSL Certificate
└── Proxy → App (port 80 internally)
- Generate self-signed certificates with OpenSSL.
- Convert them to Base64.
- Include them in the ACI YAML as volumes.
- Configure Nginx to terminate SSL and proxy to the app.
Azure Cost Management for ACI
Azure Portal → Cost Management + Billing → Cost Analysis
- Filter by resource group or type “Container Instances”.
- View cost anomalies (automatic Azure alerts on sudden increases).
- Configure budgets with email alerts at X% of budget.
- Consult Azure Advisor for optimization recommendations.
Summary – Key Points
| Concept | Essential |
|---|---|
| ACI vs AKS | ACI = simple/isolated/ephemeral. AKS = orchestration/scale. |
| Vertical scaling | Requires container recreation |
| Readiness probe | Container not ready until probe succeeds |
| Liveness probe | Container marked failed if continuous probe failure |
| Azure Files | Data persistence between instances |
| Managed Identity | Credential-free authentication for accessing Azure resources |
| Private Link | Azure-to-Azure private tunnel, avoids the internet |
| Spot instances | Cheaper, default quota 0 (support request required) |
Search Terms
azure · container · instances · core · infrastructure · microsoft · aci · via · acr · deploying · monitoring · yaml · cost · deploy · identity · monitor · optimizing · resources · restart · securing · services · storage · access · alerts