Experts Inside Blog

Deploy to Azure Local with DevOps

Geschrieben von Thomas Torggler | Dec 17, 2025 2:16:40 PM

 

Deploy to Azure Local with DevOps

The hybrid cloud isn’t just a buzzword—it’s the reality for most organizations. While public cloud offers flexibility and global reach, some workloads need to stay on-premises for compliance, latency, or cost reasons. With DevOps, we can deploy our applications on either environment. On Azure, the deployment targets function apps, static web sites, app services, or other service. On Azure Local we’re using a simple docker host instead.

This post describes a recent demo where I showed how the same application / code base can easily be deployed globally on Azure and locally on Azure Local.

 

The Hybrid Reality

Most enterprises operate in a hybrid world. You might have:

  • Sensitive workloads that must stay on-premises for compliance
  • Low-latency applications that need to be close to users or data sources
  • Cost-sensitive workloads where cloud egress or compute costs don’t make sense

Traditionally, on-premises and cloud are treated as separate worlds with different tools, different processes, different expertise. This creates friction, slows delivery, and increases the operational burden on teams.

 

PaaS on Azure Local With Docker

I wanted to deploy the same code I was using for my demo on Azure but Azure Local doesn’t provide Function Apps or Static Web Apps (yet). Could I use the same DevOps practices everywhere and just deploy to Docker instead of those Azure services? The same pipelines, the same container workflows, the same deployment patterns—just executing in different locations based on workload requirements. The answer is yes.

This is exactly what Azure Local enables. By running Azure’s infrastructure capabilities on your own hardware, you get a consistent management plane across cloud and on-premises. Combined with self-hosted Azure DevOps agents and Docker, you can build a deployment model that feels like cloud but runs locally.

 

A Simple Example

My sample application is a static web site with a nodejs backend for some server-side features like role verification and search. On Azure it runs as a Static Web App with a managed backend (Function App).

Azure Local supports VM and Kubernetes workloads natively. While Kubernetes is powerful, it comes with significant complexity which I’m happy to avoid for this example.

For single-node or small-scale deployments, Docker Compose provides everything we need:

┌─────────────────────────────────────────────────────────────┐
│ Ubuntu VM with Docker on Azure Local │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Frontend │◄──►│ API │◄──►│ Database │ │
│ │ Container │ │ Container │ │ Container │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │ │ │ │
│ └──────────────────┼──────────────────┘ │
│ │ │
│ ┌─────────▼─────────┐ │
│ │ Self-Hosted Agent │ │
│ │ (Azure DevOps) │ │
│ └─────────┬─────────┘ │
└──────────────────────────────┼──────────────────────────────┘

┌────────▼────────┐
│ Azure DevOps │
│ Pipeline │
└─────────────────┘

To make it even simpler, I have the self-hosted agent running directly on my Docker host. This eliminates the need for container registries and works just fine for a demo or local-first setup. Images are built and run on the same machine making deployments fast, and keeping data secure.

 

Building The Function App

Microsoft provide the base image that Function Apps use in the public Microsoft Container Registry, all I had to do was use a simple Dockerfile to build an image that can then be run on my local Docker Host.

 # Use the Azure Functions base image for Node.js
FROM mcr.microsoft.com/azure-functions/node:4-node20

# Set working directory
WORKDIR /home/site/wwwroot

# Copy package files
COPY package*.json ./

# Install dependencies
RUN npm install

# Copy function app files
COPY . .

# Expose the default Azure Functions port
EXPOSE 80

Running docker compose build will download the function app image, install dependencies, and finally copy my backend code into the image. This can then be run on any Docker host with docker run or be part of an application with docker compose.

 

Building The Web App

The Static Web App service is taking care of the build process on Azure (thanks to Oryx). For my local deployment, I made another very simple Dockerfile, that builds the app, and serves it with nginx.

 # Stage 1: Build Jekyll site
FROM ruby:3.2-alpine AS builder

# Install build dependencies
RUN apk add --no-cache \
build-base \
git \
nodejs \
npm

# Set working directory
WORKDIR /site

# Copy Gemfile and install dependencies
COPY frontend/Gemfile* ./
RUN bundle update
RUN bundle install

# Copy the rest of the site and build
COPY frontend/ ./
RUN bundle exec jekyll build

# Stage 2: Serve with nginx
FROM nginx:alpine

# Copy built site from builder stage
COPY --from=builder /site/_site /usr/share/nginx/html

# Copy custom nginx configuration
COPY nginx.conf /etc/nginx/conf.d/default.conf

EXPOSE 80

 

Multi-Container Applications with Docker Compose

Docker Compose enables the definition of multi-container applications declaratively:

 services:
frontend:
build: ./frontend
ports:
- "8080:80"
networks:
- app-network
api:
build: ./api
ports:
- "7071:80"
networks:
- app-network
networks:
app-network:
driver: bridge

This single file captures my entire application topology. The same definition works in development, CI/CD, and production.

 

Deployment Strategy

The deployment approach prioritizes reliability over complexity:

  1. Build new images while existing containers continue running
  2. Stop old containers with docker compose down
  3. Start new containers with docker compose up -d
  4. Cleanup old images to manage disk space


Pipeline Structure

The Azure DevOps pipeline reflects this simplicity:

 trigger:
branches:
include:
- main

pool: azlocal # Self-hosted agent pool

stages:
- stage: Build
jobs:
- job: BuildJob
steps:
- script: docker compose build
displayName: 'Build Images'

- stage: Deploy
dependsOn: Build
jobs:
- deployment: DeployJob
environment: 'production'
strategy:
runOnce:
deploy:
steps:
- script: docker compose down && docker compose up -d
displayName: 'Deploy'