Back to Blog
Azure
4 min read

Deploying Python Apps to Azure Container Apps

AzureContainer AppsPythonDockerServerless

Azure Container Apps gives you containers without managing Kubernetes. Great for APIs, background workers, and event-driven apps. Here's how to deploy a Python application.

When to Use Container Apps

Good fit:

  • HTTP APIs and web apps
  • Background processing jobs
  • Event-driven microservices
  • Apps that need to scale to zero

Consider AKS instead for:

  • Complex multi-service deployments
  • Need for Kubernetes-specific features
  • Existing Kubernetes expertise

Project Structure

my-app/
├── Dockerfile
├── requirements.txt
├── app/
│   ├── __init__.py
│   └── main.py
└── .dockerignore

Dockerfile for Python

FROM python:3.11-slim

WORKDIR /app

# Install dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copy application code
COPY app/ ./app/

# Create non-root user
RUN useradd -m appuser && chown -R appuser:appuser /app
USER appuser

# Expose port
EXPOSE 8000

# Run with Gunicorn
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "2", "app.main:app"]

requirements.txt

fastapi==0.104.1
gunicorn==21.2.0
uvicorn==0.24.0

Simple FastAPI App

# app/main.py
from fastapi import FastAPI

app = FastAPI()

@app.get("/")
def read_root():
    return {"status": "healthy"}

@app.get("/api/items/{item_id}")
def read_item(item_id: int):
    return {"item_id": item_id}

Terraform Deployment

# Container Apps Environment
resource "azurerm_container_app_environment" "this" {
  name                       = "cae-production"
  location                   = azurerm_resource_group.this.location
  resource_group_name        = azurerm_resource_group.this.name
  log_analytics_workspace_id = azurerm_log_analytics_workspace.this.id
}

# Container Registry
resource "azurerm_container_registry" "this" {
  name                = "crproduction"
  resource_group_name = azurerm_resource_group.this.name
  location            = azurerm_resource_group.this.location
  sku                 = "Basic"
  admin_enabled       = true
}

# Container App
resource "azurerm_container_app" "api" {
  name                         = "ca-api"
  container_app_environment_id = azurerm_container_app_environment.this.id
  resource_group_name          = azurerm_resource_group.this.name
  revision_mode                = "Single"

  template {
    container {
      name   = "api"
      image  = "${azurerm_container_registry.this.login_server}/api:latest"
      cpu    = 0.5
      memory = "1Gi"

      env {
        name  = "ENVIRONMENT"
        value = "production"
      }

      env {
        name        = "DATABASE_URL"
        secret_name = "database-url"
      }
    }

    min_replicas = 0
    max_replicas = 10
  }

  secret {
    name  = "database-url"
    value = var.database_connection_string
  }

  secret {
    name  = "registry-password"
    value = azurerm_container_registry.this.admin_password
  }

  registry {
    server               = azurerm_container_registry.this.login_server
    username             = azurerm_container_registry.this.admin_username
    password_secret_name = "registry-password"
  }

  ingress {
    external_enabled = true
    target_port      = 8000
    transport        = "auto"

    traffic_weight {
      percentage      = 100
      latest_revision = true
    }
  }
}

Build and Push with Azure CLI

# Build and push to ACR
az acr build --registry crproduction \
  --image api:latest \
  --file Dockerfile .

DevOps Pipeline

trigger:
  branches:
    include:
      - main
  paths:
    include:
      - 'src/**'
      - 'Dockerfile'

pool:
  vmImage: 'ubuntu-latest'

variables:
  acrName: 'crproduction'
  imageName: 'api'

stages:
- stage: Build
  jobs:
  - job: BuildAndPush
    steps:
    - task: AzureCLI@2
      displayName: 'Build and Push Image'
      inputs:
        azureSubscription: 'your-service-connection'
        scriptType: 'bash'
        scriptLocation: 'inlineScript'
        inlineScript: |
          az acr build --registry $(acrName) \
            --image $(imageName):$(Build.BuildId) \
            --image $(imageName):latest \
            --file Dockerfile .

- stage: Deploy
  dependsOn: Build
  jobs:
  - job: DeployToContainerApps
    steps:
    - task: AzureCLI@2
      displayName: 'Update Container App'
      inputs:
        azureSubscription: 'your-service-connection'
        scriptType: 'bash'
        scriptLocation: 'inlineScript'
        inlineScript: |
          az containerapp update \
            --name ca-api \
            --resource-group rg-production \
            --image $(acrName).azurecr.io/$(imageName):$(Build.BuildId)

Persistent Storage with Azure Files

For apps needing persistent storage:

resource "azurerm_container_app_environment_storage" "data" {
  name                         = "data-storage"
  container_app_environment_id = azurerm_container_app_environment.this.id
  account_name                 = azurerm_storage_account.this.name
  share_name                   = azurerm_storage_share.data.name
  access_key                   = azurerm_storage_account.this.primary_access_key
  access_mode                  = "ReadWrite"
}

resource "azurerm_container_app" "worker" {
  # ... other config ...

  template {
    container {
      name   = "worker"
      image  = "${azurerm_container_registry.this.login_server}/worker:latest"
      cpu    = 0.5
      memory = "1Gi"

      volume_mounts {
        name = "data"
        path = "/data"
      }
    }

    volume {
      name         = "data"
      storage_name = azurerm_container_app_environment_storage.data.name
      storage_type = "AzureFile"
    }
  }
}

Scaling Configuration

template {
  min_replicas = 1
  max_replicas = 10

  # Scale on HTTP requests
  http_scale_rule {
    name                = "http-scaling"
    concurrent_requests = 100
  }

  # Or scale on queue messages
  azure_queue_scale_rule {
    name         = "queue-scaling"
    queue_name   = "work-items"
    queue_length = 20
    authentication {
      secret_name       = "queue-connection"
      trigger_parameter = "connection"
    }
  }
}

Health Probes

template {
  container {
    # ... other config ...

    liveness_probe {
      transport = "HTTP"
      path      = "/health"
      port      = 8000
    }

    readiness_probe {
      transport = "HTTP"
      path      = "/ready"
      port      = 8000
    }
  }
}

Need help containerising your applications? Get in touch - we help organisations modernise their application deployments.

Need help with your Azure environment?

Get in touch for a free consultation.

Get in Touch