Deploying a Quarto Site to Azure Storage Accounts
This appendix provides a comprehensive guide to deploying your Quarto documentation site to Azure Storage Account Static Website hosting, including setup, configuration, CDN integration, and automated deployment workflows.
📋 Table of Contents
- 📖 Overview
- ✅ Prerequisites
- 🏗️ Architecture Overview
- ⚙️ Setup Azure Infrastructure
- 🚀 Deployment Methods
- 🌐 Azure CDN Integration
- 🌍 Custom Domain Configuration
- 🔧 Advanced Configuration
- 📊 Monitoring and Optimization
- 🔍 Troubleshooting Common Issues
- 💰 Cost Optimization
- 🔄 Migration from Other Platforms
- 🎯 Conclusion
- 📚 Additional Resources
📖 Overview
Azure Storage Account Static Website hosting provides a cost-effective, scalable solution for hosting Quarto documentation sites.
It offers excellent performance, global distribution via Azure CDN, and seamless integration with Azure DevOps and GitHub Actions.
Key Benefits
- 💰 Cost-effective: Pay only for storage and bandwidth used
- 🚀 High performance: Built-in CDN integration
- 🌍 Global distribution: Azure’s worldwide infrastructure
- 🔒 Secure: HTTPS by default, custom domain support
- 🔄 CI/CD integration: Works with Azure DevOps, GitHub Actions
- 📈 Scalable: Handles traffic spikes automatically
✅ Prerequisites
Before deploying to Azure Storage, ensure you have:
- Azure subscription with appropriate permissions
- Azure CLI or Azure PowerShell installed
- Quarto installed locally
- Basic understanding of Azure services
- Repository with your Quarto project (GitHub, Azure DevOps, etc.)
🏗️ Architecture Overview
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ Source Code │───▶│ Build Pipeline │───▶│ Azure Storage │
│ (GitHub/DevOps) │ │ (GitHub Actions/ │ │ Static Website │
└─────────────────┘ │ Azure DevOps) │ └─────────────────┘
└──────────────────┘ │
▼
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ Custom Domain │◀───│ Azure CDN │◀───│ $web Container│
│ (docs.site.com)│ │ (Optional but │ │ (HTML files) │
└─────────────────┘ │ Recommended) │ └─────────────────┘
└──────────────────┘
⚙️ Setup Azure Infrastructure
Step 1: Create Storage Account
Using Azure CLI
# Set variables
RESOURCE_GROUP="docs-rg"
STORAGE_ACCOUNT="yourdocsstorage" # Must be globally unique
LOCATION="East US"
# Create resource group
az group create --name $RESOURCE_GROUP --location "$LOCATION"
# Create storage account
az storage account create \
--name $STORAGE_ACCOUNT \
--resource-group $RESOURCE_GROUP \
--location "$LOCATION" \
--sku Standard_LRS \
--kind StorageV2 \
--access-tier Hot
# Enable static website hosting
az storage blob service-properties update \
--account-name $STORAGE_ACCOUNT \
--static-website \
--index-document index.html \
--404-document 404.htmlUsing Azure PowerShell
# Set variables
$ResourceGroupName = "docs-rg"
$StorageAccountName = "yourdocsstorage" # Must be globally unique
$Location = "East US"
# Create resource group
New-AzResourceGroup -Name $ResourceGroupName -Location $Location
# Create storage account
$storageAccount = New-AzStorageAccount `
-ResourceGroupName $ResourceGroupName `
-Name $StorageAccountName `
-Location $Location `
-SkuName "Standard_LRS" `
-Kind "StorageV2" `
-AccessTier Hot
# Enable static website hosting
Enable-AzStorageStaticWebsite `
-Context $storageAccount.Context `
-IndexDocument "index.html" `
-ErrorDocument404Path "404.html"Step 2: Configure Quarto for Azure Deployment
Update your _quarto.yml for Azure hosting:
project:
type: website
output-dir: docs
website:
title: "Your Documentation Site"
site-url: "https://yourdocsstorage.z13.web.core.windows.net" # Your storage URL
description: "Technical documentation hosted on Azure"
navbar:
background: primary
search: true
left:
- href: index.qmd
text: Home
right:
- icon: github
href: "https://github.com/username/repository"
format:
html:
theme: cosmo
toc: true
anchor-sections: true
smooth-scroll: true
code-copy: true
html-math-method: katex
link-external-newwindow: true
# Optimize for CDN caching
embed-resources: false
minimal: false🚀 Deployment Methods
Method 1: GitHub Actions Deployment
Create .github/workflows/deploy-azure-storage.yml:
name: Deploy Quarto Site to Azure Storage
on:
push:
branches: [main]
workflow_dispatch:
env:
AZURE_STORAGE_ACCOUNT: yourdocsstorage
AZURE_STORAGE_CONTAINER: $web
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Quarto
uses: quarto-dev/quarto-actions/setup@v2
with:
version: 'release'
- name: Install dependencies
run: |
# Add any additional dependencies
# pip install -r requirements.txt
# npm install
- name: Render Quarto project
run: quarto render
- name: Login to Azure
uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Upload to Azure Storage
uses: azure/CLI@v1
with:
inlineScript: |
# Remove existing files (optional - for clean deployment)
az storage blob delete-batch \
--account-name $AZURE_STORAGE_ACCOUNT \
--source $AZURE_STORAGE_CONTAINER \
--pattern "*"
# Upload new files
az storage blob upload-batch \
--account-name $AZURE_STORAGE_ACCOUNT \
--destination $AZURE_STORAGE_CONTAINER \
--source ./docs \
--overwrite true
# Set content types for proper serving
az storage blob upload-batch \
--account-name $AZURE_STORAGE_ACCOUNT \
--destination $AZURE_STORAGE_CONTAINER \
--source ./docs \
--pattern "*.html" \
--content-type "text/html" \
--overwrite true
az storage blob upload-batch \
--account-name $AZURE_STORAGE_ACCOUNT \
--destination $AZURE_STORAGE_CONTAINER \
--source ./docs \
--pattern "*.css" \
--content-type "text/css" \
--overwrite true
az storage blob upload-batch \
--account-name $AZURE_STORAGE_ACCOUNT \
--destination $AZURE_STORAGE_CONTAINER \
--source ./docs \
--pattern "*.js" \
--content-type "text/javascript" \
--overwrite true
- name: Purge CDN Cache (if using CDN)
uses: azure/CLI@v1
with:
inlineScript: |
# Replace with your CDN profile and endpoint names
az cdn endpoint purge \
--resource-group ${{ env.RESOURCE_GROUP }} \
--profile-name your-cdn-profile \
--name your-endpoint \
--content-paths "/*"
continue-on-error: trueSetting up Azure Service Principal
- Create Service Principal:
az ad sp create-for-rbac \
--name "QuartoDeployment" \
--role contributor \
--scopes /subscriptions/{subscription-id}/resourceGroups/{resource-group} \
--sdk-auth- Add GitHub Secret:
- Go to GitHub repository settings
- Add secret named
AZURE_CREDENTIALS - Paste the JSON output from the service principal creation
Method 2: Azure DevOps Pipeline
Create azure-pipelines.yml:
trigger:
- main
pool:
vmImage: 'ubuntu-latest'
variables:
storageAccountName: 'yourdocsstorage'
containerName: '$web'
stages:
- stage: Build
displayName: 'Build Quarto Site'
jobs:
- job: Build
displayName: 'Build'
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: '3.x'
addToPath: true
- script: |
# Install Quarto
curl -LO https://quarto.org/download/latest/quarto-linux-amd64.deb
sudo dpkg -i quarto-linux-amd64.deb
displayName: 'Install Quarto'
- script: |
quarto render
displayName: 'Render Quarto Project'
- task: PublishBuildArtifacts@1
inputs:
pathtoPublish: 'docs'
artifactName: 'quarto-site'
- stage: Deploy
displayName: 'Deploy to Azure Storage'
dependsOn: Build
condition: succeeded()
jobs:
- deployment: Deploy
displayName: 'Deploy'
environment: 'production'
strategy:
runOnce:
deploy:
steps:
- task: DownloadBuildArtifacts@0
inputs:
buildType: 'current'
downloadType: 'single'
artifactName: 'quarto-site'
downloadPath: '$(System.ArtifactsDirectory)'
- task: AzureCLI@2
displayName: 'Deploy to Storage Account'
inputs:
azureSubscription: 'your-service-connection'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
# Upload files to storage account
az storage blob upload-batch \
--account-name $(storageAccountName) \
--destination $(containerName) \
--source $(System.ArtifactsDirectory)/quarto-site \
--overwrite true
# Set proper content types
az storage blob upload-batch \
--account-name $(storageAccountName) \
--destination $(containerName) \
--source $(System.ArtifactsDirectory)/quarto-site \
--pattern "*.html" \
--content-type "text/html" \
--overwrite trueMethod 3: Manual Deployment with Azure CLI
For quick testing or one-off deployments:
# Render site locally
quarto render
# Upload to Azure Storage
az storage blob upload-batch \
--account-name yourdocsstorage \
--destination '$web' \
--source ./docs \
--overwrite true
# Set content types
az storage blob upload-batch \
--account-name yourdocsstorage \
--destination '$web' \
--source ./docs \
--pattern "*.html" \
--content-type "text/html" \
--overwrite true
az storage blob upload-batch \
--account-name yourdocsstorage \
--destination '$web' \
--source ./docs \
--pattern "*.css" \
--content-type "text/css" \
--overwrite true🌐 Azure CDN Integration
Why Use Azure CDN?
- Global Performance: Content cached at edge locations worldwide
- Custom Domain Support: Use your own domain with SSL
- Compression: Automatic gzip compression
- Caching Control: Fine-grained cache control
- DDoS Protection: Built-in protection against attacks
Setting up Azure CDN
# Create CDN profile
az cdn profile create \
--name "docs-cdn-profile" \
--resource-group $RESOURCE_GROUP \
--sku Standard_Microsoft
# Create CDN endpoint
az cdn endpoint create \
--name "docs-endpoint" \
--profile-name "docs-cdn-profile" \
--resource-group $RESOURCE_GROUP \
--origin yourdocsstorage.z13.web.core.windows.net \
--origin-host-header yourdocsstorage.z13.web.core.windows.net
# Configure caching rules
az cdn endpoint rule add \
--name "docs-endpoint" \
--profile-name "docs-cdn-profile" \
--resource-group $RESOURCE_GROUP \
--order 1 \
--rule-name "CacheHTML" \
--match-variable RequestUri \
--operator EndsWith \
--match-values "*.html" \
--action-name CacheExpiration \
--cache-behavior Override \
--cache-duration "1.00:00:00" # 1 dayCDN Configuration for Quarto Sites
# Set up compression
az cdn endpoint update \
--name "docs-endpoint" \
--profile-name "docs-cdn-profile" \
--resource-group $RESOURCE_GROUP \
--content-types-to-compress \
"text/html" \
"text/css" \
"application/javascript" \
"text/javascript" \
"application/json" \
"text/plain" \
--is-compression-enabled true
# Configure HTTPS redirect
az cdn endpoint update \
--name "docs-endpoint" \
--profile-name "docs-cdn-profile" \
--resource-group $RESOURCE_GROUP \
--https-redirect Enabled🌍 Custom Domain Configuration
Step 1: Add Custom Domain to CDN
# Add custom domain to CDN endpoint
az cdn custom-domain create \
--name "docs-domain" \
--endpoint-name "docs-endpoint" \
--profile-name "docs-cdn-profile" \
--resource-group $RESOURCE_GROUP \
--hostname "docs.yoursite.com"
# Enable HTTPS on custom domain
az cdn custom-domain enable-https \
--name "docs-domain" \
--endpoint-name "docs-endpoint" \
--profile-name "docs-cdn-profile" \
--resource-group $RESOURCE_GROUPStep 2: DNS Configuration
Configure your DNS provider:
# CNAME record for subdomain
docs.yoursite.com. CNAME docs-endpoint.azureedge.net.
# Or for apex domain, use Azure DNS
@. ALIAS docs-endpoint.azureedge.net.
Step 3: Update Quarto Configuration
website:
site-url: "https://docs.yoursite.com" # Your custom domain🔧 Advanced Configuration
Environment-Specific Deployments
Create different storage accounts for different environments:
# _quarto-dev.yml
website:
site-url: "https://devdocsstorage.z13.web.core.windows.net"
# _quarto-prod.yml
website:
site-url: "https://docs.yoursite.com"Deploy with environment-specific configuration:
# Development
quarto render --profile dev
# Production
quarto render --profile prodSecurity Headers
Add security headers using CDN rules:
# Add security headers via CDN rules
az cdn endpoint rule add \
--name "docs-endpoint" \
--profile-name "docs-cdn-profile" \
--resource-group $RESOURCE_GROUP \
--order 2 \
--rule-name "SecurityHeaders" \
--action-name ModifyResponseHeader \
--header-action Append \
--header-name "X-Frame-Options" \
--header-value "DENY"
az cdn endpoint rule add \
--name "docs-endpoint" \
--profile-name "docs-cdn-profile" \
--resource-group $RESOURCE_GROUP \
--order 3 \
--rule-name "ContentSecurityPolicy" \
--action-name ModifyResponseHeader \
--header-action Append \
--header-name "Content-Security-Policy" \
--header-value "default-src 'self'; script-src 'self' 'unsafe-inline';"Analytics Integration
Enable Azure Application Insights for detailed analytics:
<!-- Add to _includes/analytics.html -->
<script type="text/javascript">
var appInsights=window.appInsights||function(a){
function b(a){c[a]=function(){var b=arguments;c.queue.push(function(){c[a].apply(c,b)})}}var c={config:a},d=document,e=window;setTimeout(function(){var b=d.createElement("script");b.src=a.url||"https://az416426.vo.msecnd.net/scripts/a/ai.0.js",d.getElementsByTagName("script")[0].parentNode.appendChild(b)});try{c.cookie=d.cookie}catch(a){}c.queue=[];for(var f=["Event","Exception","Metric","PageView","Trace","Dependency"];f.length;)b("track"+f.pop());if(b("setAuthenticatedUserContext"),b("clearAuthenticatedUserContext"),b("startTrackEvent"),b("stopTrackEvent"),b("startTrackPage"),b("stopTrackPage"),b("flush"),!a.disableExceptionTracking){f="onerror",b("_"+f);var g=e[f];e[f]=function(a,b,d,e,h){var i=g&&g(a,b,d,e,h);return!0!==i&&c["_"+f](a,b,d,e,h),i}}return c
}({
instrumentationKey: "YOUR_INSTRUMENTATION_KEY"
});
window.appInsights=appInsights,appInsights.queue&&0===appInsights.queue.length&&appInsights.trackPageView();
</script>Include in Quarto configuration:
format:
html:
include-in-header:
- _includes/analytics.html📊 Monitoring and Optimization
Cost Monitoring
Set up cost alerts in Azure:
# Create budget for storage account
az consumption budget create \
--budget-name "docs-site-budget" \
--amount 50 \
--resource-group $RESOURCE_GROUP \
--time-grain Monthly \
--start-date "2025-01-01T00:00:00Z" \
--end-date "2025-12-31T00:00:00Z"Performance Monitoring
Monitor your site performance:
- Azure Monitor: Set up alerts for storage account metrics
- Application Insights: Track user behavior and performance
- CDN Analytics: Monitor cache hit ratios and bandwidth usage
Backup and Disaster Recovery
# Enable soft delete for blobs
az storage account blob-service-properties update \
--account-name $STORAGE_ACCOUNT \
--enable-delete-retention true \
--delete-retention-days 30
# Enable versioning
az storage account blob-service-properties update \
--account-name $STORAGE_ACCOUNT \
--enable-versioning true🔍 Troubleshooting Common Issues
1. Files Not Serving Correctly
Problem: HTML files download instead of displaying.
Solution: Set correct content types during upload:
az storage blob upload-batch \
--account-name $STORAGE_ACCOUNT \
--destination '$web' \
--source ./docs \
--pattern "*.html" \
--content-type "text/html"2. CDN Not Updating
Problem: Changes don’t appear due to CDN caching.
Solution: Purge CDN cache after deployment:
az cdn endpoint purge \
--resource-group $RESOURCE_GROUP \
--profile-name "docs-cdn-profile" \
--name "docs-endpoint" \
--content-paths "/*"3. Custom Domain SSL Issues
Problem: HTTPS not working on custom domain.
Solutions:
# Check certificate status
az cdn custom-domain show \
--name "docs-domain" \
--endpoint-name "docs-endpoint" \
--profile-name "docs-cdn-profile" \
--resource-group $RESOURCE_GROUP
# Validate domain ownership
az cdn custom-domain list \
--endpoint-name "docs-endpoint" \
--profile-name "docs-cdn-profile" \
--resource-group $RESOURCE_GROUP💰 Cost Optimization
Storage Costs
- Use Hot access tier for frequently accessed documentation
- Enable lifecycle management for old versions
- Monitor storage usage with Azure Cost Management
CDN Costs
- Configure appropriate caching rules to maximize cache hit ratios
- Use compression to reduce bandwidth costs
- Consider geo-filtering if your audience is in specific regions
🔄 Migration from Other Platforms
From GitHub Pages
- Export/clone your repository
- Set up Azure Storage Account
- Update
site-urlin_quarto.yml - Configure new deployment pipeline
From Netlify
- Download site files or use Git repository
- Update build commands for Azure deployment
- Migrate environment variables to Azure Key Vault
- Set up custom domain in Azure CDN
🎯 Conclusion
Azure Storage Account static website hosting provides a robust, scalable, and cost-effective solution for hosting Quarto documentation sites. Key advantages include:
- Global Performance: CDN integration for worldwide content delivery
- Enterprise Integration: Seamless integration with Azure DevOps and other Azure services
- Cost Control: Pay-per-use pricing with predictable costs
- Security: Built-in security features and custom domain SSL support
- Scalability: Automatic scaling to handle traffic spikes
This solution is ideal for:
- Enterprise documentation requiring integration with Azure services
- High-traffic sites needing global CDN distribution
- Multi-environment deployments (dev, staging, production)
- Sites requiring advanced monitoring and analytics