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.
- Add
azure-pipelines.ymlwith stages Test, Build, DeployStaging, DeployProduction. - In Test, declare a
postgresservice container and run tests in a Python job. - Create a Docker registry service connection to ACR and build/push with the
Docker@2task tagged$(Build.SourceVersion). - Create an Azure Resource Manager service connection (workload identity federation) and deploy with
AzureWebAppContainer@1. - Define
stagingandproductionenvironments; add an approval check onproduction. - 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)
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.
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 runmigrate. - Pipeline caching with the
Cache@2task speeds up pip installs. - Secrets in PR builds from forks are not available by default; keep tests independent of secrets.
Next steps
- Configure the target platform with Deploy Django on Azure App Service.
- For containers with jobs and scaling, see Deploy Django on Azure Container Apps.
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.