Deployment
CI/CD pipelines

Deploy Django With Azure DevOps Pipelines

Build an Azure Pipelines YAML pipeline for Django: test with a PostgreSQL service container, publish an image to Azure Container Registry, deploy to App Service or a VM through environments with approvals, and keep secrets in variable groups or Key Vault.

Problem statement

Teams in Microsoft-centred organisations often have Azure DevOps for repositories, boards, and pipelines. Deploying a Django app from Azure Pipelines works well, but the concepts differ from GitHub Actions: stages and jobs, service connections, variable groups, and environments with approvals and checks. This guide builds a YAML pipeline that tests Django against PostgreSQL, publishes a container image to Azure Container Registry (ACR), and deploys it to Azure App Service for Containers, with production behind an approval.

Azure Pipelines for DjangoTest stagePostgres serviceBuild stagepush image to ACRStaging envdeploy, migrateProd envapproval checkEnvironments with approvals turn the pipeline into a controlled release process.
  1. Add azure-pipelines.yml with stages Test, Build, DeployStaging, DeployProduction.
  2. In Test, declare a postgres service container and run tests in a Python job.
  3. Create a Docker registry service connection to ACR and build/push with the Docker@2 task tagged $(Build.SourceVersion).
  4. Create an Azure Resource Manager service connection (workload identity federation) and deploy with AzureWebAppContainer@1.
  5. Define staging and production environments; add an approval check on production.
  6. Store secrets in a variable group linked to Key Vault; run migrations in the deployment job.

Step-by-step solution

1. Test stage with PostgreSQL

trigger: [main]
pr: [main]

stages:
- stage: Test
  jobs:
  - job: test
    pool: { vmImage: ubuntu-latest }
    services:
      postgres: postgres
    container: python:3.12-slim
    variables:
      DATABASE_URL: postgres://app:app@postgres:5432/app
      DJANGO_SETTINGS_MODULE: config.settings.test
      SECRET_KEY: test-only
    steps:
    - script: pip install -r requirements.txt
    - script: python manage.py makemigrations --check --dry-run
    - script: pytest --junitxml=test-results.xml
    - task: PublishTestResults@2
      inputs: { testResultsFiles: test-results.xml }
      condition: always()

resources:
  containers:
  - container: postgres
    image: postgres:16
    env: { POSTGRES_USER: app, POSTGRES_PASSWORD: app, POSTGRES_DB: app }

When the job runs in a container, service containers are reachable by their name (postgres). Add a short wait loop if the database is not ready when tests start.

2. Build and push to ACR

- stage: Build
  dependsOn: Test
  condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
  jobs:
  - job: image
    pool: { vmImage: ubuntu-latest }
    steps:
    - task: Docker@2
      inputs:
        containerRegistry: acr-connection
        repository: myapp
        command: buildAndPush
        tags: $(Build.SourceVersion)
Azure DevOps building blocksWhat it isDjango useService connectioncredential to Azure or ACRpush images, deployVariable groupshared variables, Key Vault link✓SECRET_KEY, DB URLEnvironmenttarget with checks✓approval for productionDeployment jobjob bound to an environmentmigrate and releaseApprovals attach to environments, not to the YAML filedjango-deployment.com
Because checks live on the environment, editing YAML cannot bypass an approval.

3. Deploy to staging and production

- stage: DeployStaging
  dependsOn: Build
  jobs:
  - deployment: deploy
    environment: staging
    pool: { vmImage: ubuntu-latest }
    variables: [{ group: django-staging }]
    strategy:
      runOnce:
        deploy:
          steps:
          - task: AzureCLI@2
            displayName: Run migrations
            inputs:
              azureSubscription: azure-connection
              scriptType: bash
              scriptLocation: inlineScript
              inlineScript: |
                az container create -g myapp-rg -n migrate-$(Build.BuildId) \
                  --image myregistry.azurecr.io/myapp:$(Build.SourceVersion) \
                  --restart-policy Never --vnet myapp-vnet --subnet jobs \
                  --secure-environment-variables DATABASE_URL=$(DATABASE_URL) SECRET_KEY=$(SECRET_KEY) \
                  --command-line "python manage.py migrate --noinput"
                az container wait -g myapp-rg -n migrate-$(Build.BuildId) --custom "instanceView.state=='Succeeded'" 
          - task: AzureWebAppContainer@1
            inputs:
              azureSubscription: azure-connection
              appName: myapp-staging
              containers: myregistry.azurecr.io/myapp:$(Build.SourceVersion)

- stage: DeployProduction
  dependsOn: DeployStaging
  jobs:
  - deployment: deploy
    environment: production
    variables: [{ group: django-production }]
    strategy:
      runOnce:
        deploy:
          steps:
          - task: AzureWebAppContainer@1
            inputs:
              azureSubscription: azure-connection
              appName: myapp-prod
              containers: myregistry.azurecr.io/myapp:$(Build.SourceVersion)

For migrations, the most reliable pattern on App Service is a deployment slot: deploy the image to a staging slot, run python manage.py migrate there (App Service startup command or an Azure Container Apps job using the same image and database), verify, then swap slots. Keep migrations backward compatible so the old code keeps working during the swap. The Azure CLI step above runs migrate as a one-off Azure Container Instances group inside the VNet; it also needs registry access (--acr-identity with a managed identity, or --registry-login-server with credentials) to pull the image.

4. Approvals and secrets

In Pipelines → Environments → production → Approvals and checks, add approvers and optionally business-hours or branch-control checks. Link variable groups to Azure Key Vault so secrets are fetched at runtime and masked in logs. Use workload identity federation for the Azure service connection instead of client secrets.

from merge to production on Azure1Tests with Postgres serviceevery PR2Build and push to ACRtag = commit SHA3Deploy to staging slotmigrate, smoke test4Approval check on productionnamed approvers5Swap slotswarm instances, instant switchSlot swaps make the production switch fast and reversibledjango-deployment.com
Swapping back is the rollback, as long as migrations stayed backward compatible.

Verification check: a pull request runs only the Test stage with published results; a merge runs Build and DeployStaging, then waits on the production approval; after approval the production app serves the new image tag (check the container settings or a version endpoint).

Explanation

Azure Pipelines separates credentials (service connections), configuration (variable groups), and deployment targets (environments). Environments carry approvals and checks that the pipeline cannot override, which gives auditable releases. Building the container image once and promoting it through environments keeps staging and production identical. For Django specifically, migrations must run once per release against the right database, which slot-based or job-based patterns provide on Azure's container hosting options.

Edge cases / notes

  • Self-hosted agents inside your VNet can reach private databases for migrations directly.
  • Container Apps deployments use AzureContainerApps@1; Container Apps jobs are a clean way to run migrate.
  • Pipeline caching with the Cache@2 task speeds up pip installs.
  • Secrets in PR builds from forks are not available by default; keep tests independent of secrets.

Next steps

FAQ

Can Azure Pipelines run PostgreSQL for Django tests?

Yes. Declare a service container and run tests in a container job that reaches it by name.

How do I add an approval before production?

Add an approval check on the production environment in Azure DevOps.

Where should Django secrets live in Azure DevOps?

In variable groups linked to Azure Key Vault, referenced by deployment jobs.

How do migrations run on App Service?

Run them against the database from a staging slot or a one-off container job before swapping or switching traffic.

Keep going