Skip to main content
All Projects

DMI Internship

React Deployment — Azure DevOps Multi-Stage CI/CD Pipeline

April 2026Repository
Azure DevOpsCI/CDReactTerraformAnsibleNGINXAzurePipelinesSSHDevOps

Built a multi-stage Azure DevOps CI/CD pipeline (Build → Test → Deploy) that automatically deploys a React application to an Ubuntu VM running NGINX. Infrastructure provisioned with Terraform, VM configured with Ansible, deployment over SSH.

React Deployment — Azure DevOps Multi-Stage CI/CD Pipeline

Overview

This project extends previous infrastructure automation work into a full CI/CD workflow. Rather than deploying files manually, every commit to the main branch automatically triggers a multi-stage Azure DevOps pipeline that builds the React application, runs unit tests, publishes the build artifact, and deploys it to a live Ubuntu VM via SSH.

The result is true continuous delivery: the developer commits code, the pipeline validates and deploys it, and the application is live — without manual intervention at any stage.

Problem

Manual deployment of web applications is slow, error-prone, and does not scale. The challenge was to build a pipeline that mirrors the way professional engineering teams ship code: automated build, automated test gate, artifact-based deployment, and a web server correctly configured for single-page application routing.

Architecture

Azure Repos (main branch)
  └── Trigger: commit to main
        └── Azure Pipeline
              ├── Stage 1: Build
              │     Install Node.js 20
              │     npm install
              │     npm run build
              │     Publish /build as artifact: react-build
              │
              ├── Stage 2: Test
              │     npm test -- --watchAll=false
              │     Pipeline fails here if tests fail
              │
              └── Stage 3: Deploy
                    Download artifact
                    CopyFilesOverSSH → /var/www/html
                    SSH: configure NGINX, set permissions, restart

Infrastructure (Terraform):

  • Resource Group: react-app-rg (West US 3)
  • VNet: 10.10.0.0/16, Subnet: 10.10.1.0/24
  • NSG: inbound TCP 22 (SSH) and TCP 80 (HTTP) only
  • Static public IP + NIC
  • Ubuntu 22.04 VM (Standard_B2ats_v2), password authentication

VM Configuration (Ansible):

  • NGINX installed and enabled
  • Web root: /var/www/html
  • SPA routing: try_files $uri /index.html — handles React client-side routing correctly
  • Permissions: www-data ownership, 755 on web root

Pipeline Configuration

The pipeline YAML defines three stages with explicit dependsOn and condition: succeeded() — the deploy stage only runs if build and test both pass:

trigger:
  - main

variables:
  sshService: 'ssh-to-nginx-vm'
  artifactName: 'react-build'
  webRoot: '/var/www/html'

The SSH Service Connection (ssh-to-nginx-vm) is configured in Azure DevOps and used by both CopyFilesOverSSH@0 and SSH@0 tasks — keeping credentials out of the pipeline YAML entirely.

Key Engineering Decisions

  • Artifact-based deployment — the pipeline deploys the compiled /build directory, not raw source code. The VM never runs npm install or npm run build. Separation between build environment and runtime environment is clean.
  • Test as a gate — the test stage is a hard dependency of deploy. A failing test prevents deployment — not as a convention but as a pipeline condition.
  • SPA routing in NGINXtry_files $uri /index.html ensures that refreshing any React route (e.g. /dashboard) returns index.html rather than a 404. A common misconfiguration when serving React apps behind a web server.
  • Credentials via Service Connection — SSH credentials are stored in Azure DevOps as a Service Connection and referenced by name in the YAML. Nothing sensitive appears in the pipeline definition or the repository.

Challenges and Resolutions

Out-of-Memory (OOM) error during npm install

The self-hosted agent VM initially had ~1 GB RAM. The npm install process was terminated by the kernel:

Killed npm install
Active: failed (Result: oom-kill)

Resolution: resized the VM to Standard_B2ats_v2 and added 2 GB swap:

sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile

The agent service was restarted and the pipeline workspace cleared before re-running. The lesson: self-hosted agent sizing matters. Node dependency resolution under npm install is memory-intensive and will OOM-kill on underpowered VMs.

Deployment not reflecting changes

After updating App.js and re-running the pipeline, the browser continued to show the old version. Two causes: React requires a full rebuild (not just file copy) before changes are visible, and browser caching served stale assets. Resolution: pipeline rebuild triggered by commit to main, hard refresh (Ctrl+Shift+R) in the browser.

Results

  • Pipeline runs automatically on every commit to main — no manual deployment steps
  • All three stages completed successfully: build → test → deploy
  • React application served correctly at http://20.xxx.xxx.180
  • NGINX routing confirmed: client-side navigation works without 404 errors
  • Second pipeline run verified idempotency — redeployment overwrites cleanly with no residual state

Key Learnings

  • CI/CD pipelines should deploy built artifacts, not raw source code. The build step belongs in the pipeline, not on the target server.
  • Self-hosted agents must be sized for their workload. Node.js dependency installation is memory-intensive — npm install on a 1 GB VM will fail.
  • Swap memory is a legitimate mitigation for memory-constrained build agents, particularly for short-lived workloads.
  • Separating build, test, and deploy into discrete stages makes the pipeline easier to debug — a test failure does not trigger a deploy, and the failure is immediately attributable to a specific stage.
  • React SPA routing requires NGINX configuration beyond the default. The try_files directive is not optional.