Introduction
Deploying frontend applications to production environments can be challenging, especially when dealing with single-page applications (SPAs) like React. In this blog post, I'll walk through the process of setting up a complete CI/CD pipeline for deploying a frontend application to Azure App Service using Bitbucket Pipelines, and how to overcome common challenges like path mapping and URL rewriting.
Prerequisites
Before we begin, make sure you have:
- A Bitbucket repository with your frontend code
- An Azure subscription
- An Azure App Service plan and web app
- Service principal credentials for Azure deployment
Understanding the Pipeline Architecture
Our deployment pipeline follows these key steps:
- Install dependencies - Prepare the environment
-
Parallel processes:
- Build the application - Compile, bundle, and package the app
- Security scan - Check for sensitive data in code
- Manual deployment trigger - Deploy to production with approval
The Bitbucket Pipeline Configuration
Here's our complete bitbucket-pipelines.yml
file:
image: node:20
pipelines:
branches:
main:
- step:
name: install
caches:
- node
script:
- rm -rf node_modules package-lock.json
- npm install --legacy-peer-deps
- parallel:
- step:
name: Build
caches:
- node
script:
- rm -rf node_modules package-lock.json
- npm install --legacy-peer-deps
- npm run build
- mv web.config dist/ # Move web.config inside dist/
- apt update && apt install zip
- zip -r app-$BITBUCKET_BUILD_NUMBER.zip dist package.json -x *.git* bitbucket-pipelines.yml
artifacts:
- "*.zip"
- step:
name: Security Scan
script:
# Run a security scan for sensitive data.
- pipe: atlassian/git-secrets-scan:0.5.1
- step:
name: Deploy to Production
trigger: manual
deployment: Production
script:
- pipe: atlassian/azure-web-apps-deploy:1.2.3
variables:
AZURE_APP_ID: $AZURE_APP_ID
AZURE_PASSWORD: $AZURE_PASSWORD
AZURE_TENANT_ID: $AZURE_TENANT_ID
AZURE_RESOURCE_GROUP: $AZURE_RESOURCE_GROUP
AZURE_APP_NAME: $AZURE_APP_NAME
ZIP_FILE: 'app-$BITBUCKET_BUILD_NUMBER.zip'
DEBUG: 'true'
Let's break down each section:
Step 1: Installation
- step:
name: install
caches:
- node
script:
- rm -rf node_modules package-lock.json
- npm install --legacy-peer-deps
This step cleans any existing dependencies and installs fresh ones. The --legacy-peer-deps
flag helps avoid compatibility issues between packages.
Step 2: Parallel Execution
- parallel:
- step:
name: Build
caches:
- node
script:
- rm -rf node_modules package-lock.json
- npm install --legacy-peer-deps
- npm run build
- mv web.config dist/ # Move web.config inside dist/
- apt update && apt install zip
- zip -r app-$BITBUCKET_BUILD_NUMBER.zip dist package.json -x *.git* bitbucket-pipelines.yml
artifacts:
- "*.zip"
- step:
name: Security Scan
script:
- pipe: atlassian/git-secrets-scan:0.5.1
This section runs two processes in parallel:
- Build: Compiles the frontend application and packages it for deployment
- Security Scan: Checks the codebase for accidentally committed secrets or sensitive data
The build step is particularly important as it:
- Builds the application
- Moves the
web.config
file into the distribution folder - Creates a zip archive with a unique name based on the build number
- Defines the zip file as an artifact to be used in later steps
Step 3: Deployment
- step:
name: Deploy to Production
trigger: manual
deployment: Production
script:
- pipe: atlassian/azure-web-apps-deploy:1.2.3
variables:
AZURE_APP_ID: $AZURE_APP_ID
AZURE_PASSWORD: $AZURE_PASSWORD
AZURE_TENANT_ID: $AZURE_TENANT_ID
AZURE_RESOURCE_GROUP: $AZURE_RESOURCE_GROUP
AZURE_APP_NAME: $AZURE_APP_NAME
ZIP_FILE: 'app-$BITBUCKET_BUILD_NUMBER.zip'
DEBUG: 'true'
This step:
- Has a manual trigger for controlled deployments
- Uses the Atlassian Azure Web Apps Deploy pipe
- Passes necessary Azure credentials stored as repository variables
- Specifies the zip file created in the build step
Configuring web.config for React SPA on Azure
One of the most challenging aspects of deploying a React SPA to Azure App Service is configuring URL rewriting correctly. Since SPAs use client-side routing, all routes need to be redirected to the index.html
file.
Here's the web.config
file we're using:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<system.webServer>
<rewrite>
<rules>
<rule name="React SPA" stopProcessing="true">
<match url=".*" />
<conditions logicalGrouping="MatchAll">
<add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
<add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
</conditions>
<action type="Rewrite" url="/index.html" />
</rule>
</rules>
</rewrite>
<staticContent>
<mimeMap fileExtension=".json" mimeType="application/json" />
<mimeMap fileExtension=".js" mimeType="application/javascript" />
<mimeMap fileExtension=".css" mimeType="text/css" />
<mimeMap fileExtension=".ts" mimeType="application/javascript" />
<mimeMap fileExtension=".tsx" mimeType="application/javascript" />
</staticContent>
<httpProtocol>
<customHeaders>
<add name="Access-Control-Allow-Origin" value="*" />
<add name="X-Content-Type-Options" value="nosniff" />
</customHeaders>
</httpProtocol>
</system.webServer>
</configuration>
Understanding the web.config configuration
-
URL Rewriting:
- The rule matches all URLs (
match url=".*"
) - It applies only when the requested URL doesn't match an existing file or directory
- It redirects all such requests to
/index.html
, allowing the React router to handle them
- The rule matches all URLs (
-
MIME Type Configuration:
- Ensures proper MIME types for various file extensions
- Critical for browsers to correctly interpret JavaScript, CSS, and JSON files
-
HTTP Headers:
-
Access-Control-Allow-Origin: *
- Enables cross-origin resource sharing -
X-Content-Type-Options: nosniff
- Prevents MIME type sniffing security vulnerabilities
-
Overcoming Common Challenges
Challenge 1: Path Mapping Differences in Azure
Azure App Service has a unique architecture that uses different virtual and physical path mappings, which can cause issues when deploying frontend applications:
Understanding Azure's Path Structure:
-
Physical Path: The actual location on the server where your files are stored (e.g.,
D:\home\site\wwwroot
) -
Virtual Path: The URL path that users access (e.g.,
https://yourapp.azurewebsites.net/path
)
Common Issues:
Root Path Configuration: By default, Azure deploys your application to the root of the App Service (
/
). If your frontend expects to be served from a subdirectory, path conflicts will occur.Virtual Directory Mapping: Azure allows you to map different physical folders to different virtual paths through the Azure Portal or configuration files.
Solution:
- In the Azure Portal, navigate to your App Service → Configuration → Path mappings
- Ensure your application files are correctly mapped to the appropriate virtual path
- For single-page applications, make sure the root directory (
/
) is correctly mapped to your application's distribution folder
Alternatively, you can define virtual applications and directories in your web.config
:
<system.webServer>
<virtualDirectory path="/" physicalPath="%SystemDrive%\home\site\wwwroot\dist" />
<!-- Other configurations -->
</system.webServer>
This ensures that when a user accesses the root URL, they're served content from the dist
folder where your built frontend application resides.
Challenge 2: Web.config Configuration
The biggest challenge is often configuring the web.config file correctly for client-side routing.
Solution: Our web.config file above solves this by:
- Using the URL Rewrite module to redirect all non-file, non-directory requests to index.html
- Configuring proper MIME types for all static assets
- Setting appropriate HTTP headers for security and cross-origin requests
Setting Up Azure Environment Variables
For the deployment to work, you need to configure these repository variables in Bitbucket:
-
AZURE_APP_ID
: Your Azure service principal ID -
AZURE_PASSWORD
: Your Azure service principal password -
AZURE_TENANT_ID
: Your Azure tenant ID -
AZURE_RESOURCE_GROUP
: The resource group containing your App Service -
AZURE_APP_NAME
: The name of your App Service
You can set these variables in Bitbucket by going to:
Repository settings > Repository variables
Testing the Deployment
After deploying, you should verify that:
- Your application loads correctly
- Deep linking works (you can navigate directly to any route)
- Static assets (images, CSS, JavaScript) load properly
- API calls work as expected
Conclusion
Deploying a frontend application to Azure App Service using Bitbucket Pipelines provides a robust and automated deployment workflow. The key challenges around path mapping and web.config configuration can be overcome with proper configuration.
By following this guide, you'll have a reliable CI/CD pipeline that builds, tests, and deploys your frontend application to Azure App Service, with proper routing for single-page applications.
Remember that the most critical parts are:
- The correct web.config configuration
- Proper understanding of Azure's virtual and physical path mapping
- Secure handling of Azure credentials
With these in place, your deployments should be smooth and reliable.
Top comments (0)