John doe
John doe

Reputation: 339

Deploy Angular Universal to IIS

I am having a problem deploying my Node.js to the IIS, in my web.config:

> <handlers>
>         <add name="iisnode-ng-universal" path="dist/server.js" verb="*" modules="iisnode" />
>     </handlers>

 <rule name="ng-universal" stopProcessing="true">
        <match url=".*" />
        <conditions logicalGrouping="MatchAll">
            <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
            <add input="{HTTP_HOST}" pattern="test" />
        </conditions>
        <action type="Rewrite" url="dist/server.js" />
    </rule>

I am getting this error:

enter image description here

This how my web files tree looks:

web files

I tried to give full permission to IIS_IUSRS and IUSR but it does not seem to work. Like this post suggested: iisnode encountered an error when processing the request

Upvotes: 9

Views: 13472

Answers (4)

Abolfazl Roshanzamir
Abolfazl Roshanzamir

Reputation: 14892

If you want to run your angular application with server-side rendering:

1-Create a new project:

ng new anguniversal

2-Enable Server-side rendering (SSR)

ng add @nguniversal/express-engine

STEP 1

Firstly execute the command given below to build an Angular app with the express server.
npm run build:ssr

Now you can see the dist folder in your project folder.

STEP 2

Create a new folder in your windows server (for example name it as angular-universal) and copy the dist folder from the project folder here

STEP 3

Copy main.js file from dist/anguniversal/server folder and paste it direct in angular-universal folder.

STEP 4

Create and open the Web.config file(in the angular-universal folder) and add the code to it.

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <system.webServer>
    <handlers>
      <add name="iisnode" path="main.js" verb="*" modules="iisnode" />
    </handlers>
    <rewrite>
      <rules>
        <rule name="DynamicContent">
          <match url="/*" />
          <action type="Rewrite" url="main.js"/>
        </rule>
        <rule name="StaticContent" stopProcessing="true">
          <match url="([\S]+[.](jpg|jpeg|gif|css|png|js|ts|cscc|less|ico|html|map|svg))" />
          <action type="None" />
        </rule>
      </rules>
    </rewrite>
    <staticContent>
      <clientCache cacheControlMode="UseMaxAge" />
      <remove fileExtension=".svg" />
      <remove fileExtension=".eot" />
      <remove fileExtension=".ttf" />
      <remove fileExtension=".woff" />
      <remove fileExtension=".woff2" />
      <remove fileExtension=".otf" />
      <mimeMap fileExtension=".ttf" mimeType="application/octet-stream" />
      <mimeMap fileExtension=".svg" mimeType="image/svg+xml"  />
      <mimeMap fileExtension=".eot" mimeType="application/vnd.ms-fontobject" />
      <mimeMap fileExtension=".woff" mimeType="application/x-woff" />
      <mimeMap fileExtension=".woff2" mimeType="application/x-woff" />
      <mimeMap fileExtension=".otf" mimeType="application/otf" />
    </staticContent>
  </system.webServer>
</configuration>

STEP 5

Install IIS Node & URL Rewrite on the server.

STEP 6

Add Website… in IIS. enter image description here

STEP 7

In the IIS, go to Application Pools . for the Website you created (angular-universal). Then change the .NET CLR Version to No Managed Code.

enter image description here

Done, you can check the result.

Upvotes: 6

Sparker73
Sparker73

Reputation: 313

After upgrading my Angular 8 project to 9, and by that I mean that I upgraded practically everything to the latest, several things happened.

1.- webpack.config.js file was renamed as .bak so it seems that using webpack is not the way anymore, and indeed if I try to re-activate the webpack build nothing happens, it creates the build but the script doesn't run. Running with node server.js goes in and out without saying anything at all.

2.- Now the dist folder has no .js file in the root and the browser & server builds' folders are there like before, but even editing the web.config file to instruct the iisnode to run server/main.js instead of server.js throws 500 all the time no matters what I do.

3.- The only thing that works at first is runnign locally

npm run dev:ssr

Which means that the reference to the view in the browser build is there but it runs like it should run from the root asking for dist/browser.

I have modified the build path and server build path in angular.json to create the main.js file in the root of ./dist folder.

angular.json (outputPath are modified)

architect: {
  "build": {
    "builder": "@angular-devkit/build-angular:browser",
    "options": {
      "outputPath": "dist/browser",
      "index": "src/index.html",
      "main": "src/main.ts",
      "tsConfig": "tsconfig.app.json",
      ...
    },
    ...
  },
  "server": {
    "builder": "@angular-devkit/build-angular:server",
    "options": {
      "outputPath": "dist",
      "main": "server.ts",
      "tsConfig": "tsconfig.server.json"
    },
    ...
  },
  ...
}

Well, here comes the problem. The browser build generates ./dist/browser without problems and the server build indeed put the main.js file directly in ./dist but somehow it decided to delete the ./dist/browser folder and when running node main.js (web.config pointing to main.js) it can't find ./browser/index.html and of course not because the server build process managed to delete the browser folder.

The hacky solution, run a third build (the browser build again)

I hope this can be solved but to be honest this doesn't bother me at all, a computer runs the build, not me, but you know, it could be better and quicker if the server build proces doesn't delete de browser build folder.

Now it is deliciously deployed to Azure App Service directly from AppVeyor every time I deploy the selected build, no problems what soever.

package.json (Note the extra production build after the server build)

{
  ...
  "scripts": {
    ...
    "dev:ssr": "ng run Angular-Universal:serve-ssr",
    "serve:ssr": "node dist/main.js",
    "build:ssr": "npm run build:universal && ng run Angular-Universal:build:production",
    "start:ssr": "npm run build:ssr && npm run serve:ssr",
    "build:universal": "ng run Angular-Universal:build:production && ng run Angular-Universal:server:production",
    "prerender": "ng run Angular-Universal:prerender"
  },
  ...
}

Note that build:ssr script is doing both builds and then the browser build again. Also note that serve:ssr script was modified to run dist/main.js

Finally the server.ts file had to be modified to request the view from ./browser/index.html or ./dist/browser/index.html depending on the folder structure of the target system. When deployed to Azure App Service main.js requests browser/index.html but if the project is served with npm run dev:ssr or node main.js in my local environment with runs on macOS it requests dist/browser/index.html. Anyways a simple if can jump this obstacle.

server.ts (Changes on the app function)

// The Express app is exported so that it can be used by serverless Functions.
export function app() {
  const server = express();

  let distFolder: string;
  const path1 = join(process.cwd(), 'browser');         // Running from the deployed version (production)
  const path2 = join(process.cwd(), 'dist/browser');    // Running from the source (macOS local development)

  if (fs.existsSync(path1)) {
    distFolder = path1;
  } else if (fs.existsSync(path2)) {
    distFolder = path2;
  } else {
    return null;
  }
  console.log('distFolder:', distFolder);
  const indexHtml = fs.existsSync(join(distFolder, 'index.original.html')) ? 'index.original.html' : 'index';

  // Our Universal express-engine (found @ https://github.com/angular/universal/tree/master/modules/express-engine)
  server.engine('html', ngExpressEngine({
    bootstrap: AppServerModule,
  }));

  server.set('view engine', 'html');
  server.set('views', distFolder);

  // Example Express Rest API endpoints
  // server.get('/api/**', (req, res) => { });
  // Serve static files from /browser
  server.get('*.*', express.static(distFolder, {
    maxAge: '1y'
  }));

  // All regular routes use the Universal engine
  server.get('*', (req, res) => {
    res.render(indexHtml, { req, providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }] });
  });

  return server;
}

Just in case you were wondering what about copying the web.config file to the root of the dist folder, well, since I am using AppVeyor for CI/CD I have modified the artifact creation command to include web.config (from the repo source files directly into the .7z file)

And why not to include my web.config?

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <system.web>
    <httpRuntime enableVersionHeader="false" />
  </system.web>
  <system.webServer>
    <httpProtocol>
      <customHeaders>
        <add name="Strict-Transport-Security" value="max-age=31536000"/>
        <add name="X-Content-Type-Options" value="nosniff" />
        <add name="X-Frame-Options" value="DENY" />
        <add name="X-XSS-Protection" value="1; mode=block" />
        <remove name="X-Powered-By" />
      </customHeaders>
    </httpProtocol>
    <webSocket enabled="false" />
    <handlers>
      <!-- Indicates that the main.js file is a node.js site to be handled by the iisnode module -->
      <add name="iisnode" path="main.js" verb="*" modules="iisnode"/>
    </handlers>
    <rewrite>
      <rules>
        <rule name="HTTP to HTTPS redirect" stopProcessing="true">
          <match url="(.*)" />
          <conditions>
            <add input="{HTTPS}" pattern="off" ignoreCase="true" />
          </conditions>
          <action type="Redirect" url="https://{HTTP_HOST}/{R:1}" redirectType="Permanent" />
        </rule>
        <!-- Do not interfere with requests for node-inspector debugging -->
        <rule name="NodeInspector" patternSyntax="ECMAScript" stopProcessing="true">
          <match url="^main.js\/debug[\/]?" />
        </rule>
        <!-- All other URLs are mapped to the node.js site entry point -->
        <rule name="DynamicContent">
          <match url="^(?!.*login).*$"></match>
          <conditions>
            <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true"/>
          </conditions>
          <action type="Rewrite" url="main.js"/>
        </rule>
      </rules>
      <outboundRules>
        <rule name="Add Strict-Transport-Security when HTTPS" enabled="true">
          <match serverVariable="RESPONSE_Strict_Transport_Security" pattern=".*" />
          <conditions>
            <add input="{HTTPS}" pattern="on" ignoreCase="true" />
          </conditions>
          <action type="Rewrite" value="max-age=31536000" />
        </rule>
      </outboundRules>
    </rewrite>
    <!-- 'bin' directory has no special meaning in node.js and apps can be placed in it -->
    <security>
      <requestFiltering>
        <hiddenSegments>
          <remove segment="bin"/>
        </hiddenSegments>
      </requestFiltering>
    </security>
    <!-- Make sure error responses are left untouched -->
    <httpErrors existingResponse="PassThrough" />

    <!-- Restart the server if any of these files change -->
    <iisnode watchedFiles="web.config;*.js;browser/*.*" />
  </system.webServer>
</configuration>

I hope this could help others in my situation or at least useful for debate. No one likes to be scratching the head trying to make a successful deployment to an Azure App Service after upgrading to version 9. Please let me know if something can be improved, perhaps a build parameter to avoid browser build folder removal or whatever. Cheers.

Upvotes: 3

Supun De Silva
Supun De Silva

Reputation: 1487

The following flow worked for me after 2 days of trial and error (Angular 8)

  • Assuming you followed https://angular.io/guide/universal
    • Assuming you changed the src\tsconfig.server.json as -- use this comment if tsconfig.server.json is generated inside src folder already

{
  "extends": "./tsconfig.app.json",
  "compilerOptions": {
    "outDir": "../out-tsc/app-server",
    "module": "commonjs"
  },
  "files": [
    "src/main.server.ts" -- old
    "main.server.ts" -- use this
  ],
  "angularCompilerOptions": {
    "entryModule": "./app/app.server.module#AppServerModule"
  }
}

website_root:.
│   server.js
│   web.config
│
├───dist
│   └───browser
│           3rdpartylicenses.txt
│           favicon.ico
│           index.html
│           main-es2015.da6c4688eda738c0ae37.js
│           main-es5.da6c4688eda738c0ae37.js
│           polyfills-es2015.0ef207fb7b4761464817.js
│           polyfills-es5.2b9ce34c123ac007a8ba.js
│           runtime-es2015.e8a2810b3b08d6a1b6aa.js
│           runtime-es5.e8a2810b3b08d6a1b6aa.js
│           styles.09e2c710755c8867a460.css
│
└───server
        main.js

Deploy to Azure (inspired from this Link's answer by **r3plica)**

Only difference is an additional 'copy files' task moving the server folder to root dir

Note

If things do not work always use node to point at server.js this

node <path to server.js>

This tells you the most basic things w you have gotten wrong

Upvotes: 0

Darsan S Kumar
Darsan S Kumar

Reputation: 301

can you try the below code.. it works for me

<?xml version="1.0" encoding="utf-8"?>


<configuration>
    <system.webServer>        
      <handlers>
        <add name="iisnode" path="server.js" verb="*" modules="iisnode"/>
      </handlers>
      <rewrite>
        <rules>
            <rule name="DynamicContent">
                 <match url="/*" />
                 <action type="Rewrite" url="server.js"/>
            </rule>
       </rules>
      </rewrite>
    </system.webServer>
  </configuration>

Your server.js can be outside the dist folder.

Upvotes: 2

Related Questions