I really like Microsoft Azure, but the process of deploying an Angular 6+ app is complex, and filled with some very big hidden gotchas. As of this writing, you cannot just spin up a web app instance, push Angular code to it from a repository and have it run; you need to take several steps first.
Here are the essential steps. Note that another and in many cases better approach is to use containers, such as Docker, but I’m not going to cover that here. In this post, I’ll walk through running your Angular app directly on an Azure web app instance.
Before you begin
Make sure your production build is working on your local machine:
ng build --prod
This command should create a dist folder right off the root of your Angular project. If it doesn’t build on your machine, it’s not going to build once it’s deployed to Azure. Normally in development you’re using:
ng serve
but this only runs Angular on the development server, which is not suitable for production environments.
Jot down a couple things from your local build machine. Let’s find out which version of Node you are running (and npm version), because you’ll want to replicate it on Azure. On your local machine, open up a command shell, and type:
node -v
In my case, I get: v10.6.0 at this writing. So I’ll be remembering “10.6.0” as a value when I tell Azure what version of Node I want it to use in the “Application Settings” step below.
Make these modifications to your local Angular app:
- Azure’s going to need to know how to build your app. To do this, open up package.json and copy a few “devDependencies” into the main root “dependencies” section. Here are the ones that were important to my project’s successful on-Azure build:
"dependencies": { "@angular-devkit/build-angular": "~0.6.8", "@angular/animations": "^7.2.2", "@angular/cdk": "^7.3.0", "@angular/cli": "^7.2.2", "@angular/common": "^7.2.2", "@angular/compiler": "^7.2.2", "@angular/compiler-cli": "^7.2.2", ... }
While still in package.json, look at the “scripts” section toward the top of the file. Make you have an entry for “build” : “ng build –prod”. You’ll see in the deploy.cmd file below that we rely upon that command to do the actual build on Azure. You do not commit the dist folder to GitHub; rather, you commit the code source, and Azure first copies over the GitHub code, then it runs an ng build –prod, and it copies the results of the dist folder to the Azure production directory that you define in Application Settings.
"scripts": { "ng": "ng", "build": "ng build --prod", "test": "ng test", "lint": "ng lint", "e2e": "ng e2e" }
Note too that the “start” command was removed, which seems to confuse Azure; you don’t want Azure trying to “ng serve” your application, because that would try to invoke the development server, which is not suitable for production.
Let’s Create an Azure App
We need a Web App instance to work with. We will then configure the Application Settings to tell it which version of Node to run, what kind of deployment script to run after it sucks in all those files from GitHub when you publish a commit, and we will tell Azure where to look for the app directory.
- Using the Azure Portal, create a web app.
- Once that’s deployed, go to the Azure Application Settings for that web app, and update two key variables:
- App setting name: “WEBSITE_NODE_DEFAULT_VERSION” Set this value to indicate what version of Node Azure should spin up for you. I like to match my local dev machine exactly (nope, folks, I’m not yet using Docker for small deployments.) You can find this value on your local machine by opening up a command prompt and typing “node -v”.For me, at this writing, it’s 10.6.0, so that’s what I set the value of this variable to in Azure.
- You also need to tell Azure where to find the code to run, and it’s not going to save in the default directory — it’s going to save in the default directory appended with your project name. At the very bottom of the Application Settings area, you’ll see a section called “Virtual applications and directories”. You need to tell Azure where to go for the main virtual path of your site. It will generally be site\wwwroot\{your-angular-project-name} That is, that’s what it will be if you follow the custom DEPLOY.CMD command below.
- App setting name: “WEBSITE_NODE_DEFAULT_VERSION” Set this value to indicate what version of Node Azure should spin up for you. I like to match my local dev machine exactly (nope, folks, I’m not yet using Docker for small deployments.) You can find this value on your local machine by opening up a command prompt and typing “node -v”.For me, at this writing, it’s 10.6.0, so that’s what I set the value of this variable to in Azure.
- Next, you’ll need to teach Azure’s web service how to build and deploy your app, once it pulls in all that code from GitHub.
To do this, you create two files — one called “.deployment” and the other called “deploy.cmd.”
In root level of your Angular project, you’ll need to create a custom deploy.cmd file which builds the “dist” folder and uses that as the main production code.
To do this, we want to use a custom deploy.cmd, not just the standard Azure one. It does a production build, and then uses the results of the standard Angular dist folder as the main website code.
Here’s what the files look like. The first filename .deployment (note the period before the word), and it belongs in the root of your Angular project. This simply looks like this:
[config] command = deploy.cmd
The second filename is the actual deployment script, called deploy.cmd, and it looks like this below. Note in particular line 61, which copies from the dist folder after completing that production build.
Note that this file below not the plain vanilla deploy.cmd — it’s been slightly modified.
@if "%SCM_TRACE_LEVEL%" NEQ "4" @echo off:: ---------------------- :: KUDU Deployment Script :: Version: 1.0.17 :: ----------------------:: Prerequisites :: -------------:: Verify node.js installed where node 2>nul >nul IF %ERRORLEVEL% NEQ 0 ( echo Missing node.js executable, please install node.js, if already installed make sure it can be reached from current environment. goto error ) :: Setup :: ----- setlocal enabledelayedexpansion SET ARTIFACTS=%~dp0%..\artifacts IF NOT DEFINED DEPLOYMENT_SOURCE ( SET DEPLOYMENT_SOURCE=%~dp0%. ) IF NOT DEFINED DEPLOYMENT_TARGET ( SET DEPLOYMENT_TARGET=%ARTIFACTS%\wwwroot ) IF NOT DEFINED NEXT_MANIFEST_PATH ( SET NEXT_MANIFEST_PATH=%ARTIFACTS%\manifest IF NOT DEFINED PREVIOUS_MANIFEST_PATH ( SET PREVIOUS_MANIFEST_PATH=%ARTIFACTS%\manifest ) ) IF NOT DEFINED KUDU_SYNC_CMD ( :: Install kudu sync echo Installing Kudu Sync call npm install kudusync -g --silent IF !ERRORLEVEL! NEQ 0 goto error :: Locally just running "kuduSync" would also work SET KUDU_SYNC_CMD=%appdata%\npm\kuduSync.cmd ) goto Deployment :: Utility Functions :: ----------------- :SelectNodeVersion IF DEFINED KUDU_SELECT_NODE_VERSION_CMD ( :: The following are done only on Windows Azure Websites environment call %KUDU_SELECT_NODE_VERSION_CMD% "%DEPLOYMENT_SOURCE%" "%DEPLOYMENT_TARGET%" "%DEPLOYMENT_TEMP%" IF !ERRORLEVEL! NEQ 0 goto error IF EXIST "%DEPLOYMENT_TEMP%\__nodeVersion.tmp" ( SET /p NODE_EXE=<"%DEPLOYMENT_TEMP%\__nodeVersion.tmp" IF !ERRORLEVEL! NEQ 0 goto error ) IF EXIST "%DEPLOYMENT_TEMP%\__npmVersion.tmp" ( SET /p NPM_JS_PATH=<"%DEPLOYMENT_TEMP%\__npmVersion.tmp" IF !ERRORLEVEL! NEQ 0 goto error ) IF NOT DEFINED NODE_EXE ( SET NODE_EXE=node ) SET NPM_CMD="!NODE_EXE!" "!NPM_JS_PATH!" ) ELSE ( SET NPM_CMD=npm SET NODE_EXE=node ) goto :EOF :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: :: Deployment :: ---------- :Deployment echo Handling node.js deployment. :: 2. Select node version call :SelectNodeVersion :: 3. Install npm packages IF EXIST "%DEPLOYMENT_SOURCE%\package.json" ( pushd "%DEPLOYMENT_SOURCE%" call :ExecuteCmd !NPM_CMD! install --production IF !ERRORLEVEL! NEQ 0 goto error popd ) :: 3. Angular Prod Build echo Building App next... echo DEPLOYMENT_SOURCE is %DEPLOYMENT_SOURCE% IF EXIST "%DEPLOYMENT_SOURCE%/angular.json" ( echo Building App in %DEPLOYMENT_SOURCE%… pushd "%DEPLOYMENT_SOURCE%" call :ExecuteCmd !NPM_CMD! run build :: If the above command fails comment above and uncomment below one :: call ./node_modules/.bin/ng build --prod IF !ERRORLEVEL! NEQ 0 goto error popd ) :: 1. KuduSync IF /I "%IN_PLACE_DEPLOYMENT%" NEQ "1" ( call :ExecuteCmd "%KUDU_SYNC_CMD%" -v 50 -f "%DEPLOYMENT_SOURCE%/dist" -t "%DEPLOYMENT_TARGET%" -n "%NEXT_MANIFEST_PATH%" -p "%PREVIOUS_MANIFEST_PATH%" -i ".git;.hg;.deployment;deploy.cmd" IF !ERRORLEVEL! NEQ 0 goto error ) :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: goto end :: Execute command routin that will echo out when error :ExecuteCmd setlocal set _CMD_=%* call %_CMD_% if "%ERRORLEVEL%" NEQ "0" echo Failed exitCode=%ERRORLEVEL%, command=%_CMD_% exit /b %ERRORLEVEL% :error endlocal echo An error has occurred during web site deployment. call :exitSetErrorLevel call :exitFromFunction 2>nul :exitSetErrorLevel exit /b 1 :exitFromFunction () :end endlocal echo Finished successfully.
Key Gotcha: both .deployment and deploy.cmd need to go in the ROOT of your GitHub Repository. (e.g., if you’ve got an Angular app project that’s a subfolder of your GitHub repo, still make sure you put those files in the ROOT of your GitHub repo, not the root of the Angular sub project.)
The last steps are fairly straightforward:
- Get your Angular App hosted in a GitHub repository. There are several articles about this, here’s one.
- Tell Azure to deploy the app from GitHub (article.) Essentially, you go to the the Azure Portal, choose the web app, go to “Deployment” and set up “Deploy from GitHub.” It’s pretty straightforward if you already have your code in GitHub.
That’s it! Once you do those steps, you should then be able to get code working locally, commit it to GitHub, wait for a few minutes as Azure fetches the code from GitHub and builds it, and then serves it up on request to users visiting your website. Note again that it’s doing a production build, which is a higher level of code quality checking than the debug builds, so if your build suddenly starts failing on Azure, it’s probably not Azure’s fault, it’s probably because your production build doesn’t build locally. Best to do an ng build –prod before deploying to GitHub.
Of course, this is not a “best practice” for production builds — you should be deploying to one or more staging slots, running your test suites on the staging site, and flipping the slots into production, among other things, but that’s not in the scope of this post.
Routing
If you’re hosted on IIS, you’ll also want this web.config file shipped up to your Azure host so that Angular can take over the routing.
Troubleshooting Tips
- In the “Deployment” blade for your app on Azure, you should see it fetching code from GitHub when you do a checkin to the configured branch. If it doesn’t you might try detaching the connection and re-attaching the GitHub deployment.
- If you see it building and then giving a “Failed” message, check the Logs link and scroll through the output. If it says it cannot find any files in the d:\…\dist directory, it means the build did not succeed on Azure. The log file, as cluttered as it sometimes can be, should tell you about component (or components) that are missing. Add those to the “dependencies” section of package.json and check in a new build. Sometimes the first deployment is a bit of an iteration dance, because it will usually fail after the first one and not report all the missing libraries. Think of Azure as basically starting from scratch in the build process — it starts with a clean working directory each time it pulls builds from GitHub, then runs an ng build –prod to build the dist folder.