{"id":2012,"date":"2025-06-10T03:52:45","date_gmt":"2025-06-10T03:52:45","guid":{"rendered":"https:\/\/www.nicktailor.com\/?p=2012"},"modified":"2025-07-11T03:23:13","modified_gmt":"2025-07-11T03:23:13","slug":"how-to-deploy-a-node-js-app-to-azure-app-service-with-ci-cd","status":"publish","type":"post","link":"https:\/\/nicktailor.com\/tech-blog\/how-to-deploy-a-node-js-app-to-azure-app-service-with-ci-cd\/","title":{"rendered":"How to Deploy a Node.js App to Azure App Service with CI\/CD"},"content":{"rendered":"<h2>Option A: Code-Based Deployment (Recommended for Most Users)<\/h2>\n<p>If you don\u2019t need a custom runtime or container, Azure\u2019s built-in code deployment option is the fastest and easiest way to host production-ready Node.js applications. Azure provides a managed environment with runtime support for Node.js, and you can automate everything using Azure DevOps.<\/p>\n<p>This option is ideal for most production use cases that:<\/p>\n<ul>\n<li>Use standard versions of Node.js (or Python, .NET, PHP)<\/li>\n<li>Don\u2019t require custom OS packages or NGINX proxies<\/li>\n<li>Want quick setup and managed scaling<\/li>\n<\/ul>\n<p>This section covers everything you need to deploy your Node.js app using Azure\u2019s built-in runtime and set it up for CI\/CD in Azure DevOps.<\/p>\n<h3>Step 0: Prerequisites and Permissions<\/h3>\n<p>Before starting, make sure you have the following:<\/p>\n<ol>\n<li><strong>Azure Subscription<\/strong> with Contributor access<\/li>\n<li><strong>Azure CLI<\/strong> installed and authenticated (<code>az login<\/code>)<\/li>\n<li><strong>Azure DevOps Organization &amp; Project<\/strong><\/li>\n<li><strong>Code repository<\/strong> in Azure Repos or GitHub (we\u2019ll use Azure Repos)<\/li>\n<li>A <strong>user with the following roles<\/strong>:\n<ul>\n<li>Contributor on the Azure resource group<\/li>\n<li>Project Administrator or Build Administrator in Azure DevOps (to create pipelines and service connections)<\/li>\n<\/ul>\n<\/li>\n<\/ol>\n<h3>Step 1: Create an Azure Resource Group<\/h3>\n<pre style=\"background: #f5f5f5; padding: 1em; border-radius: 6px; overflow: auto;\"><code>az group create --name prod-rg --location eastus<\/code><\/pre>\n<h3>Step 2: Choose Your Deployment Model<\/h3>\n<p>There are two main ways to deploy to Azure App Service:<\/p>\n<ul>\n<li><strong>Code-based<\/strong>: Azure manages the runtime (Node.js, Python, etc.)<\/li>\n<li><strong>Docker-based<\/strong>: You provide a custom Docker image<\/li>\n<\/ul>\n<h4>Option A: Code-Based App Service Plan<\/h4>\n<pre style=\"background: #f5f5f5; padding: 1em; border-radius: 6px; overflow: auto;\"><code>az appservice plan create \\\n  --name prod-app-plan \\\n  --resource-group prod-rg \\\n  --sku P1V2 \\\n  --is-linux<\/code><\/pre>\n<ul>\n<li><code>az appservice plan create<\/code>: Command to create a new App Service Plan (defines compute resources)<\/li>\n<li><code>--name prod-app-plan<\/code>: The name of the service plan to create<\/li>\n<li><code>--resource-group prod-rg<\/code>: The name of the resource group where the plan will reside<\/li>\n<li><code>--sku P1V2<\/code>: The pricing tier (Premium V2, small instance). Includes autoscaling, staging slots, etc.<\/li>\n<li><code>--is-linux<\/code>: Specifies the operating system for the app as Linux (required for Node.js apps)<\/li>\n<\/ul>\n<h4>Create Web App with Built-In Node Runtime<\/h4>\n<pre style=\"background: #f5f5f5; padding: 1em; border-radius: 6px; overflow: auto;\"><code>az webapp create \\\n  --name my-prod-node-app \\\n  --resource-group prod-rg \\\n  --plan prod-app-plan \\\n  --runtime \"NODE|18-lts\"<\/code><\/pre>\n<ul>\n<li><code>az webapp create<\/code>: Creates the actual web app that will host your code<\/li>\n<li><code>--name my-prod-node-app<\/code>: The globally unique name of your app (will be part of the public URL)<\/li>\n<li><code>--resource-group prod-rg<\/code>: Assigns the app to the specified resource group<\/li>\n<li><code>--plan prod-app-plan<\/code>: Binds the app to the previously created compute plan<\/li>\n<li><code>--runtime \"NODE|18-lts\"<\/code>: Specifies the Node.js runtime version (Node 18, LTS channel)<\/li>\n<\/ul>\n<h4>Option B: Docker-Based App Service Plan<\/h4>\n<pre style=\"background: #f5f5f5; padding: 1em; border-radius: 6px; overflow: auto;\"><code>az appservice plan create \\\n  --name prod-docker-plan \\\n  --resource-group prod-rg \\\n  --sku P1V2 \\\n  --is-linux<\/code><\/pre>\n<ul>\n<li>Same as Option A \u2014 this creates a Linux-based Premium plan<\/li>\n<li>You can reuse this compute plan for one or more container-based apps<\/li>\n<\/ul>\n<h4>Create Web App Using Custom Docker Image<\/h4>\n<pre style=\"background: #f5f5f5; padding: 1em; border-radius: 6px; overflow: auto;\"><code>az webapp create \\\n  --name my-docker-app \\\n  --resource-group prod-rg \\\n  --plan prod-docker-plan \\\n  --deployment-container-image-name myregistry.azurecr.io\/myapp:latest<\/code><\/pre>\n<ul>\n<li><code>--name my-docker-app<\/code>: A unique name for your app<\/li>\n<li><code>--resource-group prod-rg<\/code>: Associates this web app with your resource group<\/li>\n<li><code>--plan prod-docker-plan<\/code>: Assigns the app to your App Service Plan<\/li>\n<li><code>--deployment-container-image-name<\/code>: Specifies the full path to your Docker image (from ACR or Docker Hub)<\/li>\n<\/ul>\n<p>Use this if you&#8217;re building a containerized app and want full control of the runtime environment. Make sure your image is accessible in Azure Container Registry or Docker Hub.<\/p>\n<h3>Step 3: Prepare Your Azure DevOps Project<\/h3>\n<ol>\n<li>Navigate to <a href=\"https:\/\/dev.azure.com\" target=\"_blank\" rel=\"noopener\">https:\/\/dev.azure.com<\/a><\/li>\n<li>Create a new <strong>Project<\/strong> (e.g., <code>ProdWebApp<\/code>)<\/li>\n<li>Go to <strong>Repos<\/strong> and push your Node.js code:<\/li>\n<\/ol>\n<pre style=\"background: #f5f5f5; padding: 1em; border-radius: 6px; overflow: auto;\"><code>git remote add origin https:\/\/dev.azure.com\/&lt;org&gt;\/&lt;project&gt;\/_git\/my-prod-node-app\ngit push -u origin main<\/code><\/pre>\n<h3>Step 4: Create a Service Connection<\/h3>\n<ol>\n<li>In DevOps, go to <strong>Project Settings &gt; Service connections<\/strong><\/li>\n<li>Click <strong>New service connection &gt; Azure Resource Manager<\/strong><\/li>\n<li>Choose <strong>Service principal (automatic)<\/strong><\/li>\n<li>Select the correct subscription and resource group<\/li>\n<li>Name it something like <code>AzureProdConnection<\/code><\/li>\n<\/ol>\n<h3>Step 5: Create the CI\/CD Pipeline<\/h3>\n<p>Add the following to your repository root as <code>.azure-pipelines.yml<\/code>.<\/p>\n<h4>Code-Based YAML Example<\/h4>\n<pre style=\"background: #f5f5f5; padding: 1em; border-radius: 6px; overflow: auto;\"><code>trigger:\n  branches:\n    include:\n      - main\n\npool:\n  vmImage: 'ubuntu-latest'\n\nstages:\n- stage: Build\n  jobs:\n  - job: BuildApp\n    steps:\n    - task: NodeTool@0\n      inputs:\n        versionSpec: '18.x'\n\n    - script: |\n        npm install\n        npm run build\n      displayName: 'Install and Build'\n\n    - task: ArchiveFiles@2\n      inputs:\n        rootFolderOrFile: '$(System.DefaultWorkingDirectory)'\n        archiveFile: '$(Build.ArtifactStagingDirectory)\/app.zip'\n        includeRootFolder: false\n\n    - task: PublishBuildArtifacts@1\n      inputs:\n        PathtoPublish: '$(Build.ArtifactStagingDirectory)'\n        ArtifactName: 'drop'\n\n- stage: Deploy\n  dependsOn: Build\n  jobs:\n  - deployment: DeployWebApp\n    environment: 'production'\n    strategy:\n      runOnce:\n        deploy:\n          steps:\n          - task: AzureWebApp@1\n            inputs:\n              azureSubscription: 'AzureProdConnection'\n              appName: 'my-prod-node-app'\n              package: '$(Pipeline.Workspace)\/drop\/app.zip'<\/code><\/pre>\n<h4>Docker-Based YAML Example<\/h4>\n<pre style=\"background: #f5f5f5; padding: 1em; border-radius: 6px; overflow: auto;\"><code>trigger:\n  branches:\n    include:\n      - main\n\npool:\n  vmImage: 'ubuntu-latest'\n\nstages:\n- stage: Deploy\n  jobs:\n  - deployment: DeployContainer\n    environment: 'production'\n    strategy:\n      runOnce:\n        deploy:\n          steps:\n          - task: AzureWebAppContainer@1\n            inputs:\n              azureSubscription: 'AzureProdConnection'\n              appName: 'my-docker-app'\n              containers: 'myregistry.azurecr.io\/myapp:latest'<\/code><\/pre>\n<h3>Step 6: Configure Pipeline and Approvals<\/h3>\n<ol>\n<li>Go to <strong>Pipelines &gt; Pipelines &gt; New<\/strong><\/li>\n<li>Select <strong>Azure Repos Git<\/strong>, choose your repo, and point to the YAML file<\/li>\n<li>Click <strong>Run Pipeline<\/strong><\/li>\n<\/ol>\n<p>To add manual approvals:<\/p>\n<ol>\n<li>Go to <strong>Pipelines &gt; Environments<\/strong><\/li>\n<li>Create a new environment named <code>production<\/code><\/li>\n<li>Link the deploy stage to this environment in your YAML:<\/li>\n<\/ol>\n<pre style=\"background: #f5f5f5; padding: 1em; border-radius: 6px; overflow: auto;\"><code>environment: 'production'<\/code><\/pre>\n<ol start=\"4\">\n<li>Enable <strong>approval and checks<\/strong> for production safety<\/li>\n<\/ol>\n<h3>Step 7: Store Secrets (Optional but Recommended)<\/h3>\n<ol>\n<li>Go to <strong>Pipelines &gt; Library<\/strong><\/li>\n<li>Create a new <strong>Variable Group<\/strong> (e.g., <code>ProdSecrets<\/code>)<\/li>\n<li>Add variables like <code>DB_PASSWORD<\/code>, <code>API_KEY<\/code>, and mark them as secret<\/li>\n<li>Reference them in pipeline YAML:<\/li>\n<\/ol>\n<pre style=\"background: #f5f5f5; padding: 1em; border-radius: 6px; overflow: auto;\"><code>variables:\n  - group: 'ProdSecrets'<\/code><\/pre>\n<h3>Troubleshooting Tips<\/h3>\n<table border=\"1\" cellpadding=\"6\">\n<thead>\n<tr>\n<th>Problem<\/th>\n<th>Solution<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Resource group not found<\/td>\n<td>Make sure you created it with <code>az group create<\/code><\/td>\n<\/tr>\n<tr>\n<td>Runtime version not supported<\/td>\n<td>Run <code>az webapp list-runtimes --os linux<\/code> to see current options<\/td>\n<\/tr>\n<tr>\n<td>Pipeline can&#8217;t deploy<\/td>\n<td>Check if the service connection has Contributor role on the resource group<\/td>\n<\/tr>\n<tr>\n<td>Build fails<\/td>\n<td>Make sure you have a valid <code>package.json<\/code> and build script<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<h3>Summary<\/h3>\n<p>By the end of this process, you will have:<\/p>\n<ul>\n<li>A production-grade Node.js app running on Azure App Service<\/li>\n<li>A scalable App Service Plan using Linux and Premium V2 resources<\/li>\n<li>A secure CI\/CD pipeline that automatically builds and deploys from Azure Repos<\/li>\n<li>Manual approval gates and secrets management for enhanced safety<\/li>\n<li>The option to deploy using either Azure-managed runtimes or fully custom Docker containers<\/li>\n<\/ul>\n<p>This setup is ideal for fast-moving<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Option A: Code-Based Deployment (Recommended for Most Users) If you don\u2019t need a custom runtime or container, Azure\u2019s built-in code deployment option is the fastest and easiest way to host production-ready Node.js applications. Azure provides a managed environment with runtime support for Node.js, and you can automate everything using Azure DevOps. This option is ideal for most production use cases<a href=\"https:\/\/nicktailor.com\/tech-blog\/how-to-deploy-a-node-js-app-to-azure-app-service-with-ci-cd\/\" class=\"read-more\">Read More &#8230;<\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[146,138],"tags":[],"class_list":["post-2012","post","type-post","status-publish","format-standard","hentry","category-azure","category-linux"],"_links":{"self":[{"href":"https:\/\/nicktailor.com\/tech-blog\/wp-json\/wp\/v2\/posts\/2012","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/nicktailor.com\/tech-blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/nicktailor.com\/tech-blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/nicktailor.com\/tech-blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/nicktailor.com\/tech-blog\/wp-json\/wp\/v2\/comments?post=2012"}],"version-history":[{"count":7,"href":"https:\/\/nicktailor.com\/tech-blog\/wp-json\/wp\/v2\/posts\/2012\/revisions"}],"predecessor-version":[{"id":2023,"href":"https:\/\/nicktailor.com\/tech-blog\/wp-json\/wp\/v2\/posts\/2012\/revisions\/2023"}],"wp:attachment":[{"href":"https:\/\/nicktailor.com\/tech-blog\/wp-json\/wp\/v2\/media?parent=2012"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/nicktailor.com\/tech-blog\/wp-json\/wp\/v2\/categories?post=2012"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/nicktailor.com\/tech-blog\/wp-json\/wp\/v2\/tags?post=2012"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}