Deploying a Client-Side Rendered create-react-app to Microsoft Azure
Deploying a React app to Microsoft Azure is simple. Except that… it isn’t. The devil is in the details. If you’re looking to deploy a create-react-app — or a similar style front-end JavaScript framework that requires pushState
-based routing — to Microsoft Azure, I believe this article will serve you well. We’re going to try to avoid the headaches of client and server side routing reconciliation.
First, a quick story.
Back in 2016, when Donovan Brown, a Senior DevOps Program Manager at Microsoft, had given a “but it works on my machine speech” at Microsoft Connect that year, I was still in my preliminary stages as a web developer. His talk was about micro-services and containers.
[…] Gone are the days when your manager comes running into your office and she is frantic and she has found a bug. And no matter how hard I try, I can’t reproduce it and it works perfectly on my machine. She says: fine Donovan, then we are going to ship your machine because that is the only place where it works. But I like my machine, so I’m not going to let her ship it…
I had a similar sort of challenge, but it had to do with routing. I was working on a website with a React front end and ASP.NET Core back end, hosted as two separate projects that were deployed to Microsoft Azure. This meant we could deploy both apps separately and enjoy the benefits that comes with separation of concern. We also know who to git blame
if and when something goes wrong. But it had downsides as well, as front-end vs. back-end routing reconciliation was one of those downsides.
One day I pushed some new code to our staging servers. I received a message shortly after telling me the website was failing on page refresh. It was throwing a 404 error. At first, I didn’t think it was my responsibility to fix the error. It had to be some server configuration issue. Turns out I was both right and wrong.
I was right to know it was a server configuration issue (though at the time, I didn’t know it had to do with routing). I was wrong to deny it my responsibility. It was only after I went on a web searching rampage that I found a use case for deploying a create-react-app to Azure under the Deployment tab on the official documentation page.
Building React for production
When building a React app for production (assuming we’re are using create-react-app), it’s worth noting the folders that get generated. Running npm run build
will generate a build folder where an optimized static version of the application lives. To get the application on a live server, all we need do is feed the server the content of the build folder. If we were working on localhost, there is no live server involved, so it is not always equivalent to having the application on a live server.
Generally, the build folder will have this structure:
→ build
→ static
→ css
→ css files
→ js
→ js files
→ media
→ media files
→ index.html
→ other files...
Client-side routing with React Router
React Router uses the HTML5 pushState
history API internally. What pushState
does is quite interesting. For example, navigating (or using Link in react router) from the page https://css-tricks.com
to the page https://css-tricks.com/archives/
will cause the URL bar to display https://css-tricks.com/archives/
but won’t cause the browser to load the page /archives
or even check that it exists. Couple this with the component-based model of React, it becomes a thing to change routes while displaying different pages based on those routes — without the all-seeing eye of the server trying to serve a page in its own directory. What happens, then, when we introduce servers by pushing the code to a live server? The docs tell it better:
If you use routers that use the HTML5 pushState history API under the hood (for example, React Router with browserHistory), many static file servers will fail. For example, if you used React Router with a route for /todos/42, the development server will respond to localhost:3000/todos/42 properly, but an Express serving a production build as above will not. This is because when there is a fresh page load for a /todos/42, the server looks for the file build/todos/42 and does not find it. The server needs to be configured to respond to a request to /todos/42 by serving index.html.
Different servers require different configuration. Express, for example, requires this:
app.get('*', (req, res) => {
res.sendFile(path.resolve(__dirname, 'client', 'build', 'index.html'));
});
…as documented in the create-react-app docs. Keep in mind though, this assumes that we’re hosting create-react-app at the server root, which is making use of a wildcard route (*
) that catches all route and respond to all route request by serving the index.html
file in the build folder which sits at the root of the server application. Also, this is tightly coupled with the back-end. If that’s the case, we would most likely have this kind of folder structure (assuming the back-end is in NodeJS):
→ Server
→ Client (this is where your react code goes)
→ build (this is the build folder, after you npm run build)
→ src
→ node_modules
→ package.json
→ other front-end files and folders
→ Other back-end files and folders
Since my front-end (create-react-app) and back-end (ASP.NET) were two different projects, serving static files by navigating the directory was sort of an impossibility.
In fact, since we are deploying a static app, we do not need the back-end. As Burke Holland put it: “Static” means that we aren’t deploying any server code; just the front-end files.
I keep mentioning ASP.NET here because during the course of my research, I figured configuring Azure required a configuration file in a
wwwroot
folder and ASP.NET’s folder structure typically has awwwroot
folder. Remember the application’s back-end was in ASP.NET? But that’s just about it. Thewwwroot
folder seemed to be hidden somewhere on Azure. And I can’t show you without deploying acreate-react-app
. So let’s go do that.
Getting started with App Services on Microsoft Azure
To get started, if you do not already have a Azure account, get a free trial then head over to the Azure portal.
- Navigate to All services ? Web ? App Services
Navigating on the Azure portal from All services, to Web, to App services - We want to add a new app, give it a name, subscription (should be free if you’re on a free trial, or if you already have one), resource group (create one or use existing), then click on the Create button down at the bottom of the panel.
- We should get a notification that the resource has been created. But it won’t immediately show up, so hit “Refresh” — I have other resources, but the AzureReactDemo2 is what I’m using here. You’ll click on the name of your newly created app, which is AzureReactDemo2 in my case.
- The blade shows you information about your app, the navigation to the left has everything you need to manage your application (overview, activity log, deployment center…).
For example, the Deployment Center is where the app deployment is managed, Slots is where things like staging, production, test are managed. Configuration is where things like environmental variables, node versions and — an important one — Kudu are managed.
The overview screen shows a general view of the application Status, URL… Click on the URL to see the live site.
The app is up and running!
What we’ve done is create a new App Service, but we have none of our code on Azure yet. As said earlier, all we need to do is to feed Azure the content of the build folder generated by building React for production, but we don’t have one yet. So let’s go local and get some React app.
Going local
We need to create a new React app, and install react-router as a dependency.
npx create-react-app azure-react-demo
cd azure-react-demo
We also want to install react-router (react-router-dom
, actually)
npm i react-router-dom
All things being equal, starting the app with npm start
, we should get the default page.
Because this will be about testing routes, I needed to make some pages. I’ve modified my local version and uploaded it to GitHub. I’m banking on the fact that you can find your way around react and react-router. Download a demo.
My folder looks like this:
The changed files have the following code:
// App.js
import React, { Component } from "react";
import "./App.css";
import Home from "./pages/Home";
import Page1 from "./pages/Page1";
import Page2 from "./pages/Page2";
import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
class App extends Component {
render() {
return (
<Router>
<Switch>
<Route exact path="/" component={Home} />
<Route path="/page1" component={Page1} />
<Route path="/page2" component={Page2} />
</Switch>
</Router>
);
}
}
export default App;
// Page1.js
import React from "react";
import { Link } from "react-router-dom";
const Page1 = () => {
return (
<div className="page page1">
<div className="flagTop" />
<div className="flagCenter">
<h1 className="country">Argentina (PAGE 1)</h1>
<div className="otherLinks">
<Link to="/page2">Nigeria</Link>
<Link to="/">Home</Link>
</div>
</div>
<div className="flagBottom" />
</div>
);
};
export default Page1;
// Page2.js
import React from "react";
import { Link } from "react-router-dom";
const Page2 = () => {
return (
<div className="page page2">
<div className="flagTop" />
<div className="flagCenter">
<h1 className="country">Nigeria (PAGE 2)</h1>
<div className="otherLinks">
<Link to="/page1">Argentina</Link>
<Link to="/">Home</Link>
</div>
</div>
<div className="flagBottom" />
</div>
);
};
export default Page2;
/* App.css */
html {
box-sizing: border-box;
}
body {
margin: 0;
}
.page {
display: grid;
grid-template-rows: repeat(3, 1fr);
height: 100vh;
}
.page1 .flagTop,
.page1 .flagBottom {
background-color: blue;
}
.page2 .flagTop,
.page2 .flagBottom {
background-color: green;
}
.flagCenter {
display: flex;
align-items: center;
flex-direction: column;
justify-content: center;
text-align: center;
}
.page a {
border: 2px solid currentColor;
font-weight: bold;
margin: 0 30px;
padding: 5px;
text-decoration: none;
text-transform: uppercase;
}
.flags {
display: flex;
width: 100%;
}
.flags > .page {
flex: 1;
}
Running the app works locally, so the routes deliver when links
are clicked and even when the page is refreshed.
Deploy the app to Azure
Now, let’s get it up on Azure! There are a few steps to make this happen.
Step 1: Head to the Deployment Center
On Azure, we need to go to the Deployment Center. There are quite a few options each with its pros and cons. We’ll be using Local Git (which means your local git app straight directly to Azure) for source control, Kudu for Build Provider.
Remember to click continue or finish when you select an option, or else, the portal will just keep staring at you.
After the third step, Azure generates a local git repo for you. And it gives you a remote link to point your react app to.
One thing to note at this point. When you push, Azure will ask for your GitHub credentials. It is under the deployment tab. There are two: App and User. App credential will be specific to an app. User will be general to all the apps you as a user has Read/Write access to. You can do without User Credentials and use App credentials, but I find that after a while, Azure stops asking for credentials and just tells me authentication failed automatically. I set a custom User Credentials. Either way, you should get past that.
In the React app, after modification, we need to build for production. This is important because what we want to upload is the content of the build folder.
We need to tell Kudu what node engine we’ll be using, or else, the build will most likely fail,
due to the reported fact that react-scripts
requires a node version higher than the default set on Azure. There are other ways to do that, but the simplest is to add a nodes engine in package.json
. I’m using version 10.0 here. Unfortunately, we can’t just add what we like, since Azure has Node versions it supports and the rest are unsupported. Check that with the CLI with the command: az webapp list-runtimes
Add the preferred node version to the package.json
file, as in:
"engines": {
"node": "10.0"
}
Step 2: Build the App
To build the React app, let’s run npm build
in the Terminal.
Step 3: Initialize the Git repo
Navigate into the build folder and initialize a Git repo in there. The URL to clone the repo is in the overview page. Depending on what credentials you’re using (App or User), it will be slightly different.
git init
git add .
git commit -m "Initial Commit"
git remote add azure <git clone url>
git push azure master
Now, visit the live app by using the URL on the overview page. As you can see, the app fails on /page2
refresh. Looking at the network tab, a 404 is thrown because the page tried to be fetched from the server — with client-side routing, as we have already set up, the page shouldn’t even be server fetched at all.
Configuring Azure to reconcile client and server side routing
In the public folder, let’s add a web.config
XML file with the following content:
<?xml version="1.0"?>
<configuration>
<system.webServer>
<rewrite>
<rules>
<rule name="React Routes" stopProcessing="true">
<match url=".*" />
<conditions logicalGrouping="MatchAll">
<add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
<add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
<add input="{REQUEST_URI}" pattern="^/(api)" negate="true" />
</conditions>
<action type="Rewrite" url="/" />
</rule>
</rules>
</rewrite>
</system.webServer>
</configuration>
I’ve intentionally decided to not format the code snippet because XML is strict about that. If you miss the formatting, the file has no effect. You can download an XML formatter for your text editor. For VSCode, that would be the XML Tools plugin.
The app can be built again at this point, although we’ll lose the Git info in the build folder since the new build overrides the old build. That means it would have to be added again, then pushed.
Now the app works as shown below! Whew.
We don’t want to have to npm run build
every time — that’s where continuous deployment comes in. Check out the link below for appropriate references.
Conclusion
There is a lot to Azure, as it can do a lot for you. That’s nice because there are times when you need it to do something that seems super specific — as we’ve seen here with client and server side routing reconciliation — and it already has your back.
That said, I’ll leave you with a couple of related resources you can turn to as you look to deploying a React app to Azure.
- Custom NodeJs Deployment on Azure Web App by Hao Luo: Learn more about Kudu and NodeJS deployment.
- Deploying a React App As a Static Site On Azure by Burke Holland: Even more options for deploying create-react-app to Microsoft Azure.
The post Deploying a Client-Side Rendered create-react-app to Microsoft Azure appeared first on CSS-Tricks.