Intermediate

Azure Container Instances – Complete Guide

Deploy, manage, integrate, optimize, monitor and secure containers on Azure Container Instances.

Level: Intermediate
Estimated duration: 6–8 hours
Last updated: June 2026

Table of Contents

  1. Introduction to Azure Containers
  2. Part 1 – Deploying Azure Container Instances
  3. Part 2 – Managing Azure Container Instances
  4. Part 3 – Integrating ACI with Azure Services
  5. Part 4 – Optimizing Performance and Costs
  6. Part 5 – Monitoring and Securing ACI
  7. Complete Code Examples
  8. Comparison Tables
  9. 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:

ServiceOrchestrationScalingComplexityPrimary use case
ACI❌ NoManual / Event⭐ Very simpleBatch jobs, prototyping, bursting
Container Apps✅ Basic (KEDA)Auto (scale-to-zero)⭐⭐ SimpleMicroservices, event-driven APIs
AKS✅ Full (K8s)Auto (HPA/VPA/KEDA)⭐⭐⭐⭐ AdvancedComplex production workloads
App Service❌ NoAuto (plans)⭐⭐ ModerateWeb applications, REST APIs
Azure Functions❌ NoAuto (Consumption)⭐⭐ ModerateEvent-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

# ===== 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 resourceDescription
Physical hostSame Azure machine (physical or virtual)
CPU and MemoryLimits configured at the group level
LifecycleCreated, started, stopped, deleted together
Internal communicationVia localhost on any port
External IP addressA single IP for the entire group
DNS Label (FQDN)A shared FQDN for the group
External volumesAzure 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

PolicyDescriptionUse case
AlwaysRestarts if the container stops or failsContinuous web services
NeverNever restartsOne-shot tasks, batch jobs
OnFailureRestarts 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: secureValue entries 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:

TypePublic IPVNetUse case
Public✅ Yes❌ NoDirect access from internet
Private❌ No✅ YesPrivate internal connections
None❌ No❌ NoContainers 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:

FactorBilling unitCost impact
vCPUvCPU-second$$$ - Primary factor
MemoryGB-second$$ - Second factor
Outbound networkGB of outgoing data$ - Variable
Azure Files volumesGB-month$ - If used
ACR imagesGB stored-month$ - Marginal
Azure regionMultiplied by region priceVaries 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

CriterionACIAKSContainer AppsApp 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
PricingPay per use (seconds)VM nodes + ManagedPay per useMonthly plan
Persistent volumes✅ Azure Files✅ Multi-options✅ Azure Files✅ Built-in
NetworkingVNet, Public, NoneFull VNetVNet, IngressVNet Integration
Windows✅ Yes✅ Yes❌ Linux only✅ Yes
Multi-container✅ Container Groups✅ Pods✅ Sidecars❌ No
Primary use caseBatch jobs, burstingProd microservicesEvent-driven, APIsWeb applications

Comparison of storage types for ACI

TypePersistencePerformanceSharingACI use case
Container filesystem❌ Ephemeral⭐⭐⭐ Fast❌ NoTemporary cache, temp files
Azure Files (SMB)✅ Persistent⭐⭐ Moderate✅ Multi-containersShared data, uploads
Azure Files (NFS)✅ Persistent⭐⭐⭐ Better✅ Multi-containersIntensive Linux workloads
emptyDir❌ Ephemeral⭐⭐⭐ Fast✅ Within the groupSharing between group containers
Secret volume❌ In-memory⭐⭐⭐ RAM speed❌ NoCertificates, sensitive configs

ACI technical limits

ResourceDefault limitMaximum limit
vCPU per container4 vCPU4 vCPU
RAM per container16 GB16 GB
Containers per group6060
Volume mounts1 per container1 per container
Exposed ports55
Max duration (Spot)Interruptible-
Public IPs1 per group1 per group

Glossary

TermDefinition
ACIAzure Container Instances - serverless container deployment service
Container GroupACI deployment unit, like a Kubernetes Pod, grouping multiple containers
ACRAzure Container Registry - private registry for storing Docker images
Readiness ProbeCheck whether the container is ready to receive traffic
Liveness ProbeContinuous check whether the container is still alive
Restart PolicyContainer restart strategy (Always, Never, OnFailure)
secureValueEnvironment variable hidden in the Azure portal and logs
Azure FilesManaged SMB/NFS file share service in Azure
Application GatewayLayer 7 load balancer with WAF, SSL termination
Private LinkPrivate connection to an Azure service via the Azure network backbone
Managed IdentityAzure-managed identity allowing access to resources without credentials
User-assigned MIIndependent Managed Identity reusable by multiple resources
System-assigned MIManaged Identity tied to a resource’s lifecycle
KEDAKubernetes Event-Driven Autoscaler - event-based scaling
Spot InstanceInstance using unused Azure capacity at a reduced rate
Log AnalyticsWorkspace for collecting and analyzing Azure logs
KQLKusto Query Language - query language for Log Analytics
USE MethodMonitoring framework: Utilization, Saturation, Errors
RED MethodMonitoring framework: Rate, Errors, Duration
Azure Cost ManagementAzure service for analyzing and controlling cloud spending
Vertical ScalingIncreasing/reducing CPU/RAM resources of a container (requires recreation)
Horizontal ScalingAdding/removing instances of the same container
Pay-as-you-goBilling model based on actual consumption
Container ImageImmutable package containing everything needed to run an application
DockerfileDeclarative script for building a Docker image
Multi-stage buildDockerfile technique for reducing the final image size
FQDNFully Qualified Domain Name - full domain name of an ACI
DNS LabelPart of the FQDN configured for the ACI (must be unique in the region)
Azure MonitorCentralized Azure service for resource monitoring
Action GroupGroup of actions to trigger when an Azure Monitor alert fires
Azure AdvisorRecommendation service for optimizing Azure resources

Part 1 – Deploying Azure Container Instances

When to use ACI?

ServiceUse case
Azure Container Instances (ACI)Isolated containers, short-lived, no orchestration required
Azure Kubernetes Service (AKS)Orchestration, automatic scalability, complex workloads
Azure Container AppsMicroservices with event-driven scaling, sidecars
Azure App ServiceWeb 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)

  1. Search “Container instances” → Create.
  2. Choose: subscription, resource group, container name, region.
  3. Image source: Quickstart, Docker Hub, Private registry (ACR).
  4. For ACR: enable Admin user, enter credentials.
  5. Configure CPU/RAM, ports, DNS label.
  6. 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

TypeDescription
PublicPublic IP assigned, accessible from internet
PrivateDeployed in a VNet, private IP, no direct public IP
NoneNo 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

PolicyDescription
AlwaysRestarts if the container stops or fails
NeverNever restarts (one-shot task)
OnFailureRestarts 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

FactorDescription
ComputevCPU × duration + memory × duration
NetworkOutbound data transfers
StoragePersistent volumes, image sizes
RuntimeThe longer the container runs, the more it costs
RegionPrices 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

ProblemSolution
OverprovisioningMonitor actual usage via Azure Monitor, reduce CPU/RAM
UnderprovisioningMonitor crashes and throttling, increase resources
Long-running containersStop/delete after task completion
Large imagesUse 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)
UtilizationRate (requests/sec)
SaturationErrors (% requests failed)
ErrorsDuration (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 \
  ...
  • 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

ConceptEssential
ACI vs AKSACI = simple/isolated/ephemeral. AKS = orchestration/scale.
Vertical scalingRequires container recreation
Readiness probeContainer not ready until probe succeeds
Liveness probeContainer marked failed if continuous probe failure
Azure FilesData persistence between instances
Managed IdentityCredential-free authentication for accessing Azure resources
Private LinkAzure-to-Azure private tunnel, avoids the internet
Spot instancesCheaper, 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

Interested in this course?

Contact us to book it or get a custom training plan for your team.