Skip to main content
Back to blog

Using Docker with Azure DevOps for Test Environments

How to use Docker containers with Azure DevOps pipelines to create consistent, reproducible test environments. Covers Docker Compose for local dev, service containers in pipelines, containerised test runners, and database containers.

InnovateBits3 min read
Share

Docker eliminates the "works on my machine" problem in test automation. When your test environment is defined in code (a Dockerfile or docker-compose.yml), the same environment runs locally, in CI, and in every future pipeline run.


Service containers in Azure Pipelines

Azure Pipelines natively supports Docker service containers — containers that run alongside your pipeline job. This is the simplest way to add a database, cache, or mock server to your test pipeline.

trigger:
  branches:
    include: [main]
 
pool:
  vmImage: ubuntu-latest
 
# Service containers run alongside the job
resources:
  containers:
    - container: postgres
      image: postgres:16
      env:
        POSTGRES_PASSWORD: testpass
        POSTGRES_DB: testdb
        POSTGRES_USER: testuser
      ports:
        - 5432:5432
      options: >-
        --health-cmd "pg_isready -U testuser"
        --health-interval 10s
        --health-timeout 5s
        --health-retries 5
 
    - container: redis
      image: redis:7-alpine
      ports:
        - 6379:6379
 
jobs:
  - job: IntegrationTests
    services:
      postgres: postgres
      redis: redis
 
    steps:
      - task: NodeTool@0
        inputs:
          versionSpec: '20.x'
 
      - script: npm ci
 
      - script: npm run db:migrate
        displayName: Run database migrations
        env:
          DATABASE_URL: postgresql://testuser:testpass@localhost:5432/testdb
 
      - script: npm run test:integration
        displayName: Run integration tests
        env:
          DATABASE_URL: postgresql://testuser:testpass@localhost:5432/testdb
          REDIS_URL: redis://localhost:6379
 
      - task: PublishTestResults@2
        inputs:
          testResultsFormat: JUnit
          testResultsFiles: test-results/integration.xml
        condition: always()

Running tests in a Docker container

For consistent browser and dependency versions:

pool:
  vmImage: ubuntu-latest
 
steps:
  - script: |
      docker run --rm \
        -v $(Build.SourcesDirectory):/workspace \
        -w /workspace \
        -e BASE_URL=$(STAGING_URL) \
        -e CI=true \
        mcr.microsoft.com/playwright:v1.45.0-jammy \
        npx playwright test
    displayName: Run Playwright in container
 
  - task: PublishTestResults@2
    inputs:
      testResultsFormat: JUnit
      testResultsFiles: test-results/results.xml
    condition: always()

This guarantees identical Playwright and browser versions between local runs and CI.


Docker Compose for local development matching CI

# docker-compose.test.yml
version: '3.8'
services:
  postgres:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: testpass
      POSTGRES_DB: testdb
      POSTGRES_USER: testuser
    ports:
      - '5432:5432'
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U testuser"]
      interval: 10s
      timeout: 5s
      retries: 5
 
  redis:
    image: redis:7-alpine
    ports:
      - '6379:6379'
 
  app:
    build: .
    environment:
      DATABASE_URL: postgresql://testuser:testpass@postgres:5432/testdb
      REDIS_URL: redis://redis:6379
    depends_on:
      postgres:
        condition: service_healthy
    ports:
      - '3000:3000'

Run locally:

docker-compose -f docker-compose.test.yml up -d
npm run test:integration
docker-compose -f docker-compose.test.yml down

The same database, same Redis, same app version as CI.


Building and caching Docker images in pipelines

variables:
  DOCKER_BUILDKIT: '1'
  IMAGE_NAME: $(Build.Repository.Name)
  IMAGE_TAG: $(Build.BuildId)
 
steps:
  - task: Docker@2
    displayName: Build test image
    inputs:
      command: build
      dockerfile: Dockerfile.test
      tags: $(IMAGE_NAME):$(IMAGE_TAG)
      arguments: --build-arg BUILDKIT_INLINE_CACHE=1
 
  - task: Docker@2
    displayName: Push to ACR
    inputs:
      command: push
      containerRegistry: $(ACR_SERVICE_CONNECTION)
      repository: $(IMAGE_NAME)
      tags: $(IMAGE_TAG)

Common errors and fixes

Error: Service container shows as unhealthy in pipeline Fix: Add a health check and --health-retries option. The pipeline starts the job before the container is ready without health checks. Use options: --health-cmd "..." --health-retries 5.

Error: cannot connect to localhost:5432 in the test job Fix: Service containers are accessed via localhost in Azure Pipelines. Ensure the port mapping is correct (5432:5432) and the connection string uses localhost not the container name.

Error: Docker build fails with "permission denied" on Linux agents Fix: Add the current user to the docker group: - script: sudo usermod -aG docker $USER && newgrp docker. Or use sudo docker build in the script.

Error: Tests pass in Docker locally but fail in pipeline container Fix: Check environment variable mapping. Variables passed with -e flag in local Docker run must be set as pipeline variables. Also check file mount paths — $(Build.SourcesDirectory) is the correct pipeline path.

Free newsletter

Stay ahead in AI-driven QA

Get practical tutorials on test automation, AI testing, and quality engineering — straight to your inbox. No spam, unsubscribe any time.

Discussion

Sign in with GitHub to comment · powered by Giscus