Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Azure DevOps YAML Pipelines: A Practical Guide

By Kokil Thapa | Last reviewed: August 2026

Migrating from classic GUI editors to infrastructure-as-code is the single most important step for stabilizing your release process. This Azure DevOps YAML Pipelines: A Practical Guide provides the exact configuration patterns needed to build, test, and deploy PHP and Laravel applications reliably. While I primarily use Deployer 7 and GitLab CI for my own legal-tech and eCommerce projects, many enterprise clients require Azure-native solutions; understanding these YAML structures ensures you can deliver robust automation regardless of the platform. For teams evaluating their broader development stack before committing to a pipeline strategy, reviewing the core responsibilities of a full-stack developer in Nepal helps clarify whether you need dedicated DevOps resources or if full-service engineering covers your needs.

How do you structure Azure DevOps YAML Pipelines for PHP applications?

The foundation of any effective pipeline is a clean separation between building artifacts and deploying them. In practice, monolithic pipeline definitions become unmaintainable quickly. For Laravel or Symfony applications running on PHP 8.4, you should adopt a multi-stage architecture that mirrors your promotion path. The YAML syntax is strict; indentation errors are the most common cause of initial failures.

A production-grade pipeline for a PHP application typically requires three distinct phases: validation, artifact creation, and environment-specific deployment. Unlike compiled languages, PHP deployments often involve transferring source code plus vendor dependencies and built frontend assets. Your YAML must account for Composer installation, NPM/Vite builds, and potentially database migrations.

Build StagePHP 8.4 + Node 22Composer InstallVite BuildRun TestsStaging StageDeploy ArtifactMigrate DBClear CacheSmoke TestsProduction StageManual ApprovalZero-DowntimeSwap SymlinksVerify Health
Multi-stage Azure DevOps YAML Pipelines architecture separating build validation from staging and production deployments

Your root azure-pipelines.yml should trigger on both branches and tags. For client projects where stability matters more than speed, I recommend triggering production deploys only on semantic version tags rather than direct main branch pushes. This prevents accidental releases when merging pull requests.

trigger:
  branches:
    include:
      - main
      - develop
  tags:
    include:
      - v*

stages:
  - stage: Build
    displayName: 'Build & Test'
    jobs:
      - job: BuildJob
        pool:
          vmImage: 'ubuntu-24.04'
        steps:
          - task: UsePhpVersion@1
            inputs:
              version: '8.4.x'
          - script: composer install --no-dev --optimize-autoloader
            displayName: 'Install Dependencies'
          - script: npm ci && npm run build
            displayName: 'Build Frontend Assets'

This structure keeps the build stage focused purely on creating a verified artifact. Notice the use of npm ci instead of npm install; this ensures deterministic installs based on your lockfile, preventing "works on my machine" failures in CI.

How do you manage secrets and environment variables securely?

Hardcoding credentials in YAML files is a critical security failure. Azure DevOps provides variable groups and Azure Key Vault integration specifically to prevent this. On a recent legal-tech portal handling sensitive client documents, we stored all database credentials, API keys for eSewa/Khalti payment gateways, and SSH private keys exclusively in variable groups linked to specific pipeline stages.

Variable groups allow you to maintain different configurations for staging and production without changing code. You reference them at the stage level, ensuring production secrets never leak into build logs or staging environments. Always mark sensitive variables as "secret" in the Azure DevOps UI; this masks them in logs automatically.

  • Never commit .env files: Use a .env.example template in your repository and inject real values during the pipeline execution via variable substitution tasks.
  • Scope variable groups: Link production secrets only to the Production stage, not the entire pipeline. This limits blast radius if a build job is compromised.
  • Use service connections: For SSH deployments or Azure App Service targets, configure authenticated service connections rather than embedding credentials in scripts.
  • Audit access: Restrict who can edit variable groups containing production secrets. Treat them with the same rigor as production database access.

When injecting these variables into your Laravel application during deployment, use the ReplaceTokens task or simple sed commands within your deployment script. Avoid complex string manipulation in YAML itself; keep the logic in shell scripts where it can be tested locally.

What are the best practices for caching dependencies in Azure Pipelines?

Without caching, every pipeline run downloads and compiles hundreds of megabytes of dependencies. For a typical Laravel project with Vite, uncached builds can take 8–12 minutes. With proper cache configuration, subsequent runs drop to 2–3 minutes. This directly impacts developer feedback loops and monthly compute costs.

Without CacheDownload Composer Packages (3 min)Install Node Modules (2 min)Compile Assets (3 min)Run Test Suite (2 min)Total: ~10 MinutesWith Cache HitRestore Vendor Cache (5 sec)Restore Node Cache (5 sec)Compile Assets (3 min)Run Test Suite (2 min)Total: ~5 Minutes
Impact of dependency caching on Azure DevOps YAML Pipelines build duration for PHP and Node.js projects

The Cache task in Azure Pipelines uses a key based on your lockfiles. When composer.lock or package-lock.json changes, the cache misses and rebuilds. When they remain static, restoration takes seconds. Configure separate caches for Composer and NPM to avoid invalidating one when the other updates.

- task: Cache@2
  inputs:
    key: 'composer | "$(Agent.OS)" | composer.lock'
    restoreKeys: |
      composer | "$(Agent.OS)"
    path: $(Pipeline.Workspace)/.composer-cache
  displayName: 'Cache Composer packages'

- script: |
    export COMPOSER_CACHE_DIR=$(Pipeline.Workspace)/.composer-cache
    composer install --no-dev --prefer-dist
  displayName: 'Install with cache'

Note the use of restoreKeys. This partial match allows restoring an older cache even if the lockfile changed slightly, which still saves download time for unchanged packages. Without this fallback, minor dependency updates force complete cold installs. For teams managing multiple PHP versions, include the PHP version in the cache key to prevent binary incompatibilities.

How does Azure DevOps compare to GitLab CI for Laravel deployments?

Many Nepali businesses and international clients ask whether to standardize on Azure DevOps or stick with GitLab CI. Having shipped production systems using both, the choice depends on ecosystem integration rather than raw capability. Both platforms handle Laravel deployments competently, but their operational characteristics differ significantly.

CriteriaAzure DevOps YAML PipelinesGitLab CI / CD
Configuration StyleSingle or multi-file YAML, verbose schemaCompact YAML, includes/templates system
Microsoft EcosystemNative integration with Azure App Service, SQL, ADRequires manual service principal setup
Self-Hosted RunnersSupported, Windows/Linux/macOS agentsExcellent runner ecosystem, Docker executor default
Artifact ManagementPipeline artifacts, Universal PackagesBuilt-in package registry, container registry
Approval GatesEnvironment approvals, checks, business hoursProtected environments, manual jobs
Learning CurveSteeper, enterprise-oriented terminologyLower barrier, developer-centric design
Cost ModelFree tier generous, parallelism costs add upFree tier adequate, SaaS minutes predictable

In my experience, Azure DevOps excels when your infrastructure already lives in Azure or when corporate compliance mandates Microsoft tooling. The approval gates and environment protections are more granular out-of-the-box. However, for pure Laravel/PHP shops without Azure dependencies, GitLab CI's template system and tighter repository integration often result in faster initial setup and simpler maintenance. If you are exploring modern admin panel development alongside your CI/CD strategy, understanding Laravel Filament admin panels can reduce backend complexity regardless of which pipeline platform you choose.

How do you implement zero-downtime deployments with Azure Pipelines?

Deploying directly to a live directory causes brief outages during file transfers and cache clearing. Zero-downtime deployment requires atomic symlink swaps, identical to Deployer 7 workflows but orchestrated through Azure YAML. The pipeline builds a timestamped release directory, prepares it completely offline, then swaps the current symlink in a single operation.

1. Upload ReleaseCreate timestamped dirUpload artifactInstall vendorsBuild assetsLink shared/.env2. Prepare LiveRun migrationsWarm config cacheHealth check newVerify readiness3. Atomic Swapln -sfn new currentReload PHP-FPMClear opcacheCleanup old releasesRollbackRe-point symlinkInstant recoveryNo data loss
Zero-downtime deployment workflow in Azure DevOps YAML Pipelines with atomic symlink swap and instant rollback capability

The critical detail is running migrations and cache warming against the new release directory before swapping symlinks. If migration fails, the symlink never moves and users see no interruption. Only after successful preparation does the atomic ln -sfn command execute. PHP-FPM must reload afterward to clear opcode caches pointing to old paths.

- script: |
    RELEASE_DIR="/var/www/releases/$(Build.BuildId)"
    ln -sfn $RELEASE_DIR /var/www/current
    sudo systemctl reload php8.4-fpm
    echo "Deployed release $(Build.BuildId) successfully"
  displayName: 'Atomic Symlink Swap'
  condition: succeeded()

Always include a rollback step or separate rollback pipeline. With symlink-based deployments, rollback is instantaneous: point the symlink back to the previous release directory and reload FPM. No file transfers, no git resets, no waiting. This safety net is non-negotiable for any customer-facing eCommerce or legal portal where downtime translates directly to lost revenue or missed deadlines.

Implementing Reliable Azure DevOps YAML Pipelines

Building trustworthy Azure DevOps YAML Pipelines requires treating your CI/CD configuration with the same rigor as application code. Start with multi-stage separation, implement dependency caching early, secure secrets in variable groups, and always deploy via atomic operations. Whether you are maintaining a high-traffic WooCommerce store or a sensitive legal-tech platform, these patterns prevent the majority of deployment failures I encounter in production audits. If your team needs hands-on assistance architecting pipelines or evaluating whether Azure DevOps fits your specific workflow, reach out to discuss your deployment requirements.

Frequently Asked Questions

Classic pipelines use a GUI editor while YAML pipelines define CI/CD as code in a repository file. YAML enables version control, peer review via pull requests, and infrastructure-as-code practices that GUI-based classic editors cannot support for team collaboration or audit trails.

Free tier includes one parallel job and unlimited minutes for public projects. Private projects get 1,800 free minutes monthly. Paid tiers start at USD 40 (~NPR 5,300) per additional parallel job. Costs scale with concurrency needs, not pipeline count, making it affordable for small Nepal-based dev teams.

Migrate when you need version-controlled CI/CD configuration, multi-stage deployments, or template reuse across repositories. If your current classic pipeline works reliably and requires no changes, defer migration until a natural refactor point to avoid unnecessary risk and testing overhead.

Define stages sequentially using the stages keyword with dependencies between build, test, and deploy phases. Each stage runs in isolation with its own agent pool. Use condition expressions to gate production deploys on successful test stages. This mirrors the zero-downtime symlinked release pattern I use with Deployer 7 on GitLab CI, adapted for Azure's stage model.

Yes, store shared templates in a dedicated repository and reference them using the resources.repositories syntax. Parameterize templates for environment-specific values like connection strings or deployment targets. This avoids duplicating pipeline logic across ten different service repos, similar to how I manage shared Deployer configurations across sister sites like notarykathmandu.com and translationnepal.com.

Store sensitive values in Azure Key Vault or pipeline variable groups marked as secret. Never hardcode credentials in YAML files. Reference secrets using $(variableName) syntax at runtime. For service connections, use managed identities where possible instead of personal access tokens. Audit secret access through pipeline logs which mask secret values automatically during execution.

Service connections lack required permissions on target resources, or the pipeline service account needs explicit role assignments. Verify the service principal has Contributor or Deployment roles on the target subscription. Check that YAML references the correct service connection name exactly. In my experience, this mirrors PHP-FPM ownership issues after deploy where the executing user lacks write access to release directories.

Use the Cache task with key patterns based on lock files like composer.lock or package-lock.json. Configure restoreKeys for partial matches when exact keys miss. Cached paths must be absolute or relative to $(Pipeline.Workspace). Cache reduces build times by 40-60% for PHP/Node projects by skipping redundant dependency downloads between runs.

Syntax errors include incorrect indentation, missing required fields like trigger or pool, or invalid expression syntax. The YAML schema validator catches these before execution. Common issues are unquoted special characters in strings and malformed condition expressions. Use the built-in validator in the pipeline editor or run az pipelines validate locally to catch errors before committing broken configurations.

Add environments with approval checks configured in project settings, then reference them in deploy stages. Approvals pause execution until designated reviewers approve via email or portal notification. Combine with timeout policies to auto-reject stale approvals. This provides the same safety mechanism as manual production gates I configure in GitLab CI for legal-tech portals handling sensitive client data.

Yes, use the SSH deployment task or custom script tasks with ssh commands. Configure SSH service connections with private key authentication stored securely. Ensure target servers allow the pipeline agent IP range through firewalls. For PHP applications, combine with rsync for artifact transfer and remote commands for PHP-FPM reloads, matching the Deployer 7 workflow I use for Ubuntu-based production deployments.

Profile each stage duration in pipeline analytics to identify bottlenecks. Enable diagnostic logging with system.debug=true to expose hidden operations. Check agent pool utilization for queue wait times. Optimize by parallelizing independent jobs, caching aggressively, and using self-hosted agents for consistent hardware. Slow pipelines often stem from uncached npm install or composer install steps running repeatedly.

Laravel requires specific PHP extensions and environment variables that default Microsoft-hosted agents may lack. Use container jobs with custom Docker images containing required extensions. Ensure .env.testing exists for CI runs. Artifacts must include vendor directory if not installing during deploy. Storage symlink creation needs explicit scripting since Azure agents don't auto-run php artisan storage:link like local development environments.

Run migrations in a dedicated stage before application deployment with rollback scripts prepared. Use maintenance mode flags to prevent user access during schema changes. Test migrations against staging databases first. For Laravel, wrap php artisan migrate:fresh in conditional logic that only executes in non-production stages. Always backup production databases before migration stages execute, following the same caution I apply when upgrading live WooCommerce or legal portal databases.

Yes, define strategy.matrix with version arrays to test against PHP 8.2, 8.3, and 8.4 simultaneously. Each matrix combination spawns a parallel job. Use variables to parameterize composer install and test commands per version. This validates framework compatibility across supported PHP releases before merging, ensuring Laravel 12 applications work correctly on all production-supported interpreter versions without sequential testing delays.

Share this article

Quick Contact Options
Choose how you want to connect me: